From 39be85c55248048a994fc265bffb1f835955b34a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Feb 2026 21:33:52 +0000 Subject: [PATCH 01/31] docs: add Home Assistant integration brainstorm Comprehensive analysis of integration options including: - Architecture options (Add-on + Integration, MQTT Discovery, Pure Native) - Recommended phased approach from MQTT discovery to full integration - Entity platforms, services, and config flow designs - Deep integration features (camera streams, automations, dashboard cards) - Configuration sync strategies and HACS distribution https://claude.ai/code/session_019LEwJ9ARyfpJqZMqTPpZxn --- docs/home-assistant-integration-brainstorm.md | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 docs/home-assistant-integration-brainstorm.md diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md new file mode 100644 index 00000000..4ca6e984 --- /dev/null +++ b/docs/home-assistant-integration-brainstorm.md @@ -0,0 +1,406 @@ +# HomeSec + Home Assistant: Deep Integration Brainstorm + +## Executive Summary + +HomeSec is already well-architected for Home Assistant integration with its plugin system and existing MQTT notifier. The key opportunity is to go from "basic MQTT alerts" to a **first-class Home Assistant experience** where users can: + +1. **Install** homesec directly from Home Assistant +2. **Configure** cameras, alerts, and AI settings through the HA UI +3. **Monitor** camera health, detection stats, and pipeline status as HA entities +4. **Automate** based on detection events with rich metadata +5. **View** clips and live streams directly in HA dashboards + +--- + +## Integration Architecture Options + +### Option A: Add-on + Native Integration (Recommended) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Home Assistant OS │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────┐ ┌─────────────────────────────┐ │ +│ │ HomeSec Add-on │ │ HomeSec Integration │ │ +│ │ (Docker container)│◄──►│ (HA Core component) │ │ +│ │ │ │ │ │ +│ │ - Pipeline service │ │ - Config flow UI │ │ +│ │ - RTSP sources │ │ - Entity platforms │ │ +│ │ - YOLO/VLM │ │ - Services │ │ +│ │ - Storage backends │ │ - Event subscriptions │ │ +│ └─────────────────────┘ └─────────────────────────────┘ │ +│ │ │ │ +│ └────────┬───────────────────┘ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Communication Layer │ │ +│ │ - REST API (new) for config/control │ │ +│ │ - WebSocket (new) for real-time events │ │ +│ │ - MQTT for alerts (existing) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Pros:** +- Best of both worlds: heavy processing isolated in add-on +- Full HA UI configuration via the integration +- Works with HA OS, Supervised, and standalone Docker +- Clean separation of concerns + +**Cons:** +- Two components to maintain +- Requires API layer between them + +--- + +### Option B: MQTT Discovery (Quick Win) + +Enhance the existing MQTT notifier to publish **discovery messages** that auto-create HA entities: + +``` +homeassistant/sensor/homesec/{camera_name}_status/config +homeassistant/sensor/homesec/{camera_name}_last_detection/config +homeassistant/binary_sensor/homesec/{camera_name}_motion/config +homeassistant/device_trigger/homesec/{camera_name}_alert/config +``` + +**Pros:** +- Minimal changes to homesec +- Works immediately with any HA installation +- No custom integration needed + +**Cons:** +- Limited configuration from HA (still need YAML) +- No deep UI integration + +--- + +### Option C: Pure Native Integration + +Move core homesec logic into a Home Assistant integration (runs in HA's Python environment). + +**Pros:** +- Single installation point +- Deep HA integration + +**Cons:** +- Heavy processing (YOLO, VLM) in HA's event loop could cause issues +- Harder to use outside HA +- Memory/CPU constraints + +--- + +## Recommended Implementation Plan + +### Phase 1: MQTT Discovery Enhancement (Quick Win) + +Enhance the existing MQTT notifier to publish discovery configs: + +```python +# New entities auto-created in HA: +- binary_sensor.homesec_{camera}_motion # Motion detected +- sensor.homesec_{camera}_last_activity # "person", "delivery", etc. +- sensor.homesec_{camera}_risk_level # LOW/MEDIUM/HIGH/CRITICAL +- sensor.homesec_{camera}_health # healthy/degraded/unhealthy +- device_tracker.homesec_{camera} # Online/offline status +``` + +**Changes to homesec:** +1. Add `mqtt_discovery: true` config option +2. On startup, publish discovery configs for each camera +3. Listen for HA birth message to republish discovery +4. Publish state updates on detection events + +### Phase 2: REST API for Configuration + +Add a new REST API to homesec for remote configuration: + +```yaml +# New endpoints +GET /api/v1/config # Get current config +PUT /api/v1/config # Update config +GET /api/v1/cameras # List cameras +POST /api/v1/cameras # Add camera +PUT /api/v1/cameras/{name} # Update camera +DELETE /api/v1/cameras/{name} # Remove camera +GET /api/v1/cameras/{name}/status # Camera status +POST /api/v1/cameras/{name}/test # Test camera connection +GET /api/v1/clips # List recent clips +GET /api/v1/clips/{id} # Get clip details +GET /api/v1/events # Event history +WS /api/v1/ws # Real-time events +``` + +### Phase 3: Home Assistant Add-on + +Create a Home Assistant add-on for easy installation: + +```yaml +# config.yaml (add-on manifest) +name: HomeSec +description: Self-hosted AI video security +version: 1.2.2 +slug: homesec +arch: [amd64, aarch64] +ports: + 8080/tcp: 8080 # API/Health + 8554/tcp: 8554 # RTSP proxy (optional) +map: + - config:rw # Store config + - media:rw # Store clips +services: + - mqtt:need # Requires MQTT broker +options: + config_file: /config/homesec/config.yaml +``` + +**Add-on features:** +- Bundled PostgreSQL (or use HA's) +- Auto-configure MQTT from HA's broker +- Ingress support for web UI (if we build one) +- Watchdog for auto-restart + +### Phase 4: Native Home Assistant Integration + +Build a custom integration (`custom_components/homesec/`): + +``` +custom_components/homesec/ +├── __init__.py # Setup, config entry +├── manifest.json # Integration metadata +├── config_flow.py # UI-based configuration +├── const.py # Constants +├── coordinator.py # DataUpdateCoordinator +├── camera.py # Camera entity platform +├── sensor.py # Sensor entities +├── binary_sensor.py # Motion sensors +├── switch.py # Enable/disable cameras +├── services.yaml # Service definitions +├── strings.json # UI strings +└── translations/ + └── en.json +``` + +**Entity Platforms:** + +| Platform | Entities | Description | +|----------|----------|-------------| +| `camera` | Per-camera | Live RTSP stream proxy | +| `binary_sensor` | `motion`, `person_detected` | Detection states | +| `sensor` | `last_activity`, `risk_level`, `clip_count` | Detection metadata | +| `switch` | `camera_enabled`, `alerts_enabled` | Per-camera toggles | +| `select` | `alert_sensitivity` | LOW/MEDIUM/HIGH | +| `image` | `last_snapshot` | Most recent detection frame | + +**Services:** + +```yaml +homesec.add_camera: + description: Add a new camera + fields: + name: Camera identifier + rtsp_url: RTSP stream URL + +homesec.set_alert_policy: + description: Configure alert policy for camera + fields: + camera: Camera name + min_risk_level: Minimum risk level to alert + activity_types: List of activity types to alert on + +homesec.process_clip: + description: Manually process a video clip + fields: + file_path: Path to video file +``` + +**Config Flow:** + +``` +Step 1: Connection +├── HomeSec URL: http://localhost:8080 +└── [Test Connection] + +Step 2: Camera Setup +├── Discovered cameras: [front_door, backyard, ...] +├── [x] front_door - Configure → +│ └── Alert sensitivity: [Medium ▼] +│ └── Notify on: [x] Person [x] Vehicle [ ] Animal +└── [+] Add new camera + +Step 3: Notifications +├── [x] Create HA notifications for alerts +├── [x] Add events to logbook +└── Device tracker for camera health +``` + +--- + +## Deep Integration Features + +### 1. Camera Streams in HA + +Proxy RTSP streams through homesec with authentication: + +```python +class HomesecCamera(Camera): + """Representation of a HomeSec camera.""" + + _attr_supported_features = CameraEntityFeature.STREAM + + async def stream_source(self) -> str: + """Return the stream source URL.""" + return f"rtsp://{self._host}:8554/{self._camera_name}" +``` + +### 2. Rich Event Data for Automations + +```yaml +# Example automation using homesec events +automation: + - alias: "Alert on suspicious person at night" + trigger: + - platform: state + entity_id: sensor.homesec_front_door_last_activity + to: "person" + condition: + - condition: state + entity_id: sensor.homesec_front_door_risk_level + state: "HIGH" + - condition: sun + after: sunset + action: + - service: notify.mobile_app + data: + title: "Security Alert" + message: "{{ state_attr('sensor.homesec_front_door_last_activity', 'summary') }}" + data: + image: "{{ state_attr('sensor.homesec_front_door_last_activity', 'snapshot_url') }}" + actions: + - action: VIEW_CLIP + title: "View Clip" + uri: "{{ state_attr('sensor.homesec_front_door_last_activity', 'clip_url') }}" +``` + +### 3. Dashboard Cards + +Create custom Lovelace cards: + +```yaml +type: custom:homesec-camera-card +entity: camera.homesec_front_door +show_detections: true +show_timeline: true +detection_overlay: true # Show bounding boxes on stream +``` + +### 4. Device Registry Integration + +Group all entities under device: + +```python +device_info = DeviceInfo( + identifiers={(DOMAIN, camera_name)}, + name=f"HomeSec {camera_name}", + manufacturer="HomeSec", + model="AI Security Camera", + sw_version="1.2.2", + via_device=(DOMAIN, "homesec_hub"), +) +``` + +### 5. Diagnostics + +Provide diagnostic data for troubleshooting: + +```python +async def async_get_config_entry_diagnostics(hass, entry): + return { + "config": {**entry.data, "api_key": "REDACTED"}, + "cameras": [...], + "health": await coordinator.api.get_health(), + "recent_events": [...], + } +``` + +--- + +## Configuration Sync Strategy + +### Option A: HA as Source of Truth + +HA integration manages config, pushes to homesec: + +``` +User edits in HA UI → Integration → REST API → HomeSec + ↓ + Restarts with new config +``` + +### Option B: HomeSec as Source of Truth + +HomeSec config is canonical, HA reads it: + +``` +User edits YAML → HomeSec → MQTT Discovery → HA entities created + → REST API → Integration reads state +``` + +### Option C: Hybrid (Recommended) + +- **Core config** (storage, database, VLM provider): HomeSec YAML +- **Camera config**: Editable from both, synced via API +- **Alert policies**: Editable from HA, stored in homesec + +--- + +## HACS Distribution + +For custom integration distribution: + +```json +// hacs.json +{ + "name": "HomeSec", + "render_readme": true, + "domains": ["camera", "sensor", "binary_sensor", "switch"], + "iot_class": "local_push" +} +``` + +```json +// manifest.json +{ + "domain": "homesec", + "name": "HomeSec", + "version": "1.0.0", + "documentation": "https://github.com/lan17/homesec", + "dependencies": ["mqtt"], + "codeowners": ["@lan17"], + "iot_class": "local_push", + "integration_type": "hub" +} +``` + +--- + +## Summary: Recommended Roadmap + +| Phase | Effort | Value | Description | +|-------|--------|-------|-------------| +| **1. MQTT Discovery** | Low | High | Auto-create entities from existing MQTT | +| **2. REST API** | Medium | High | Enable remote configuration | +| **3. Add-on** | Medium | High | One-click install for HA OS users | +| **4. Integration** | High | Very High | Full HA UI configuration | +| **5. Dashboard Cards** | Medium | Medium | Rich visualization | + +--- + +## References + +- [Home Assistant Developer Docs - Creating Integrations](https://developers.home-assistant.io/docs/creating_component_index/) +- [Home Assistant Config Flow](https://developers.home-assistant.io/docs/config_entries_config_flow_handler/) +- [Home Assistant Camera Entity](https://developers.home-assistant.io/docs/core/entity/camera/) +- [Home Assistant DataUpdateCoordinator](https://developers.home-assistant.io/docs/integration_fetching_data/) +- [MQTT Discovery](https://www.home-assistant.io/integrations/mqtt/) +- [HACS Documentation](https://www.hacs.xyz/) From 814142d1c0a876a1e412192c52301c9e67aa07ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Feb 2026 22:08:31 +0000 Subject: [PATCH 02/31] docs: add detailed Home Assistant implementation plan Comprehensive implementation guide covering: - Phase 1: MQTT Discovery enhancement with HA auto-entity creation - Phase 2: REST API for remote configuration (FastAPI) - Phase 3: Home Assistant Add-on for HA OS/Supervised - Phase 4: Native HA integration with config flow, coordinator, entities - Phase 5: Advanced features (camera streams, Lovelace cards) Includes complete code examples, file structures, testing strategy, migration guides, and acceptance criteria for each phase. https://claude.ai/code/session_019LEwJ9ARyfpJqZMqTPpZxn --- docs/home-assistant-implementation-plan.md | 2791 ++++++++++++++++++++ 1 file changed, 2791 insertions(+) create mode 100644 docs/home-assistant-implementation-plan.md diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md new file mode 100644 index 00000000..88b5c56e --- /dev/null +++ b/docs/home-assistant-implementation-plan.md @@ -0,0 +1,2791 @@ +# HomeSec + Home Assistant: Detailed Implementation Plan + +This document provides a comprehensive, step-by-step implementation plan for integrating HomeSec with Home Assistant. Each phase includes specific file changes, code examples, testing strategies, and acceptance criteria. + +--- + +## Table of Contents + +1. [Phase 1: MQTT Discovery Enhancement](#phase-1-mqtt-discovery-enhancement) +2. [Phase 2: REST API for Configuration](#phase-2-rest-api-for-configuration) +3. [Phase 3: Home Assistant Add-on](#phase-3-home-assistant-add-on) +4. [Phase 4: Native Home Assistant Integration](#phase-4-native-home-assistant-integration) +5. [Phase 5: Advanced Features](#phase-5-advanced-features) +6. [Testing Strategy](#testing-strategy) +7. [Migration Guide](#migration-guide) + +--- + +## Phase 1: MQTT Discovery Enhancement + +**Goal:** Auto-create Home Assistant entities from HomeSec without requiring manual HA configuration. + +**Estimated Effort:** 2-3 days + +### 1.1 Configuration Model Updates + +**File:** `src/homesec/models/config.py` + +Add MQTT discovery configuration to the existing `MQTTConfig`: + +```python +class MQTTDiscoveryConfig(BaseModel): + """Configuration for Home Assistant MQTT Discovery.""" + + enabled: bool = False + prefix: str = "homeassistant" # HA discovery prefix + node_id: str = "homesec" # Unique node identifier + device_name: str = "HomeSec" # Display name in HA + device_manufacturer: str = "HomeSec" + device_model: str = "AI Security Pipeline" + # Republish discovery on HA restart + subscribe_to_birth: bool = True + birth_topic: str = "homeassistant/status" + + +class MQTTConfig(BaseModel): + """Extended MQTT configuration.""" + + host: str + port: int = 1883 + username: str | None = None + password_env: str | None = None + topic_template: str = "homecam/alerts/{camera_name}" + qos: int = 1 + retain: bool = False + # New: Discovery settings + discovery: MQTTDiscoveryConfig = MQTTDiscoveryConfig() +``` + +### 1.2 Discovery Message Builder + +**New File:** `src/homesec/plugins/notifiers/mqtt_discovery.py` + +```python +"""MQTT Discovery message builder for Home Assistant.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any + +from homesec.models.config import MQTTDiscoveryConfig + + +@dataclass +class DiscoveryEntity: + """Represents a single HA entity discovery config.""" + + component: str # sensor, binary_sensor, camera, etc. + object_id: str # Unique ID within component + config: dict[str, Any] # Discovery payload + + +class MQTTDiscoveryBuilder: + """Builds MQTT discovery messages for Home Assistant.""" + + def __init__(self, config: MQTTDiscoveryConfig, version: str): + self.config = config + self.version = version + + def _device_info(self, camera_name: str) -> dict[str, Any]: + """Generate device info block for grouping entities.""" + return { + "identifiers": [f"{self.config.node_id}_{camera_name}"], + "name": f"{self.config.device_name} {camera_name}", + "manufacturer": self.config.device_manufacturer, + "model": self.config.device_model, + "sw_version": self.version, + "via_device": f"{self.config.node_id}_hub", + } + + def _hub_device_info(self) -> dict[str, Any]: + """Generate device info for the HomeSec hub.""" + return { + "identifiers": [f"{self.config.node_id}_hub"], + "name": self.config.device_name, + "manufacturer": self.config.device_manufacturer, + "model": f"{self.config.device_model} Hub", + "sw_version": self.version, + } + + def build_camera_entities(self, camera_name: str) -> list[DiscoveryEntity]: + """Build all discovery entities for a single camera.""" + entities = [] + base_topic = f"homesec/{camera_name}" + + # 1. Binary Sensor: Motion Detected + entities.append(DiscoveryEntity( + component="binary_sensor", + object_id=f"{camera_name}_motion", + config={ + "name": "Motion", + "unique_id": f"homesec_{camera_name}_motion", + "device_class": "motion", + "state_topic": f"{base_topic}/motion", + "payload_on": "ON", + "payload_off": "OFF", + "device": self._device_info(camera_name), + } + )) + + # 2. Binary Sensor: Person Detected + entities.append(DiscoveryEntity( + component="binary_sensor", + object_id=f"{camera_name}_person", + config={ + "name": "Person Detected", + "unique_id": f"homesec_{camera_name}_person", + "device_class": "occupancy", + "state_topic": f"{base_topic}/person", + "payload_on": "ON", + "payload_off": "OFF", + "device": self._device_info(camera_name), + } + )) + + # 3. Sensor: Last Activity Type + entities.append(DiscoveryEntity( + component="sensor", + object_id=f"{camera_name}_activity", + config={ + "name": "Last Activity", + "unique_id": f"homesec_{camera_name}_activity", + "state_topic": f"{base_topic}/activity", + "value_template": "{{ value_json.activity_type }}", + "json_attributes_topic": f"{base_topic}/activity", + "json_attributes_template": "{{ value_json | tojson }}", + "icon": "mdi:motion-sensor", + "device": self._device_info(camera_name), + } + )) + + # 4. Sensor: Risk Level + entities.append(DiscoveryEntity( + component="sensor", + object_id=f"{camera_name}_risk", + config={ + "name": "Risk Level", + "unique_id": f"homesec_{camera_name}_risk", + "state_topic": f"{base_topic}/risk", + "icon": "mdi:shield-alert", + "device": self._device_info(camera_name), + } + )) + + # 5. Sensor: Camera Health + entities.append(DiscoveryEntity( + component="sensor", + object_id=f"{camera_name}_health", + config={ + "name": "Health", + "unique_id": f"homesec_{camera_name}_health", + "state_topic": f"{base_topic}/health", + "icon": "mdi:heart-pulse", + "device": self._device_info(camera_name), + } + )) + + # 6. Sensor: Last Clip URL + entities.append(DiscoveryEntity( + component="sensor", + object_id=f"{camera_name}_last_clip", + config={ + "name": "Last Clip", + "unique_id": f"homesec_{camera_name}_last_clip", + "state_topic": f"{base_topic}/clip", + "value_template": "{{ value_json.view_url }}", + "json_attributes_topic": f"{base_topic}/clip", + "icon": "mdi:filmstrip", + "device": self._device_info(camera_name), + } + )) + + # 7. Image: Last Snapshot + entities.append(DiscoveryEntity( + component="image", + object_id=f"{camera_name}_snapshot", + config={ + "name": "Last Snapshot", + "unique_id": f"homesec_{camera_name}_snapshot", + "image_topic": f"{base_topic}/snapshot", + "device": self._device_info(camera_name), + } + )) + + # 8. Device Trigger: Alert + entities.append(DiscoveryEntity( + component="device_automation", + object_id=f"{camera_name}_alert_trigger", + config={ + "automation_type": "trigger", + "type": "alert", + "subtype": "security_alert", + "topic": f"{base_topic}/alert", + "device": self._device_info(camera_name), + } + )) + + return entities + + def build_hub_entities(self) -> list[DiscoveryEntity]: + """Build discovery entities for the HomeSec hub.""" + entities = [] + base_topic = "homesec/hub" + + # Hub Health + entities.append(DiscoveryEntity( + component="sensor", + object_id="hub_status", + config={ + "name": "Status", + "unique_id": "homesec_hub_status", + "state_topic": f"{base_topic}/status", + "icon": "mdi:server", + "device": self._hub_device_info(), + } + )) + + # Total Clips Today + entities.append(DiscoveryEntity( + component="sensor", + object_id="hub_clips_today", + config={ + "name": "Clips Today", + "unique_id": "homesec_hub_clips_today", + "state_topic": f"{base_topic}/stats", + "value_template": "{{ value_json.clips_today }}", + "icon": "mdi:filmstrip-box-multiple", + "device": self._hub_device_info(), + } + )) + + # Total Alerts Today + entities.append(DiscoveryEntity( + component="sensor", + object_id="hub_alerts_today", + config={ + "name": "Alerts Today", + "unique_id": "homesec_hub_alerts_today", + "state_topic": f"{base_topic}/stats", + "value_template": "{{ value_json.alerts_today }}", + "icon": "mdi:bell-alert", + "device": self._hub_device_info(), + } + )) + + return entities + + def get_discovery_topic(self, entity: DiscoveryEntity) -> str: + """Get the MQTT topic for publishing discovery config.""" + return f"{self.config.prefix}/{entity.component}/{self.config.node_id}/{entity.object_id}/config" + + def get_discovery_payload(self, entity: DiscoveryEntity) -> str: + """Get the JSON payload for discovery config.""" + return json.dumps(entity.config) +``` + +### 1.3 Enhanced MQTT Notifier + +**File:** `src/homesec/plugins/notifiers/mqtt.py` + +Extend the existing MQTT notifier to support discovery: + +```python +"""Enhanced MQTT Notifier with Home Assistant Discovery support.""" + +from __future__ import annotations + +import asyncio +import json +import logging +from typing import TYPE_CHECKING + +import aiomqtt + +from homesec.interfaces import Notifier +from homesec.models.alert import Alert +from homesec.models.config import MQTTConfig +from homesec.plugins.registry import plugin, PluginType + +from .mqtt_discovery import MQTTDiscoveryBuilder + +if TYPE_CHECKING: + from homesec.models.clip import Clip + +logger = logging.getLogger(__name__) + + +@plugin(plugin_type=PluginType.NOTIFIER, name="mqtt") +class MQTTNotifier(Notifier): + """MQTT notifier with Home Assistant discovery support.""" + + config_cls = MQTTConfig + + def __init__(self, config: MQTTConfig, version: str = "1.0.0"): + self.config = config + self.version = version + self._client: aiomqtt.Client | None = None + self._discovery_builder: MQTTDiscoveryBuilder | None = None + self._cameras: set[str] = set() + self._discovery_published: bool = False + + if config.discovery.enabled: + self._discovery_builder = MQTTDiscoveryBuilder( + config.discovery, version + ) + + async def start(self) -> None: + """Start the MQTT client and publish discovery if enabled.""" + self._client = aiomqtt.Client( + hostname=self.config.host, + port=self.config.port, + username=self.config.username, + password=self._get_password(), + ) + await self._client.__aenter__() + + if self.config.discovery.enabled and self.config.discovery.subscribe_to_birth: + # Subscribe to HA birth topic to republish discovery on HA restart + await self._client.subscribe(self.config.discovery.birth_topic) + asyncio.create_task(self._listen_for_birth()) + + async def _listen_for_birth(self) -> None: + """Listen for Home Assistant birth messages to republish discovery.""" + async for message in self._client.messages: + if message.topic.matches(self.config.discovery.birth_topic): + if message.payload.decode() == "online": + logger.info("Home Assistant came online, republishing discovery") + await self._publish_discovery() + + async def register_camera(self, camera_name: str) -> None: + """Register a camera for discovery.""" + self._cameras.add(camera_name) + if self.config.discovery.enabled and self._discovery_published: + # Publish discovery for new camera immediately + await self._publish_camera_discovery(camera_name) + + async def _publish_discovery(self) -> None: + """Publish all discovery messages.""" + if not self._discovery_builder or not self._client: + return + + # Publish hub entities + for entity in self._discovery_builder.build_hub_entities(): + topic = self._discovery_builder.get_discovery_topic(entity) + payload = self._discovery_builder.get_discovery_payload(entity) + await self._client.publish(topic, payload, retain=True) + logger.debug(f"Published discovery: {topic}") + + # Publish camera entities + for camera_name in self._cameras: + await self._publish_camera_discovery(camera_name) + + self._discovery_published = True + logger.info(f"Published MQTT discovery for {len(self._cameras)} cameras") + + async def _publish_camera_discovery(self, camera_name: str) -> None: + """Publish discovery messages for a single camera.""" + if not self._discovery_builder or not self._client: + return + + for entity in self._discovery_builder.build_camera_entities(camera_name): + topic = self._discovery_builder.get_discovery_topic(entity) + payload = self._discovery_builder.get_discovery_payload(entity) + await self._client.publish(topic, payload, retain=True) + + async def notify(self, alert: Alert) -> None: + """Send alert and update entity states.""" + if not self._client: + raise RuntimeError("MQTT client not started") + + camera = alert.camera_name + base_topic = f"homesec/{camera}" + + # Original alert topic (backwards compatible) + alert_topic = self.config.topic_template.format(camera_name=camera) + await self._client.publish( + alert_topic, + alert.model_dump_json(), + qos=self.config.qos, + retain=self.config.retain, + ) + + # If discovery is enabled, also publish to state topics + if self.config.discovery.enabled: + # Motion state + await self._client.publish( + f"{base_topic}/motion", + "ON", + retain=True, + ) + + # Person detection (if detected) + if alert.analysis and "person" in (alert.analysis.detected_objects or []): + await self._client.publish( + f"{base_topic}/person", + "ON", + retain=True, + ) + + # Activity details + activity_payload = { + "activity_type": alert.activity_type or "unknown", + "summary": alert.summary, + "risk_level": alert.risk_level.value if alert.risk_level else None, + "timestamp": alert.ts.isoformat(), + "clip_id": alert.clip_id, + } + await self._client.publish( + f"{base_topic}/activity", + json.dumps(activity_payload), + retain=True, + ) + + # Risk level + await self._client.publish( + f"{base_topic}/risk", + alert.risk_level.value if alert.risk_level else "UNKNOWN", + retain=True, + ) + + # Clip info + clip_payload = { + "clip_id": alert.clip_id, + "view_url": alert.view_url, + "storage_uri": alert.storage_uri, + "timestamp": alert.ts.isoformat(), + } + await self._client.publish( + f"{base_topic}/clip", + json.dumps(clip_payload), + retain=True, + ) + + # Device trigger for automations + await self._client.publish( + f"{base_topic}/alert", + json.dumps({"event_type": "alert", **activity_payload}), + ) + + # Schedule motion reset after 30 seconds + asyncio.create_task(self._reset_motion_state(camera)) + + async def _reset_motion_state(self, camera_name: str, delay: float = 30.0) -> None: + """Reset motion binary sensor after delay.""" + await asyncio.sleep(delay) + if self._client: + await self._client.publish( + f"homesec/{camera_name}/motion", + "OFF", + retain=True, + ) + await self._client.publish( + f"homesec/{camera_name}/person", + "OFF", + retain=True, + ) + + async def publish_health(self, camera_name: str, health: str) -> None: + """Publish camera health state.""" + if self._client and self.config.discovery.enabled: + await self._client.publish( + f"homesec/{camera_name}/health", + health, + retain=True, + ) + + async def publish_hub_stats(self, clips_today: int, alerts_today: int) -> None: + """Publish hub statistics.""" + if self._client and self.config.discovery.enabled: + await self._client.publish( + "homesec/hub/status", + "online", + retain=True, + ) + await self._client.publish( + "homesec/hub/stats", + json.dumps({ + "clips_today": clips_today, + "alerts_today": alerts_today, + }), + retain=True, + ) + + async def stop(self) -> None: + """Stop the MQTT client.""" + if self._client: + # Publish offline status + if self.config.discovery.enabled: + await self._client.publish( + "homesec/hub/status", + "offline", + retain=True, + ) + await self._client.__aexit__(None, None, None) + self._client = None +``` + +### 1.4 Application Integration + +**File:** `src/homesec/app.py` + +Update the application to register cameras with the MQTT notifier: + +```python +# In HomesecApp.start() method, after initializing sources: + +# Register cameras with MQTT notifier for discovery +for notifier in self.notifiers: + if hasattr(notifier, 'register_camera'): + for source in self.sources: + await notifier.register_camera(source.camera_name) + + # Publish initial discovery + if hasattr(notifier, '_publish_discovery'): + await notifier._publish_discovery() +``` + +### 1.5 Configuration Example + +**File:** `config/example.yaml` (add section) + +```yaml +notifiers: + - type: mqtt + host: localhost + port: 1883 + username: homeassistant + password_env: MQTT_PASSWORD + topic_template: "homecam/alerts/{camera_name}" + qos: 1 + retain: false + discovery: + enabled: true + prefix: homeassistant + node_id: homesec + device_name: HomeSec + subscribe_to_birth: true + birth_topic: homeassistant/status +``` + +### 1.6 Testing + +**New File:** `tests/unit/plugins/notifiers/test_mqtt_discovery.py` + +```python +"""Tests for MQTT Discovery functionality.""" + +import pytest +import json + +from homesec.models.config import MQTTDiscoveryConfig +from homesec.plugins.notifiers.mqtt_discovery import MQTTDiscoveryBuilder + + +class TestMQTTDiscoveryBuilder: + """Tests for MQTTDiscoveryBuilder.""" + + @pytest.fixture + def builder(self): + config = MQTTDiscoveryConfig(enabled=True) + return MQTTDiscoveryBuilder(config, "1.2.3") + + def test_build_camera_entities_creates_all_types(self, builder): + """Should create all expected entity types for a camera.""" + entities = builder.build_camera_entities("front_door") + + components = {e.component for e in entities} + assert "binary_sensor" in components + assert "sensor" in components + assert "image" in components + assert "device_automation" in components + + def test_discovery_topic_format(self, builder): + """Should generate correct discovery topic.""" + entities = builder.build_camera_entities("front_door") + motion_entity = next(e for e in entities if "motion" in e.object_id) + + topic = builder.get_discovery_topic(motion_entity) + assert topic == "homeassistant/binary_sensor/homesec/front_door_motion/config" + + def test_device_info_grouping(self, builder): + """Should group entities under same device.""" + entities = builder.build_camera_entities("front_door") + + device_ids = { + e.config["device"]["identifiers"][0] + for e in entities + if "device" in e.config + } + assert len(device_ids) == 1 + assert "homesec_front_door" in device_ids.pop() + + def test_hub_entities(self, builder): + """Should create hub-level entities.""" + entities = builder.build_hub_entities() + + assert len(entities) >= 2 + assert any("status" in e.object_id for e in entities) + assert any("clips_today" in e.object_id for e in entities) +``` + +### 1.7 Acceptance Criteria + +- [ ] `mqtt.discovery.enabled: true` causes discovery messages to be published +- [ ] All cameras appear as devices in Home Assistant +- [ ] Binary sensors (motion, person) work correctly +- [ ] Sensors (activity, risk, health) update on alerts +- [ ] Device triggers fire for automations +- [ ] Discovery republishes when HA restarts (birth message) +- [ ] Backwards compatible with existing MQTT alert topic + +--- + +## Phase 2: REST API for Configuration + +**Goal:** Enable remote configuration and monitoring of HomeSec via HTTP API. + +**Estimated Effort:** 5-7 days + +### 2.1 API Framework Setup + +**New File:** `src/homesec/api/__init__.py` + +```python +"""HomeSec REST API package.""" + +from .server import create_app, APIServer + +__all__ = ["create_app", "APIServer"] +``` + +**New File:** `src/homesec/api/server.py` + +```python +"""REST API server for HomeSec.""" + +from __future__ import annotations + +import logging +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from .routes import cameras, clips, config, events, health, websocket + +if TYPE_CHECKING: + from homesec.app import HomesecApp + +logger = logging.getLogger(__name__) + + +def create_app(homesec_app: HomesecApp) -> FastAPI: + """Create the FastAPI application.""" + + @asynccontextmanager + async def lifespan(app: FastAPI): + # Store reference to HomeSec app + app.state.homesec = homesec_app + yield + + app = FastAPI( + title="HomeSec API", + description="REST API for HomeSec video security pipeline", + version="1.0.0", + lifespan=lifespan, + ) + + # CORS for Home Assistant frontend + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Configure appropriately + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Register routes + app.include_router(health.router, prefix="/api/v1", tags=["health"]) + app.include_router(config.router, prefix="/api/v1", tags=["config"]) + app.include_router(cameras.router, prefix="/api/v1", tags=["cameras"]) + app.include_router(clips.router, prefix="/api/v1", tags=["clips"]) + app.include_router(events.router, prefix="/api/v1", tags=["events"]) + app.include_router(websocket.router, prefix="/api/v1", tags=["websocket"]) + + return app + + +class APIServer: + """Manages the API server lifecycle.""" + + def __init__(self, app: FastAPI, host: str = "0.0.0.0", port: int = 8080): + self.app = app + self.host = host + self.port = port + self._server = None + + async def start(self) -> None: + """Start the API server.""" + import uvicorn + + config = uvicorn.Config( + self.app, + host=self.host, + port=self.port, + log_level="info", + ) + self._server = uvicorn.Server(config) + await self._server.serve() + + async def stop(self) -> None: + """Stop the API server.""" + if self._server: + self._server.should_exit = True +``` + +### 2.2 API Routes + +**New File:** `src/homesec/api/routes/cameras.py` + +```python +"""Camera management API routes.""" + +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel + +from ..dependencies import get_homesec_app + +router = APIRouter(prefix="/cameras") + + +class CameraCreate(BaseModel): + """Request model for creating a camera.""" + name: str + type: str # rtsp, ftp, local_folder + config: dict # Type-specific configuration + + +class CameraUpdate(BaseModel): + """Request model for updating a camera.""" + config: dict | None = None + alert_policy: dict | None = None + + +class CameraResponse(BaseModel): + """Response model for camera.""" + name: str + type: str + healthy: bool + last_heartbeat: float | None + config: dict + alert_policy: dict | None + + +class CameraListResponse(BaseModel): + """Response model for camera list.""" + cameras: list[CameraResponse] + total: int + + +@router.get("", response_model=CameraListResponse) +async def list_cameras(app=Depends(get_homesec_app)): + """List all configured cameras.""" + cameras = [] + for source in app.sources: + cameras.append(CameraResponse( + name=source.camera_name, + type=source.source_type, + healthy=source.is_healthy(), + last_heartbeat=source.last_heartbeat(), + config=source.get_config(), + alert_policy=app.get_camera_alert_policy(source.camera_name), + )) + return CameraListResponse(cameras=cameras, total=len(cameras)) + + +@router.get("/{camera_name}", response_model=CameraResponse) +async def get_camera(camera_name: str, app=Depends(get_homesec_app)): + """Get a specific camera's configuration.""" + source = app.get_source(camera_name) + if not source: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Camera '{camera_name}' not found", + ) + return CameraResponse( + name=source.camera_name, + type=source.source_type, + healthy=source.is_healthy(), + last_heartbeat=source.last_heartbeat(), + config=source.get_config(), + alert_policy=app.get_camera_alert_policy(camera_name), + ) + + +@router.post("", response_model=CameraResponse, status_code=status.HTTP_201_CREATED) +async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): + """Add a new camera.""" + if app.get_source(camera.name): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Camera '{camera.name}' already exists", + ) + + try: + source = await app.add_camera( + name=camera.name, + source_type=camera.type, + config=camera.config, + ) + return CameraResponse( + name=source.camera_name, + type=source.source_type, + healthy=source.is_healthy(), + last_heartbeat=source.last_heartbeat(), + config=source.get_config(), + alert_policy=None, + ) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + +@router.put("/{camera_name}", response_model=CameraResponse) +async def update_camera( + camera_name: str, + update: CameraUpdate, + app=Depends(get_homesec_app), +): + """Update a camera's configuration.""" + source = app.get_source(camera_name) + if not source: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Camera '{camera_name}' not found", + ) + + if update.config: + await app.update_camera_config(camera_name, update.config) + + if update.alert_policy: + await app.update_camera_alert_policy(camera_name, update.alert_policy) + + source = app.get_source(camera_name) + return CameraResponse( + name=source.camera_name, + type=source.source_type, + healthy=source.is_healthy(), + last_heartbeat=source.last_heartbeat(), + config=source.get_config(), + alert_policy=app.get_camera_alert_policy(camera_name), + ) + + +@router.delete("/{camera_name}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_camera(camera_name: str, app=Depends(get_homesec_app)): + """Remove a camera.""" + source = app.get_source(camera_name) + if not source: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Camera '{camera_name}' not found", + ) + + await app.remove_camera(camera_name) + + +@router.get("/{camera_name}/status") +async def get_camera_status(camera_name: str, app=Depends(get_homesec_app)): + """Get detailed camera status.""" + source = app.get_source(camera_name) + if not source: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Camera '{camera_name}' not found", + ) + + return { + "name": camera_name, + "healthy": source.is_healthy(), + "last_heartbeat": source.last_heartbeat(), + "last_heartbeat_age_s": source.last_heartbeat_age(), + "stats": await source.get_stats(), + } + + +@router.post("/{camera_name}/test") +async def test_camera(camera_name: str, app=Depends(get_homesec_app)): + """Test camera connection.""" + source = app.get_source(camera_name) + if not source: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Camera '{camera_name}' not found", + ) + + try: + result = await source.ping() + return {"success": result, "message": "Connection successful" if result else "Connection failed"} + except Exception as e: + return {"success": False, "message": str(e)} +``` + +**New File:** `src/homesec/api/routes/clips.py` + +```python +"""Clip management API routes.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import BaseModel + +from ..dependencies import get_homesec_app + +router = APIRouter(prefix="/clips") + + +class ClipResponse(BaseModel): + """Response model for a clip.""" + clip_id: str + camera_name: str + status: str + created_at: datetime + storage_uri: str | None + view_url: str | None + filter_result: dict | None + analysis_result: dict | None + alert_decision: dict | None + + +class ClipListResponse(BaseModel): + """Response model for clip list.""" + clips: list[ClipResponse] + total: int + page: int + page_size: int + + +@router.get("", response_model=ClipListResponse) +async def list_clips( + app=Depends(get_homesec_app), + camera: str | None = None, + status: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=100), +): + """List clips with optional filtering.""" + clips, total = await app.repository.list_clips( + camera=camera, + status=status, + since=since, + until=until, + offset=(page - 1) * page_size, + limit=page_size, + ) + + return ClipListResponse( + clips=[ClipResponse(**c.model_dump()) for c in clips], + total=total, + page=page, + page_size=page_size, + ) + + +@router.get("/{clip_id}", response_model=ClipResponse) +async def get_clip(clip_id: str, app=Depends(get_homesec_app)): + """Get a specific clip.""" + clip = await app.repository.get_clip(clip_id) + if not clip: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Clip '{clip_id}' not found", + ) + return ClipResponse(**clip.model_dump()) + + +@router.delete("/{clip_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_clip(clip_id: str, app=Depends(get_homesec_app)): + """Delete a clip.""" + clip = await app.repository.get_clip(clip_id) + if not clip: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Clip '{clip_id}' not found", + ) + await app.repository.delete_clip(clip_id) + + +@router.post("/{clip_id}/reprocess", status_code=status.HTTP_202_ACCEPTED) +async def reprocess_clip(clip_id: str, app=Depends(get_homesec_app)): + """Reprocess a clip through the pipeline.""" + clip = await app.repository.get_clip(clip_id) + if not clip: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Clip '{clip_id}' not found", + ) + + await app.pipeline.reprocess(clip_id) + return {"message": "Clip queued for reprocessing", "clip_id": clip_id} +``` + +**New File:** `src/homesec/api/routes/events.py` + +```python +"""Event history API routes.""" + +from __future__ import annotations + +from datetime import datetime + +from fastapi import APIRouter, Depends, Query +from pydantic import BaseModel + +from ..dependencies import get_homesec_app + +router = APIRouter(prefix="/events") + + +class EventResponse(BaseModel): + """Response model for an event.""" + id: int + clip_id: str + event_type: str + timestamp: datetime + event_data: dict + + +class EventListResponse(BaseModel): + """Response model for event list.""" + events: list[EventResponse] + total: int + + +@router.get("", response_model=EventListResponse) +async def list_events( + app=Depends(get_homesec_app), + clip_id: str | None = None, + event_type: str | None = None, + camera: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + limit: int = Query(100, ge=1, le=1000), +): + """List events with optional filtering.""" + events, total = await app.repository.list_events( + clip_id=clip_id, + event_type=event_type, + camera=camera, + since=since, + until=until, + limit=limit, + ) + + return EventListResponse( + events=[EventResponse(**e.model_dump()) for e in events], + total=total, + ) +``` + +**New File:** `src/homesec/api/routes/websocket.py` + +```python +"""WebSocket routes for real-time updates.""" + +from __future__ import annotations + +import asyncio +import json +import logging +from typing import TYPE_CHECKING + +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +if TYPE_CHECKING: + from homesec.app import HomesecApp + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +class ConnectionManager: + """Manages WebSocket connections.""" + + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def broadcast(self, message: dict): + """Broadcast message to all connected clients.""" + for connection in self.active_connections: + try: + await connection.send_json(message) + except Exception: + pass # Connection might be closed + + +manager = ConnectionManager() + + +@router.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """WebSocket endpoint for real-time events.""" + await manager.connect(websocket) + + app: HomesecApp = websocket.app.state.homesec + + # Subscribe to app events + event_queue = asyncio.Queue() + + async def event_handler(event_type: str, data: dict): + await event_queue.put({"type": event_type, "data": data}) + + app.subscribe_events(event_handler) + + try: + while True: + # Wait for either client message or app event + done, pending = await asyncio.wait( + [ + asyncio.create_task(websocket.receive_text()), + asyncio.create_task(event_queue.get()), + ], + return_when=asyncio.FIRST_COMPLETED, + ) + + for task in done: + result = task.result() + + if isinstance(result, str): + # Message from client + try: + msg = json.loads(result) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong"}) + except json.JSONDecodeError: + pass + else: + # Event from app + await websocket.send_json(result) + + for task in pending: + task.cancel() + + except WebSocketDisconnect: + manager.disconnect(websocket) + app.unsubscribe_events(event_handler) +``` + +### 2.3 API Configuration + +**File:** `src/homesec/models/config.py` (add) + +```python +class APIConfig(BaseModel): + """Configuration for the REST API.""" + + enabled: bool = True + host: str = "0.0.0.0" + port: int = 8080 + cors_origins: list[str] = ["*"] + # Authentication (optional) + auth_enabled: bool = False + api_key_env: str | None = None + # Rate limiting + rate_limit_enabled: bool = True + rate_limit_requests: int = 100 + rate_limit_window_seconds: int = 60 +``` + +### 2.4 OpenAPI Documentation + +The FastAPI app automatically generates OpenAPI docs at `/api/v1/docs` (Swagger UI) and `/api/v1/redoc` (ReDoc). + +### 2.5 Acceptance Criteria + +- [ ] All CRUD operations for cameras work +- [ ] Clip listing with filtering works +- [ ] Event history API works +- [ ] WebSocket broadcasts real-time events +- [ ] OpenAPI documentation is accurate +- [ ] CORS works for Home Assistant frontend +- [ ] API authentication (optional) works + +--- + +## Phase 3: Home Assistant Add-on + +**Goal:** Provide one-click installation for Home Assistant OS/Supervised users. + +**Estimated Effort:** 3-4 days + +### 3.1 Add-on Repository Structure + +**New Repository:** `homesec-ha-addons` + +``` +homesec-ha-addons/ +├── README.md +├── repository.json +└── homesec/ + ├── config.yaml # Add-on manifest + ├── Dockerfile # Container build + ├── build.yaml # Build configuration + ├── run.sh # Startup script + ├── DOCS.md # Documentation + ├── CHANGELOG.md # Version history + ├── icon.png # Add-on icon (512x512) + ├── logo.png # Add-on logo (256x256) + └── translations/ + └── en.yaml # UI strings +``` + +### 3.2 Add-on Manifest + +**File:** `homesec/config.yaml` + +```yaml +name: HomeSec +version: "1.2.2" +slug: homesec +description: Self-hosted AI video security pipeline +url: https://github.com/lan17/homesec +arch: + - amd64 + - aarch64 +init: false +homeassistant_api: true +hassio_api: true +host_network: false +ingress: true +ingress_port: 8080 +ingress_stream: true +panel_icon: mdi:cctv +panel_title: HomeSec + +# Port mappings +ports: + 8080/tcp: null # API (exposed via ingress) + 8554/tcp: 8554 # RTSP proxy (if implemented) + +# Volume mappings +map: + - config:rw # /config - HA config directory + - media:rw # /media - Media storage + - share:rw # /share - Shared data + +# Services +services: + - mqtt:want # Use HA's MQTT broker + +# Options schema +schema: + config_path: str? + log_level: list(debug|info|warning|error)? + # Database + database_url: str? + # Storage + storage_type: list(local|dropbox)? + storage_path: str? + dropbox_token: password? + # VLM + vlm_enabled: bool? + openai_api_key: password? + openai_model: str? + # MQTT Discovery + mqtt_discovery: bool? + +# Default options +options: + config_path: /config/homesec/config.yaml + log_level: info + database_url: "" + storage_type: local + storage_path: /media/homesec/clips + vlm_enabled: false + mqtt_discovery: true + +# Startup dependencies +startup: services +stage: stable + +# Advanced options +advanced: true +privileged: [] +apparmor: true + +# Watchdog for auto-restart +watchdog: http://[HOST]:[PORT:8080]/api/v1/health +``` + +### 3.3 Add-on Dockerfile + +**File:** `homesec/Dockerfile` + +```dockerfile +# syntax=docker/dockerfile:1 +ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.8 +FROM ${BUILD_FROM} + +# Install runtime dependencies +RUN apk add --no-cache \ + python3 \ + py3-pip \ + ffmpeg \ + postgresql-client \ + opencv \ + && rm -rf /var/cache/apk/* + +# Install HomeSec +ARG HOMESEC_VERSION=1.2.2 +RUN pip3 install --no-cache-dir homesec==${HOMESEC_VERSION} + +# Copy root filesystem +COPY rootfs / + +# Set working directory +WORKDIR /app + +# Labels +LABEL \ + io.hass.name="HomeSec" \ + io.hass.description="Self-hosted AI video security pipeline" \ + io.hass.version="${HOMESEC_VERSION}" \ + io.hass.type="addon" \ + io.hass.arch="amd64|aarch64" + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD curl -f http://localhost:8080/api/v1/health || exit 1 +``` + +### 3.4 Startup Script + +**File:** `homesec/rootfs/etc/services.d/homesec/run` + +```bash +#!/usr/bin/with-contenv bashio +# ============================================================================== +# HomeSec Add-on Startup Script +# ============================================================================== + +# Read options +CONFIG_PATH=$(bashio::config 'config_path') +LOG_LEVEL=$(bashio::config 'log_level') +DATABASE_URL=$(bashio::config 'database_url') +STORAGE_TYPE=$(bashio::config 'storage_type') +STORAGE_PATH=$(bashio::config 'storage_path') +VLM_ENABLED=$(bashio::config 'vlm_enabled') +MQTT_DISCOVERY=$(bashio::config 'mqtt_discovery') + +# Get MQTT credentials from HA if available +if bashio::services.available "mqtt"; then + MQTT_HOST=$(bashio::services mqtt "host") + MQTT_PORT=$(bashio::services mqtt "port") + MQTT_USER=$(bashio::services mqtt "username") + MQTT_PASS=$(bashio::services mqtt "password") + + export MQTT_HOST MQTT_PORT MQTT_USER MQTT_PASS + bashio::log.info "Using Home Assistant MQTT broker at ${MQTT_HOST}:${MQTT_PORT}" +fi + +# Create config directory if needed +mkdir -p "$(dirname "${CONFIG_PATH}")" + +# Generate config if it doesn't exist +if [[ ! -f "${CONFIG_PATH}" ]]; then + bashio::log.info "Generating initial configuration at ${CONFIG_PATH}" + cat > "${CONFIG_PATH}" << EOF +version: 1 + +cameras: [] + +storage: + type: ${STORAGE_TYPE} + path: ${STORAGE_PATH} + +state_store: + type: postgres + url_env: DATABASE_URL + +notifiers: + - type: mqtt + host_env: MQTT_HOST + port_env: MQTT_PORT + username_env: MQTT_USER + password_env: MQTT_PASS + discovery: + enabled: ${MQTT_DISCOVERY} + +health: + enabled: true + port: 8080 + +api: + enabled: true + port: 8080 +EOF +fi + +# Set up database if using local PostgreSQL +if [[ -z "${DATABASE_URL}" ]]; then + bashio::log.info "No database URL specified, using SQLite fallback" + export DATABASE_URL="sqlite:///config/homesec/homesec.db" +fi + +# Export secrets from config +if bashio::config.has_value 'dropbox_token'; then + export DROPBOX_TOKEN=$(bashio::config 'dropbox_token') +fi + +if bashio::config.has_value 'openai_api_key'; then + export OPENAI_API_KEY=$(bashio::config 'openai_api_key') +fi + +# Create storage directory +mkdir -p "${STORAGE_PATH}" + +bashio::log.info "Starting HomeSec..." + +# Run HomeSec +exec python3 -m homesec.cli run \ + --config "${CONFIG_PATH}" \ + --log-level "${LOG_LEVEL}" +``` + +### 3.5 Ingress Configuration + +**File:** `homesec/rootfs/etc/nginx/includes/ingress.conf` + +```nginx +# Proxy to HomeSec API +location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Timeouts for long-running connections + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; +} +``` + +### 3.6 Acceptance Criteria + +- [ ] Add-on installs successfully from repository +- [ ] Auto-configures MQTT from Home Assistant +- [ ] Ingress provides access to API/UI +- [ ] Configuration options work correctly +- [ ] Watchdog restarts on failure +- [ ] Logs are accessible in HA +- [ ] Works on both amd64 and aarch64 + +--- + +## Phase 4: Native Home Assistant Integration + +**Goal:** Full UI-based configuration and deep entity integration in Home Assistant. + +**Estimated Effort:** 7-10 days + +### 4.1 Integration Structure + +**Directory:** `custom_components/homesec/` + +``` +custom_components/homesec/ +├── __init__.py # Setup and entry points +├── manifest.json # Integration metadata +├── const.py # Constants +├── config_flow.py # UI configuration flow +├── coordinator.py # Data update coordinator +├── entity.py # Base entity class +├── camera.py # Camera platform +├── sensor.py # Sensor platform +├── binary_sensor.py # Binary sensor platform +├── switch.py # Switch platform +├── select.py # Select platform +├── image.py # Image platform +├── diagnostics.py # Diagnostic data +├── services.yaml # Service definitions +├── strings.json # UI strings +└── translations/ + └── en.json # English translations +``` + +### 4.2 Manifest + +**File:** `custom_components/homesec/manifest.json` + +```json +{ + "domain": "homesec", + "name": "HomeSec", + "codeowners": ["@lan17"], + "config_flow": true, + "dependencies": ["mqtt"], + "documentation": "https://github.com/lan17/homesec", + "integration_type": "hub", + "iot_class": "local_push", + "issue_tracker": "https://github.com/lan17/homesec/issues", + "requirements": ["aiohttp>=3.8.0"], + "version": "1.0.0" +} +``` + +### 4.3 Constants + +**File:** `custom_components/homesec/const.py` + +```python +"""Constants for HomeSec integration.""" + +from typing import Final + +DOMAIN: Final = "homesec" + +# Configuration keys +CONF_HOST: Final = "host" +CONF_PORT: Final = "port" +CONF_API_KEY: Final = "api_key" +CONF_VERIFY_SSL: Final = "verify_ssl" + +# Default values +DEFAULT_PORT: Final = 8080 +DEFAULT_VERIFY_SSL: Final = True + +# Platforms +PLATFORMS: Final = [ + "binary_sensor", + "camera", + "image", + "select", + "sensor", + "switch", +] + +# Entity categories +DIAGNOSTIC_SENSORS: Final = ["health", "last_heartbeat"] + +# Update intervals +SCAN_INTERVAL_SECONDS: Final = 30 +WEBSOCKET_RECONNECT_DELAY: Final = 5 + +# Attributes +ATTR_CLIP_ID: Final = "clip_id" +ATTR_CLIP_URL: Final = "clip_url" +ATTR_SNAPSHOT_URL: Final = "snapshot_url" +ATTR_ACTIVITY_TYPE: Final = "activity_type" +ATTR_RISK_LEVEL: Final = "risk_level" +ATTR_SUMMARY: Final = "summary" +ATTR_DETECTED_OBJECTS: Final = "detected_objects" +``` + +### 4.4 Config Flow + +**File:** `custom_components/homesec/config_flow.py` + +```python +"""Config flow for HomeSec integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +import aiohttp +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_API_KEY +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + DOMAIN, + DEFAULT_PORT, + CONF_VERIFY_SSL, + DEFAULT_VERIFY_SSL, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_API_KEY): str, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + } +) + + +async def validate_connection( + hass: HomeAssistant, + host: str, + port: int, + api_key: str | None = None, + verify_ssl: bool = True, +) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + session = async_get_clientsession(hass, verify_ssl=verify_ssl) + + headers = {} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + url = f"http://{host}:{port}/api/v1/health" + + async with session.get(url, headers=headers) as response: + if response.status == 401: + raise InvalidAuth + if response.status != 200: + raise CannotConnect + + data = await response.json() + return { + "title": f"HomeSec ({host})", + "version": data.get("version", "unknown"), + "cameras": data.get("sources", []), + } + + +class HomesecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for HomeSec.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._host: str | None = None + self._port: int = DEFAULT_PORT + self._api_key: str | None = None + self._verify_ssl: bool = DEFAULT_VERIFY_SSL + self._cameras: list[dict] = [] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + self._host = user_input[CONF_HOST] + self._port = user_input.get(CONF_PORT, DEFAULT_PORT) + self._api_key = user_input.get(CONF_API_KEY) + self._verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + + try: + info = await validate_connection( + self.hass, + self._host, + self._port, + self._api_key, + self._verify_ssl, + ) + self._cameras = info.get("cameras", []) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Check if already configured + await self.async_set_unique_id(f"homesec_{self._host}_{self._port}") + self._abort_if_unique_id_configured() + + return await self.async_step_cameras() + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + ) + + async def async_step_cameras( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle camera configuration step.""" + if user_input is not None: + # Create the config entry + return self.async_create_entry( + title=f"HomeSec ({self._host})", + data={ + CONF_HOST: self._host, + CONF_PORT: self._port, + CONF_API_KEY: self._api_key, + CONF_VERIFY_SSL: self._verify_ssl, + }, + options={ + "cameras": user_input.get("cameras", []), + }, + ) + + # Build camera selection schema + camera_names = [c["name"] for c in self._cameras] + schema = vol.Schema( + { + vol.Optional( + "cameras", + default=camera_names, + ): vol.All( + vol.Coerce(list), + [vol.In(camera_names)], + ), + } + ) + + return self.async_show_form( + step_id="cameras", + data_schema=schema, + description_placeholders={ + "camera_count": str(len(self._cameras)), + }, + ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options flow for HomeSec.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + # Fetch current cameras from HomeSec + coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] + camera_names = [c["name"] for c in coordinator.data.get("cameras", [])] + current_cameras = self.config_entry.options.get("cameras", camera_names) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + "cameras", + default=current_cameras, + ): vol.All( + vol.Coerce(list), + [vol.In(camera_names)], + ), + vol.Optional( + "scan_interval", + default=self.config_entry.options.get("scan_interval", 30), + ): vol.All(vol.Coerce(int), vol.Range(min=10, max=300)), + } + ), + ) + + +class CannotConnect(Exception): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(Exception): + """Error to indicate there is invalid auth.""" +``` + +### 4.5 Data Coordinator + +**File:** `custom_components/homesec/coordinator.py` + +```python +"""Data coordinator for HomeSec integration.""" + +from __future__ import annotations + +import asyncio +import logging +from datetime import timedelta +from typing import Any + +import aiohttp +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import DOMAIN, SCAN_INTERVAL_SECONDS, WEBSOCKET_RECONNECT_DELAY + +_LOGGER = logging.getLogger(__name__) + + +class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator to manage HomeSec data updates.""" + + def __init__( + self, + hass: HomeAssistant, + host: str, + port: int, + api_key: str | None = None, + verify_ssl: bool = True, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=SCAN_INTERVAL_SECONDS), + ) + self.host = host + self.port = port + self.api_key = api_key + self.verify_ssl = verify_ssl + self._session = async_get_clientsession(hass, verify_ssl=verify_ssl) + self._ws_task: asyncio.Task | None = None + self._event_callbacks: list[callable] = [] + + @property + def base_url(self) -> str: + """Return the base URL for the API.""" + return f"http://{self.host}:{self.port}/api/v1" + + @property + def _headers(self) -> dict[str, str]: + """Return headers for API requests.""" + headers = {"Content-Type": "application/json"} + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" + return headers + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch data from HomeSec API.""" + try: + async with asyncio.timeout(10): + # Fetch health status + async with self._session.get( + f"{self.base_url}/health", + headers=self._headers, + ) as response: + response.raise_for_status() + health = await response.json() + + # Fetch cameras + async with self._session.get( + f"{self.base_url}/cameras", + headers=self._headers, + ) as response: + response.raise_for_status() + cameras_data = await response.json() + + return { + "health": health, + "cameras": cameras_data.get("cameras", []), + "connected": True, + } + + except asyncio.TimeoutError as err: + raise UpdateFailed("Timeout connecting to HomeSec") from err + except aiohttp.ClientError as err: + raise UpdateFailed(f"Error communicating with HomeSec: {err}") from err + + async def async_start_websocket(self) -> None: + """Start WebSocket connection for real-time updates.""" + if self._ws_task is not None: + return + self._ws_task = asyncio.create_task(self._websocket_loop()) + + async def async_stop_websocket(self) -> None: + """Stop WebSocket connection.""" + if self._ws_task is not None: + self._ws_task.cancel() + try: + await self._ws_task + except asyncio.CancelledError: + pass + self._ws_task = None + + async def _websocket_loop(self) -> None: + """Maintain WebSocket connection and handle events.""" + ws_url = f"ws://{self.host}:{self.port}/api/v1/ws" + + while True: + try: + async with self._session.ws_connect(ws_url) as ws: + _LOGGER.info("Connected to HomeSec WebSocket") + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + data = msg.json() + await self._handle_ws_event(data) + elif msg.type == aiohttp.WSMsgType.ERROR: + _LOGGER.error("WebSocket error: %s", ws.exception()) + break + + except aiohttp.ClientError as err: + _LOGGER.error("WebSocket connection error: %s", err) + + except asyncio.CancelledError: + _LOGGER.info("WebSocket task cancelled") + return + + _LOGGER.info( + "WebSocket disconnected, reconnecting in %s seconds", + WEBSOCKET_RECONNECT_DELAY, + ) + await asyncio.sleep(WEBSOCKET_RECONNECT_DELAY) + + async def _handle_ws_event(self, data: dict[str, Any]) -> None: + """Handle incoming WebSocket event.""" + event_type = data.get("type") + event_data = data.get("data", {}) + + _LOGGER.debug("Received WebSocket event: %s", event_type) + + # Trigger immediate data refresh for certain events + if event_type in ["alert", "clip_recorded", "camera_status_changed"]: + await self.async_request_refresh() + + # Notify registered callbacks + for callback in self._event_callbacks: + try: + await callback(event_type, event_data) + except Exception: + _LOGGER.exception("Error in event callback") + + def register_event_callback(self, callback: callable) -> callable: + """Register a callback for WebSocket events.""" + self._event_callbacks.append(callback) + + def remove(): + self._event_callbacks.remove(callback) + + return remove + + # API Methods + + async def async_get_cameras(self) -> list[dict]: + """Get list of cameras.""" + async with self._session.get( + f"{self.base_url}/cameras", + headers=self._headers, + ) as response: + response.raise_for_status() + data = await response.json() + return data.get("cameras", []) + + async def async_add_camera( + self, + name: str, + camera_type: str, + config: dict, + ) -> dict: + """Add a new camera.""" + async with self._session.post( + f"{self.base_url}/cameras", + headers=self._headers, + json={"name": name, "type": camera_type, "config": config}, + ) as response: + response.raise_for_status() + return await response.json() + + async def async_update_camera( + self, + camera_name: str, + config: dict | None = None, + alert_policy: dict | None = None, + ) -> dict: + """Update camera configuration.""" + payload = {} + if config is not None: + payload["config"] = config + if alert_policy is not None: + payload["alert_policy"] = alert_policy + + async with self._session.put( + f"{self.base_url}/cameras/{camera_name}", + headers=self._headers, + json=payload, + ) as response: + response.raise_for_status() + return await response.json() + + async def async_delete_camera(self, camera_name: str) -> None: + """Delete a camera.""" + async with self._session.delete( + f"{self.base_url}/cameras/{camera_name}", + headers=self._headers, + ) as response: + response.raise_for_status() + + async def async_test_camera(self, camera_name: str) -> dict: + """Test camera connection.""" + async with self._session.post( + f"{self.base_url}/cameras/{camera_name}/test", + headers=self._headers, + ) as response: + response.raise_for_status() + return await response.json() +``` + +### 4.6 Entity Platforms + +**File:** `custom_components/homesec/sensor.py` + +```python +"""Sensor platform for HomeSec integration.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, ATTR_ACTIVITY_TYPE, ATTR_RISK_LEVEL, ATTR_SUMMARY +from .coordinator import HomesecCoordinator +from .entity import HomesecEntity + +CAMERA_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="last_activity", + name="Last Activity", + icon="mdi:motion-sensor", + ), + SensorEntityDescription( + key="risk_level", + name="Risk Level", + icon="mdi:shield-alert", + ), + SensorEntityDescription( + key="health", + name="Health", + icon="mdi:heart-pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="clips_today", + name="Clips Today", + icon="mdi:filmstrip-box", + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up HomeSec sensors.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensorEntity] = [] + + for camera in coordinator.data.get("cameras", []): + camera_name = camera["name"] + for description in CAMERA_SENSORS: + entities.append( + HomesecCameraSensor(coordinator, camera_name, description) + ) + + async_add_entities(entities) + + +class HomesecCameraSensor(HomesecEntity, SensorEntity): + """Representation of a HomeSec camera sensor.""" + + def __init__( + self, + coordinator: HomesecCoordinator, + camera_name: str, + description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, camera_name) + self.entity_description = description + self._attr_unique_id = f"{camera_name}_{description.key}" + + @property + def native_value(self) -> str | int | None: + """Return the state of the sensor.""" + camera = self._get_camera_data() + if not camera: + return None + + key = self.entity_description.key + + if key == "last_activity": + return camera.get("last_activity", {}).get("activity_type") + elif key == "risk_level": + return camera.get("last_activity", {}).get("risk_level") + elif key == "health": + return "healthy" if camera.get("healthy") else "unhealthy" + elif key == "clips_today": + return camera.get("stats", {}).get("clips_today", 0) + + return None + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return additional state attributes.""" + camera = self._get_camera_data() + if not camera: + return {} + + key = self.entity_description.key + attrs = {} + + if key == "last_activity": + activity = camera.get("last_activity", {}) + attrs[ATTR_ACTIVITY_TYPE] = activity.get("activity_type") + attrs[ATTR_RISK_LEVEL] = activity.get("risk_level") + attrs[ATTR_SUMMARY] = activity.get("summary") + attrs["clip_id"] = activity.get("clip_id") + attrs["view_url"] = activity.get("view_url") + attrs["timestamp"] = activity.get("timestamp") + + return attrs +``` + +**File:** `custom_components/homesec/binary_sensor.py` + +```python +"""Binary sensor platform for HomeSec integration.""" + +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import HomesecCoordinator +from .entity import HomesecEntity + +CAMERA_BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="motion", + name="Motion", + device_class=BinarySensorDeviceClass.MOTION, + ), + BinarySensorEntityDescription( + key="person", + name="Person Detected", + device_class=BinarySensorDeviceClass.OCCUPANCY, + ), + BinarySensorEntityDescription( + key="online", + name="Online", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up HomeSec binary sensors.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[BinarySensorEntity] = [] + + for camera in coordinator.data.get("cameras", []): + camera_name = camera["name"] + for description in CAMERA_BINARY_SENSORS: + entities.append( + HomesecCameraBinarySensor(coordinator, camera_name, description) + ) + + async_add_entities(entities) + + +class HomesecCameraBinarySensor(HomesecEntity, BinarySensorEntity): + """Representation of a HomeSec camera binary sensor.""" + + def __init__( + self, + coordinator: HomesecCoordinator, + camera_name: str, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator, camera_name) + self.entity_description = description + self._attr_unique_id = f"{camera_name}_{description.key}" + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + camera = self._get_camera_data() + if not camera: + return None + + key = self.entity_description.key + + if key == "motion": + return camera.get("motion_detected", False) + elif key == "person": + return camera.get("person_detected", False) + elif key == "online": + return camera.get("healthy", False) + + return None +``` + +### 4.7 Services + +**File:** `custom_components/homesec/services.yaml` + +```yaml +add_camera: + name: Add Camera + description: Add a new camera to HomeSec + fields: + name: + name: Name + description: Unique identifier for the camera + required: true + example: "front_door" + selector: + text: + type: + name: Type + description: Camera source type + required: true + example: "rtsp" + selector: + select: + options: + - "rtsp" + - "ftp" + - "local_folder" + rtsp_url: + name: RTSP URL + description: RTSP stream URL (for RTSP type) + example: "rtsp://192.168.1.100:554/stream" + selector: + text: + +remove_camera: + name: Remove Camera + description: Remove a camera from HomeSec + target: + device: + integration: homesec + +set_alert_policy: + name: Set Alert Policy + description: Configure alert policy for a camera + target: + device: + integration: homesec + fields: + min_risk_level: + name: Minimum Risk Level + description: Minimum risk level to trigger alerts + required: true + selector: + select: + options: + - "LOW" + - "MEDIUM" + - "HIGH" + - "CRITICAL" + activity_types: + name: Activity Types + description: Activity types that trigger alerts + selector: + select: + multiple: true + options: + - "person" + - "vehicle" + - "animal" + - "package" + - "suspicious" + +test_camera: + name: Test Camera + description: Test camera connection + target: + device: + integration: homesec +``` + +### 4.8 Translations + +**File:** `custom_components/homesec/translations/en.json` + +```json +{ + "config": { + "step": { + "user": { + "title": "Connect to HomeSec", + "description": "Enter the connection details for your HomeSec instance.", + "data": { + "host": "Host", + "port": "Port", + "api_key": "API Key (optional)", + "verify_ssl": "Verify SSL certificate" + } + }, + "cameras": { + "title": "Select Cameras", + "description": "Found {camera_count} cameras. Select which ones to add to Home Assistant.", + "data": { + "cameras": "Cameras" + } + } + }, + "error": { + "cannot_connect": "Failed to connect to HomeSec", + "invalid_auth": "Invalid API key", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "This HomeSec instance is already configured" + } + }, + "options": { + "step": { + "init": { + "title": "HomeSec Options", + "data": { + "cameras": "Enabled cameras", + "scan_interval": "Update interval (seconds)" + } + } + } + }, + "entity": { + "sensor": { + "last_activity": { + "name": "Last Activity" + }, + "risk_level": { + "name": "Risk Level" + }, + "health": { + "name": "Health" + }, + "clips_today": { + "name": "Clips Today" + } + }, + "binary_sensor": { + "motion": { + "name": "Motion" + }, + "person": { + "name": "Person Detected" + }, + "online": { + "name": "Online" + } + } + }, + "services": { + "add_camera": { + "name": "Add Camera", + "description": "Add a new camera to HomeSec" + }, + "remove_camera": { + "name": "Remove Camera", + "description": "Remove a camera from HomeSec" + }, + "set_alert_policy": { + "name": "Set Alert Policy", + "description": "Configure alert policy for a camera" + }, + "test_camera": { + "name": "Test Camera", + "description": "Test camera connection" + } + } +} +``` + +### 4.9 Acceptance Criteria + +- [ ] Config flow connects to HomeSec and discovers cameras +- [ ] All entity platforms create entities correctly +- [ ] DataUpdateCoordinator fetches data at correct intervals +- [ ] WebSocket connection provides real-time updates +- [ ] Services work correctly (add/remove camera, set policy, test) +- [ ] Options flow allows reconfiguration +- [ ] Diagnostics provide useful debug information +- [ ] All strings are translatable +- [ ] HACS installation works + +--- + +## Phase 5: Advanced Features + +**Goal:** Premium features for power users. + +**Estimated Effort:** 5-7 days + +### 5.1 Camera Entity with Live Stream + +**File:** `custom_components/homesec/camera.py` + +```python +"""Camera platform for HomeSec integration.""" + +from __future__ import annotations + +from homeassistant.components.camera import Camera, CameraEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import HomesecCoordinator +from .entity import HomesecEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up HomeSec cameras.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities = [ + HomesecCameraEntity(coordinator, camera["name"]) + for camera in coordinator.data.get("cameras", []) + ] + + async_add_entities(entities) + + +class HomesecCameraEntity(HomesecEntity, Camera): + """Representation of a HomeSec camera.""" + + _attr_supported_features = CameraEntityFeature.STREAM + + def __init__( + self, + coordinator: HomesecCoordinator, + camera_name: str, + ) -> None: + """Initialize the camera.""" + HomesecEntity.__init__(self, coordinator, camera_name) + Camera.__init__(self) + self._attr_unique_id = f"{camera_name}_camera" + self._attr_name = None # Use device name + + async def stream_source(self) -> str | None: + """Return the source of the stream.""" + camera = self._get_camera_data() + if not camera: + return None + + # Get RTSP URL from camera config + config = camera.get("config", {}) + rtsp_url = config.get("rtsp_url") + + if rtsp_url: + return rtsp_url + + # Fallback to HomeSec RTSP proxy if available + return f"rtsp://{self.coordinator.host}:8554/{self._camera_name}" + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return a still image from the camera.""" + camera = self._get_camera_data() + if not camera: + return None + + # Request snapshot from HomeSec API + try: + async with self.coordinator._session.get( + f"{self.coordinator.base_url}/cameras/{self._camera_name}/snapshot", + headers=self.coordinator._headers, + ) as response: + if response.status == 200: + return await response.read() + except Exception: + pass + + return None +``` + +### 5.2 Custom Lovelace Card (Optional) + +**File:** `custom_components/homesec/www/homesec-camera-card.js` + +```javascript +class HomesecCameraCard extends HTMLElement { + set hass(hass) { + if (!this.content) { + this.innerHTML = ` + +
+
+ +
+
+ Motion + Person +
+
+
+
+
+
+
+ + `; + this.content = true; + } + + const config = this._config; + const cameraEntity = config.entity; + const state = hass.states[cameraEntity]; + + if (state) { + // Update camera image + const img = this.querySelector("#camera-image"); + img.src = `/api/camera_proxy/${cameraEntity}?token=${state.attributes.access_token}`; + + // Update detection badges + const motionEntity = cameraEntity.replace("camera.", "binary_sensor.") + "_motion"; + const personEntity = cameraEntity.replace("camera.", "binary_sensor.") + "_person"; + + const motionBadge = this.querySelector("#motion-badge"); + const personBadge = this.querySelector("#person-badge"); + + motionBadge.className = `badge ${hass.states[motionEntity]?.state === "on" ? "active" : "inactive"}`; + personBadge.className = `badge ${hass.states[personEntity]?.state === "on" ? "active" : "inactive"}`; + + // Update activity info + const activityEntity = cameraEntity.replace("camera.", "sensor.") + "_last_activity"; + const activityState = hass.states[activityEntity]; + + if (activityState) { + this.querySelector("#activity").textContent = `Activity: ${activityState.state}`; + this.querySelector("#risk").textContent = `Risk: ${activityState.attributes.risk_level || "N/A"}`; + } + } + } + + setConfig(config) { + if (!config.entity) { + throw new Error("You need to define an entity"); + } + this._config = config; + } + + getCardSize() { + return 4; + } +} + +customElements.define("homesec-camera-card", HomesecCameraCard); +``` + +### 5.3 Event Timeline Panel (Optional) + +Create a custom panel for viewing event history with timeline visualization. + +### 5.4 Acceptance Criteria + +- [ ] Camera entities show live streams +- [ ] Snapshot images work +- [ ] Custom Lovelace card displays detection overlays +- [ ] Event timeline shows historical data + +--- + +## Testing Strategy + +### Unit Tests + +``` +tests/ +├── unit/ +│ ├── api/ +│ │ ├── test_routes_cameras.py +│ │ ├── test_routes_clips.py +│ │ └── test_routes_events.py +│ ├── plugins/ +│ │ └── notifiers/ +│ │ └── test_mqtt_discovery.py +│ └── models/ +│ └── test_config_mqtt.py +├── integration/ +│ ├── test_mqtt_ha_integration.py +│ ├── test_api_full_flow.py +│ └── test_ha_addon.py +└── e2e/ + └── test_ha_config_flow.py +``` + +### Integration Tests + +1. **MQTT Discovery Tests:** + - Publish discovery → verify entities appear in HA + - HA restart → verify discovery republishes + - Camera add → verify new entities created + +2. **API Tests:** + - Full CRUD cycle for cameras + - WebSocket event delivery + - Authentication flows + +3. **Add-on Tests:** + - Installation on HA OS + - MQTT auto-configuration + - Ingress access + +### Manual Testing Checklist + +- [ ] Install add-on from repository +- [ ] Configure via HA UI +- [ ] Add/remove cameras +- [ ] Verify entities update on alerts +- [ ] Test automations with HomeSec triggers +- [ ] Verify camera streams in dashboard + +--- + +## Migration Guide + +### From Standalone to Add-on + +1. Export current `config.yaml` +2. Install HomeSec add-on +3. Copy config to `/config/homesec/config.yaml` +4. Update database URL if using external Postgres +5. Start add-on + +### From MQTT-only to Full Integration + +1. Keep existing MQTT configuration +2. Install custom integration via HACS +3. Configure integration with HomeSec URL +4. Entities will be created alongside MQTT entities +5. Optionally disable MQTT discovery to avoid duplicates + +--- + +## Appendix: File Change Summary + +### Phase 1: MQTT Discovery +- `src/homesec/models/config.py` - Add MQTTDiscoveryConfig +- `src/homesec/plugins/notifiers/mqtt.py` - Enhance with discovery +- `src/homesec/plugins/notifiers/mqtt_discovery.py` - New file +- `src/homesec/app.py` - Register cameras with notifier +- `config/example.yaml` - Add discovery example +- `tests/unit/plugins/notifiers/test_mqtt_discovery.py` - New tests + +### Phase 2: REST API +- `src/homesec/api/` - New package (server.py, routes/*, dependencies.py) +- `src/homesec/models/config.py` - Add APIConfig +- `src/homesec/app.py` - Integrate API server +- `pyproject.toml` - Add fastapi, uvicorn dependencies + +### Phase 3: Add-on +- New repository: `homesec-ha-addons/` +- `homesec/config.yaml` - Add-on manifest +- `homesec/Dockerfile` - Container build +- `homesec/rootfs/` - Startup scripts, nginx config + +### Phase 4: Integration +- `custom_components/homesec/` - Full integration package +- HACS repository configuration + +### Phase 5: Advanced +- `custom_components/homesec/camera.py` - Camera platform +- `custom_components/homesec/www/` - Lovelace cards + +--- + +## Timeline Summary + +| Phase | Duration | Dependencies | +|-------|----------|--------------| +| Phase 1: MQTT Discovery | 2-3 days | None | +| Phase 2: REST API | 5-7 days | None | +| Phase 3: Add-on | 3-4 days | Phase 2 | +| Phase 4: Integration | 7-10 days | Phase 2 | +| Phase 5: Advanced | 5-7 days | Phase 4 | + +**Total: 22-31 days** + +Phases 1 and 2 can run in parallel. Phase 3 and 4 can run in parallel after Phase 2. From 5dafb633024eb9adb32d0c3832af6540cae12510 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 15:37:31 -0800 Subject: [PATCH 03/31] docs: update HA plan and brainstorm --- docs/home-assistant-implementation-plan.md | 436 +++++++----------- docs/home-assistant-integration-brainstorm.md | 42 +- 2 files changed, 191 insertions(+), 287 deletions(-) diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index 88b5c56e..04ea75f6 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -4,15 +4,37 @@ This document provides a comprehensive, step-by-step implementation plan for int --- +## Decision Snapshot (2026-02-01) + +- Chosen approach: Add-on + native integration with HomeSec as the runtime. +- Required: runtime add/remove cameras and other config changes from HA. +- API stack: FastAPI, async endpoints only, async SQLAlchemy only. +- Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA can trigger restart. +- Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). +- Tests: Given/When/Then comments required for all new tests. +- P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). + +## Execution Order (Option A) + +1. Phase 2: REST API for Configuration (control plane) +2. Phase 4: Native Home Assistant Integration +3. Phase 3: Home Assistant Add-on +4. Phase 1: MQTT Discovery Enhancement (optional parallel track) +5. Phase 5: Advanced Features + +--- + ## Table of Contents -1. [Phase 1: MQTT Discovery Enhancement](#phase-1-mqtt-discovery-enhancement) -2. [Phase 2: REST API for Configuration](#phase-2-rest-api-for-configuration) -3. [Phase 3: Home Assistant Add-on](#phase-3-home-assistant-add-on) -4. [Phase 4: Native Home Assistant Integration](#phase-4-native-home-assistant-integration) -5. [Phase 5: Advanced Features](#phase-5-advanced-features) -6. [Testing Strategy](#testing-strategy) -7. [Migration Guide](#migration-guide) +1. [Decision Snapshot](#decision-snapshot-2026-02-01) +2. [Execution Order](#execution-order-option-a) +3. [Phase 1: MQTT Discovery Enhancement](#phase-1-mqtt-discovery-enhancement) +4. [Phase 2: REST API for Configuration](#phase-2-rest-api-for-configuration) +5. [Phase 3: Home Assistant Add-on](#phase-3-home-assistant-add-on) +6. [Phase 4: Native Home Assistant Integration](#phase-4-native-home-assistant-integration) +7. [Phase 5: Advanced Features](#phase-5-advanced-features) +8. [Testing Strategy](#testing-strategy) +9. [Migration Guide](#migration-guide) --- @@ -48,8 +70,7 @@ class MQTTConfig(BaseModel): host: str port: int = 1883 - username: str | None = None - password_env: str | None = None + auth: MQTTAuthConfig | None = None topic_template: str = "homecam/alerts/{camera_name}" qos: int = 1 retain: bool = False @@ -290,261 +311,38 @@ class MQTTDiscoveryBuilder: **File:** `src/homesec/plugins/notifiers/mqtt.py` -Extend the existing MQTT notifier to support discovery: - -```python -"""Enhanced MQTT Notifier with Home Assistant Discovery support.""" - -from __future__ import annotations - -import asyncio -import json -import logging -from typing import TYPE_CHECKING - -import aiomqtt - -from homesec.interfaces import Notifier -from homesec.models.alert import Alert -from homesec.models.config import MQTTConfig -from homesec.plugins.registry import plugin, PluginType +Extend the existing `paho-mqtt` notifier (do not switch libraries). Key changes: -from .mqtt_discovery import MQTTDiscoveryBuilder +- Keep the `Notifier.send()` interface and use `asyncio.to_thread` for publish operations. +- Add discovery publish helpers using `MQTTDiscoveryBuilder` (new file). +- Store camera names in-memory to publish discovery per camera. +- On HA birth message, republish discovery and current state topics. +- Keep backward-compatible alert topic publishing. -if TYPE_CHECKING: - from homesec.models.clip import Clip - -logger = logging.getLogger(__name__) +Implementation notes: - -@plugin(plugin_type=PluginType.NOTIFIER, name="mqtt") -class MQTTNotifier(Notifier): - """MQTT notifier with Home Assistant discovery support.""" - - config_cls = MQTTConfig - - def __init__(self, config: MQTTConfig, version: str = "1.0.0"): - self.config = config - self.version = version - self._client: aiomqtt.Client | None = None - self._discovery_builder: MQTTDiscoveryBuilder | None = None - self._cameras: set[str] = set() - self._discovery_published: bool = False - - if config.discovery.enabled: - self._discovery_builder = MQTTDiscoveryBuilder( - config.discovery, version - ) - - async def start(self) -> None: - """Start the MQTT client and publish discovery if enabled.""" - self._client = aiomqtt.Client( - hostname=self.config.host, - port=self.config.port, - username=self.config.username, - password=self._get_password(), - ) - await self._client.__aenter__() - - if self.config.discovery.enabled and self.config.discovery.subscribe_to_birth: - # Subscribe to HA birth topic to republish discovery on HA restart - await self._client.subscribe(self.config.discovery.birth_topic) - asyncio.create_task(self._listen_for_birth()) - - async def _listen_for_birth(self) -> None: - """Listen for Home Assistant birth messages to republish discovery.""" - async for message in self._client.messages: - if message.topic.matches(self.config.discovery.birth_topic): - if message.payload.decode() == "online": - logger.info("Home Assistant came online, republishing discovery") - await self._publish_discovery() - - async def register_camera(self, camera_name: str) -> None: - """Register a camera for discovery.""" - self._cameras.add(camera_name) - if self.config.discovery.enabled and self._discovery_published: - # Publish discovery for new camera immediately - await self._publish_camera_discovery(camera_name) - - async def _publish_discovery(self) -> None: - """Publish all discovery messages.""" - if not self._discovery_builder or not self._client: - return - - # Publish hub entities - for entity in self._discovery_builder.build_hub_entities(): - topic = self._discovery_builder.get_discovery_topic(entity) - payload = self._discovery_builder.get_discovery_payload(entity) - await self._client.publish(topic, payload, retain=True) - logger.debug(f"Published discovery: {topic}") - - # Publish camera entities - for camera_name in self._cameras: - await self._publish_camera_discovery(camera_name) - - self._discovery_published = True - logger.info(f"Published MQTT discovery for {len(self._cameras)} cameras") - - async def _publish_camera_discovery(self, camera_name: str) -> None: - """Publish discovery messages for a single camera.""" - if not self._discovery_builder or not self._client: - return - - for entity in self._discovery_builder.build_camera_entities(camera_name): - topic = self._discovery_builder.get_discovery_topic(entity) - payload = self._discovery_builder.get_discovery_payload(entity) - await self._client.publish(topic, payload, retain=True) - - async def notify(self, alert: Alert) -> None: - """Send alert and update entity states.""" - if not self._client: - raise RuntimeError("MQTT client not started") - - camera = alert.camera_name - base_topic = f"homesec/{camera}" - - # Original alert topic (backwards compatible) - alert_topic = self.config.topic_template.format(camera_name=camera) - await self._client.publish( - alert_topic, - alert.model_dump_json(), - qos=self.config.qos, - retain=self.config.retain, - ) - - # If discovery is enabled, also publish to state topics - if self.config.discovery.enabled: - # Motion state - await self._client.publish( - f"{base_topic}/motion", - "ON", - retain=True, - ) - - # Person detection (if detected) - if alert.analysis and "person" in (alert.analysis.detected_objects or []): - await self._client.publish( - f"{base_topic}/person", - "ON", - retain=True, - ) - - # Activity details - activity_payload = { - "activity_type": alert.activity_type or "unknown", - "summary": alert.summary, - "risk_level": alert.risk_level.value if alert.risk_level else None, - "timestamp": alert.ts.isoformat(), - "clip_id": alert.clip_id, - } - await self._client.publish( - f"{base_topic}/activity", - json.dumps(activity_payload), - retain=True, - ) - - # Risk level - await self._client.publish( - f"{base_topic}/risk", - alert.risk_level.value if alert.risk_level else "UNKNOWN", - retain=True, - ) - - # Clip info - clip_payload = { - "clip_id": alert.clip_id, - "view_url": alert.view_url, - "storage_uri": alert.storage_uri, - "timestamp": alert.ts.isoformat(), - } - await self._client.publish( - f"{base_topic}/clip", - json.dumps(clip_payload), - retain=True, - ) - - # Device trigger for automations - await self._client.publish( - f"{base_topic}/alert", - json.dumps({"event_type": "alert", **activity_payload}), - ) - - # Schedule motion reset after 30 seconds - asyncio.create_task(self._reset_motion_state(camera)) - - async def _reset_motion_state(self, camera_name: str, delay: float = 30.0) -> None: - """Reset motion binary sensor after delay.""" - await asyncio.sleep(delay) - if self._client: - await self._client.publish( - f"homesec/{camera_name}/motion", - "OFF", - retain=True, - ) - await self._client.publish( - f"homesec/{camera_name}/person", - "OFF", - retain=True, - ) - - async def publish_health(self, camera_name: str, health: str) -> None: - """Publish camera health state.""" - if self._client and self.config.discovery.enabled: - await self._client.publish( - f"homesec/{camera_name}/health", - health, - retain=True, - ) - - async def publish_hub_stats(self, clips_today: int, alerts_today: int) -> None: - """Publish hub statistics.""" - if self._client and self.config.discovery.enabled: - await self._client.publish( - "homesec/hub/status", - "online", - retain=True, - ) - await self._client.publish( - "homesec/hub/stats", - json.dumps({ - "clips_today": clips_today, - "alerts_today": alerts_today, - }), - retain=True, - ) - - async def stop(self) -> None: - """Stop the MQTT client.""" - if self._client: - # Publish offline status - if self.config.discovery.enabled: - await self._client.publish( - "homesec/hub/status", - "offline", - retain=True, - ) - await self._client.__aexit__(None, None, None) - self._client = None -``` +- Use `MQTTConfig.auth.username_env` and `MQTTConfig.auth.password_env` for credentials. +- Avoid blocking `loop_start`/`loop_stop` in the event loop (wrap in `asyncio.to_thread`). ### 1.4 Application Integration **File:** `src/homesec/app.py` -Update the application to register cameras with the MQTT notifier: +Update the application to register cameras with the MQTT notifier. Prefer a small +`DiscoveryNotifier` Protocol (or `isinstance` check) over `hasattr` on private methods. ```python -# In HomesecApp.start() method, after initializing sources: +# In Application._create_components(), after sources are created: -# Register cameras with MQTT notifier for discovery -for notifier in self.notifiers: - if hasattr(notifier, 'register_camera'): - for source in self.sources: +# Register cameras with discovery-capable notifier(s) +for entry in self._notifier_entries: + notifier = entry.notifier + if isinstance(notifier, DiscoveryNotifier): + for source in self._sources: await notifier.register_camera(source.camera_name) # Publish initial discovery - if hasattr(notifier, '_publish_discovery'): - await notifier._publish_discovery() + await notifier.publish_discovery() ``` ### 1.5 Configuration Example @@ -556,8 +354,9 @@ notifiers: - type: mqtt host: localhost port: 1883 - username: homeassistant - password_env: MQTT_PASSWORD + auth: + username_env: MQTT_USERNAME + password_env: MQTT_PASSWORD topic_template: "homecam/alerts/{camera_name}" qos: 1 retain: false @@ -649,6 +448,15 @@ class TestMQTTDiscoveryBuilder: **Estimated Effort:** 5-7 days +### 2.0 Control Plane Requirements + +- FastAPI only; all endpoints are `async def`. +- Use async SQLAlchemy only for DB access (no sync engines or blocking DB calls). +- No blocking operations inside endpoints; use `asyncio.to_thread` for file I/O and restarts. +- API must not write directly to `StateStore`/`EventStore`. Add read methods on `ClipRepository`. +- Config updates are validated with Pydantic, persisted to disk, and return `restart_required: true`. +- API provides a restart endpoint to request a graceful shutdown. + ### 2.1 API Framework Setup **New File:** `src/homesec/api/__init__.py` @@ -678,18 +486,18 @@ from fastapi.middleware.cors import CORSMiddleware from .routes import cameras, clips, config, events, health, websocket if TYPE_CHECKING: - from homesec.app import HomesecApp + from homesec.app import Application logger = logging.getLogger(__name__) -def create_app(homesec_app: HomesecApp) -> FastAPI: +def create_app(app_instance: Application) -> FastAPI: """Create the FastAPI application.""" @asynccontextmanager async def lifespan(app: FastAPI): - # Store reference to HomeSec app - app.state.homesec = homesec_app + # Store reference to Application + app.state.homesec = app_instance yield app = FastAPI( @@ -747,7 +555,42 @@ class APIServer: self._server.should_exit = True ``` -### 2.2 API Routes +**Port coordination:** The current `HealthServer` defaults to port 8080. Either: + +- Move health to a new default (e.g., 8081) when API is enabled, or +- Replace the aiohttp health server with a FastAPI `/api/v1/health` endpoint. + +### 2.2 Config Persistence + Restart + +**New File:** `src/homesec/config/manager.py` + +Responsibilities: + +- Load and validate config via existing `load_config()` and Pydantic models. +- Persist updated config atomically (write temp file, fsync, rename). +- Return `restart_required: true` for any config-changing endpoints. +- Expose methods: + - `get_config() -> Config` + - `update_config(new_config: dict) -> ConfigUpdateResult` + - `update_camera(...) -> ConfigUpdateResult` + - `remove_camera(...) -> ConfigUpdateResult` +- Use `asyncio.to_thread` for file I/O to keep endpoints non-blocking. + +**Repository extensions:** + +- Add read APIs to `ClipRepository`: + - `get_clip(clip_id)` + - `list_clips(...)` + - `list_events(...)` + - `delete_clip(clip_id)` (mark deleted + emit event) +- Implement with async SQLAlchemy in `PostgresStateStore` / `PostgresEventStore`. + +**New Endpoint:** `POST /api/v1/system/restart` + +- Triggers graceful shutdown (`Application.request_shutdown()`). +- HA can call this after config update, or restart the add-on. + +### 2.3 API Routes **New File:** `src/homesec/api/routes/cameras.py` @@ -765,6 +608,9 @@ from ..dependencies import get_homesec_app router = APIRouter(prefix="/cameras") +# Note: All config-mutating endpoints return restart_required=True and do not +# attempt hot-reload. HA may call /api/v1/system/restart or restart the add-on. + class CameraCreate(BaseModel): """Request model for creating a camera.""" @@ -795,6 +641,12 @@ class CameraListResponse(BaseModel): total: int +class ConfigChangeResponse(BaseModel): + """Response model for config changes.""" + restart_required: bool = True + camera: CameraResponse | None = None + + @router.get("", response_model=CameraListResponse) async def list_cameras(app=Depends(get_homesec_app)): """List all configured cameras.""" @@ -830,7 +682,7 @@ async def get_camera(camera_name: str, app=Depends(get_homesec_app)): ) -@router.post("", response_model=CameraResponse, status_code=status.HTTP_201_CREATED) +@router.post("", response_model=ConfigChangeResponse, status_code=status.HTTP_201_CREATED) async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): """Add a new camera.""" if app.get_source(camera.name): @@ -845,13 +697,16 @@ async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): source_type=camera.type, config=camera.config, ) - return CameraResponse( - name=source.camera_name, - type=source.source_type, - healthy=source.is_healthy(), - last_heartbeat=source.last_heartbeat(), - config=source.get_config(), - alert_policy=None, + return ConfigChangeResponse( + restart_required=True, + camera=CameraResponse( + name=source.camera_name, + type=source.source_type, + healthy=source.is_healthy(), + last_heartbeat=source.last_heartbeat(), + config=source.get_config(), + alert_policy=None, + ), ) except ValueError as e: raise HTTPException( @@ -860,7 +715,7 @@ async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): ) -@router.put("/{camera_name}", response_model=CameraResponse) +@router.put("/{camera_name}", response_model=ConfigChangeResponse) async def update_camera( camera_name: str, update: CameraUpdate, @@ -881,17 +736,20 @@ async def update_camera( await app.update_camera_alert_policy(camera_name, update.alert_policy) source = app.get_source(camera_name) - return CameraResponse( - name=source.camera_name, - type=source.source_type, - healthy=source.is_healthy(), - last_heartbeat=source.last_heartbeat(), - config=source.get_config(), - alert_policy=app.get_camera_alert_policy(camera_name), + return ConfigChangeResponse( + restart_required=True, + camera=CameraResponse( + name=source.camera_name, + type=source.source_type, + healthy=source.is_healthy(), + last_heartbeat=source.last_heartbeat(), + config=source.get_config(), + alert_policy=app.get_camera_alert_policy(camera_name), + ), ) -@router.delete("/{camera_name}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{camera_name}", response_model=ConfigChangeResponse) async def delete_camera(camera_name: str, app=Depends(get_homesec_app)): """Remove a camera.""" source = app.get_source(camera_name) @@ -902,6 +760,7 @@ async def delete_camera(camera_name: str, app=Depends(get_homesec_app)): ) await app.remove_camera(camera_name) + return ConfigChangeResponse(restart_required=True, camera=None) @router.get("/{camera_name}/status") @@ -1118,7 +977,7 @@ from typing import TYPE_CHECKING from fastapi import APIRouter, WebSocket, WebSocketDisconnect if TYPE_CHECKING: - from homesec.app import HomesecApp + from homesec.app import Application logger = logging.getLogger(__name__) @@ -1155,7 +1014,7 @@ async def websocket_endpoint(websocket: WebSocket): """WebSocket endpoint for real-time events.""" await manager.connect(websocket) - app: HomesecApp = websocket.app.state.homesec + app: Application = websocket.app.state.homesec # Subscribe to app events event_queue = asyncio.Queue() @@ -1199,7 +1058,7 @@ async def websocket_endpoint(websocket: WebSocket): app.unsubscribe_events(event_handler) ``` -### 2.3 API Configuration +### 2.4 API Configuration **File:** `src/homesec/models/config.py` (add) @@ -1220,13 +1079,15 @@ class APIConfig(BaseModel): rate_limit_window_seconds: int = 60 ``` -### 2.4 OpenAPI Documentation +### 2.5 OpenAPI Documentation The FastAPI app automatically generates OpenAPI docs at `/api/v1/docs` (Swagger UI) and `/api/v1/redoc` (ReDoc). -### 2.5 Acceptance Criteria +### 2.6 Acceptance Criteria - [ ] All CRUD operations for cameras work +- [ ] Config changes are validated, persisted, and return `restart_required: true` +- [ ] Restart endpoint triggers graceful shutdown - [ ] Clip listing with filtering works - [ ] Event history API works - [ ] WebSocket broadcasts real-time events @@ -1267,6 +1128,12 @@ homesec-ha-addons/ **File:** `homesec/config.yaml` +Note: Update to current Home Assistant add-on schema: + +- Use `addon_config` mapping instead of `config:rw` where possible. +- Read runtime options from `/data/options.json` via Bashio. +- Keep secrets out of the generated config (env vars only). + ```yaml name: HomeSec version: "1.2.2" @@ -1832,6 +1699,12 @@ class InvalidAuth(Exception): """Error to indicate there is invalid auth.""" ``` +Integration behavior for config changes: + +- When users add/update/remove cameras in HA, call the HomeSec API. +- If `restart_required: true`, show a confirmation and invoke `/api/v1/system/restart` + (or instruct the user to restart the add-on). + ### 4.5 Data Coordinator **File:** `custom_components/homesec/coordinator.py` @@ -2674,6 +2547,8 @@ Create a custom panel for viewing event history with timeline visualization. ## Testing Strategy +All new tests must include Given/When/Then comments (per `TESTING.md`). + ### Unit Tests ``` @@ -2756,6 +2631,7 @@ tests/ ### Phase 2: REST API - `src/homesec/api/` - New package (server.py, routes/*, dependencies.py) +- `src/homesec/config/manager.py` - Config persistence + restart signaling - `src/homesec/models/config.py` - Add APIConfig - `src/homesec/app.py` - Integrate API server - `pyproject.toml` - Add fastapi, uvicorn dependencies @@ -2788,4 +2664,4 @@ tests/ **Total: 22-31 days** -Phases 1 and 2 can run in parallel. Phase 3 and 4 can run in parallel after Phase 2. +Execution order for Option A: Phase 2 -> Phase 4 -> Phase 3, with Phase 1 optional/parallel. Phase 5 follows Phase 4. diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 4ca6e984..2f7e3315 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -12,9 +12,27 @@ HomeSec is already well-architected for Home Assistant integration with its plug --- +## Decision Snapshot (2026-02-01) + +- Chosen direction: Add-on + native integration (Option A below) with HomeSec as the runtime. +- Required: runtime add/remove cameras and other config changes from HA. +- API stack: FastAPI, async endpoints only, async SQLAlchemy only. +- Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA may trigger restart. +- Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). +- Tests: Given/When/Then comments required for all new tests. +- P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). + +## Constraints and Non-Goals + +- No blocking work in API endpoints; file I/O and long operations must use `asyncio.to_thread`. +- Avoid in-process hot-reload of pipeline components in v1; prefer restart after config changes. +- Do not move heavy inference into Home Assistant (keep compute inside HomeSec runtime). + +--- + ## Integration Architecture Options -### Option A: Add-on + Native Integration (Recommended) +### Option A: Add-on + Native Integration (Chosen) ``` ┌─────────────────────────────────────────────────────────────┐ @@ -92,6 +110,14 @@ Move core homesec logic into a Home Assistant integration (runs in HA's Python e ## Recommended Implementation Plan +Execution order for Option A: + +1. REST API + config persistence (control plane) +2. Native HA integration (config flow + entities) +3. Home Assistant add-on packaging +4. MQTT discovery (optional parallel track) +5. Advanced UX (optional) + ### Phase 1: MQTT Discovery Enhancement (Quick Win) Enhance the existing MQTT notifier to publish discovery configs: @@ -113,7 +139,7 @@ Enhance the existing MQTT notifier to publish discovery configs: ### Phase 2: REST API for Configuration -Add a new REST API to homesec for remote configuration: +Add a new REST API to homesec for remote configuration. All endpoints are `async def` and use async SQLAlchemy only. ```yaml # New endpoints @@ -131,6 +157,8 @@ GET /api/v1/events # Event history WS /api/v1/ws # Real-time events ``` +Config updates are validated with Pydantic, written to disk, and return `restart_required: true`. HA can then call a restart endpoint or restart the add-on. + ### Phase 3: Home Assistant Add-on Create a Home Assistant add-on for easy installation: @@ -349,8 +377,8 @@ User edits YAML → HomeSec → MQTT Discovery → HA entities created ### Option C: Hybrid (Recommended) - **Core config** (storage, database, VLM provider): HomeSec YAML -- **Camera config**: Editable from both, synced via API -- **Alert policies**: Editable from HA, stored in homesec +- **Camera config**: Editable from HA via API, persisted to YAML; restart required +- **Alert policies**: Editable from HA, stored in homesec; restart required --- @@ -388,10 +416,10 @@ For custom integration distribution: | Phase | Effort | Value | Description | |-------|--------|-------|-------------| -| **1. MQTT Discovery** | Low | High | Auto-create entities from existing MQTT | -| **2. REST API** | Medium | High | Enable remote configuration | +| **1. REST API** | Medium | High | Enable remote configuration and control plane | +| **2. Integration** | High | Very High | Full HA UI configuration | | **3. Add-on** | Medium | High | One-click install for HA OS users | -| **4. Integration** | High | Very High | Full HA UI configuration | +| **4. MQTT Discovery (optional)** | Low | Medium | Auto-create entities for non-integration users | | **5. Dashboard Cards** | Medium | Medium | Rich visualization | --- From 291520089a038cc5090e291845ec04b5d555f2ee Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 16:49:47 -0800 Subject: [PATCH 04/31] docs: refine HA plan and brainstorm --- docs/home-assistant-implementation-plan.md | 489 ++++++------------ docs/home-assistant-integration-brainstorm.md | 87 ++-- 2 files changed, 198 insertions(+), 378 deletions(-) diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index 04ea75f6..95908f6e 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -10,6 +10,10 @@ This document provides a comprehensive, step-by-step implementation plan for int - Required: runtime add/remove cameras and other config changes from HA. - API stack: FastAPI, async endpoints only, async SQLAlchemy only. - Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA can trigger restart. +- Config storage: **Override YAML file** is source of truth for dynamic config. Base YAML is bootstrap-only. +- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists replace. +- Single instance: HA integration assumes one HomeSec instance (`single_config_entry`). +- Secrets: never stored in HomeSec; config stores env var names; HA/add-on passes env vars at boot. - Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). - Tests: Given/When/Then comments required for all new tests. - P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). @@ -454,8 +458,12 @@ class TestMQTTDiscoveryBuilder: - Use async SQLAlchemy only for DB access (no sync engines or blocking DB calls). - No blocking operations inside endpoints; use `asyncio.to_thread` for file I/O and restarts. - API must not write directly to `StateStore`/`EventStore`. Add read methods on `ClipRepository`. -- Config updates are validated with Pydantic, persisted to disk, and return `restart_required: true`. +- Config is loaded from **multiple YAML files** (left → right). Rightmost wins. +- Merge semantics: dicts deep-merge; lists replace. +- Config updates are validated with Pydantic, **written to the override YAML only**, and return `restart_required: true`. - API provides a restart endpoint to request a graceful shutdown. +- Server config: introduce `FastAPIServerConfig` (host/port, enabled, api_key_env, CORS, health path) to replace `HealthConfig`. +- Secrets are never stored in config; only env var names are persisted. ### 2.1 API Framework Setup @@ -483,7 +491,7 @@ from typing import TYPE_CHECKING from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from .routes import cameras, clips, config, events, health, websocket +from .routes import cameras, clips, config, events, health if TYPE_CHECKING: from homesec.app import Application @@ -522,7 +530,7 @@ def create_app(app_instance: Application) -> FastAPI: app.include_router(cameras.router, prefix="/api/v1", tags=["cameras"]) app.include_router(clips.router, prefix="/api/v1", tags=["clips"]) app.include_router(events.router, prefix="/api/v1", tags=["events"]) - app.include_router(websocket.router, prefix="/api/v1", tags=["websocket"]) + # MQTT is used for event push (no WebSocket in v1) return app @@ -555,10 +563,9 @@ class APIServer: self._server.should_exit = True ``` -**Port coordination:** The current `HealthServer` defaults to port 8080. Either: - -- Move health to a new default (e.g., 8081) when API is enabled, or -- Replace the aiohttp health server with a FastAPI `/api/v1/health` endpoint. +**Port coordination:** Replace the aiohttp `HealthServer` with FastAPI and make +the port configurable via `FastAPIServerConfig` (default 8080). Provide both +`/health` and `/api/v1/health` for compatibility. ### 2.2 Config Persistence + Restart @@ -566,8 +573,11 @@ class APIServer: Responsibilities: -- Load and validate config via existing `load_config()` and Pydantic models. -- Persist updated config atomically (write temp file, fsync, rename). +- Load and validate config from **multiple YAML files** (left → right). Rightmost wins. +- Base YAML is bootstrap-only; **override YAML** contains all HA-managed config. +- Merge semantics: dicts deep-merge; lists replace. +- Persist updated override YAML atomically (write temp file, fsync, rename). +- Store `config_version` in the override file and enforce optimistic concurrency. - Return `restart_required: true` for any config-changing endpoints. - Expose methods: - `get_config() -> Config` @@ -575,6 +585,16 @@ Responsibilities: - `update_camera(...) -> ConfigUpdateResult` - `remove_camera(...) -> ConfigUpdateResult` - Use `asyncio.to_thread` for file I/O to keep endpoints non-blocking. +- Provide `dump_override(path: Path)` to export backup YAML. + - `ConfigUpdateResult` should include the new `config_version`. +- Application should expose `config_store` and `config_version` for API routes. +- Override file is machine-owned; comment preservation is not required. +- Application should load configs via ConfigManager with multiple `--config` paths. + +CLI requirements: + +- Support multiple `--config` flags (order matters). +- Default override path: `config/ha-overrides.yaml` (override can be passed as the last `--config`). **Repository extensions:** @@ -610,6 +630,7 @@ router = APIRouter(prefix="/cameras") # Note: All config-mutating endpoints return restart_required=True and do not # attempt hot-reload. HA may call /api/v1/system/restart or restart the add-on. +# All config-mutating endpoints require config_version for optimistic concurrency. class CameraCreate(BaseModel): @@ -617,12 +638,14 @@ class CameraCreate(BaseModel): name: str type: str # rtsp, ftp, local_folder config: dict # Type-specific configuration + config_version: int class CameraUpdate(BaseModel): """Request model for updating a camera.""" config: dict | None = None alert_policy: dict | None = None + config_version: int class CameraResponse(BaseModel): @@ -644,6 +667,7 @@ class CameraListResponse(BaseModel): class ConfigChangeResponse(BaseModel): """Response model for config changes.""" restart_required: bool = True + config_version: int camera: CameraResponse | None = None @@ -651,14 +675,16 @@ class ConfigChangeResponse(BaseModel): async def list_cameras(app=Depends(get_homesec_app)): """List all configured cameras.""" cameras = [] - for source in app.sources: + config = app.config_store.get_config() + for camera in config.cameras: + source = app.get_source(camera.name) cameras.append(CameraResponse( - name=source.camera_name, - type=source.source_type, - healthy=source.is_healthy(), - last_heartbeat=source.last_heartbeat(), - config=source.get_config(), - alert_policy=app.get_camera_alert_policy(source.camera_name), + name=camera.name, + type=camera.source.type, + healthy=source.is_healthy() if source else False, + last_heartbeat=source.last_heartbeat() if source else None, + config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), + alert_policy=app.get_camera_alert_policy(camera.name), )) return CameraListResponse(cameras=cameras, total=len(cameras)) @@ -666,18 +692,20 @@ async def list_cameras(app=Depends(get_homesec_app)): @router.get("/{camera_name}", response_model=CameraResponse) async def get_camera(camera_name: str, app=Depends(get_homesec_app)): """Get a specific camera's configuration.""" - source = app.get_source(camera_name) - if not source: + config = app.config_store.get_config() + camera = next((c for c in config.cameras if c.name == camera_name), None) + if not camera: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Camera '{camera_name}' not found", ) + source = app.get_source(camera_name) return CameraResponse( - name=source.camera_name, - type=source.source_type, - healthy=source.is_healthy(), - last_heartbeat=source.last_heartbeat(), - config=source.get_config(), + name=camera.name, + type=camera.source.type, + healthy=source.is_healthy() if source else False, + last_heartbeat=source.last_heartbeat() if source else None, + config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), alert_policy=app.get_camera_alert_policy(camera_name), ) @@ -692,19 +720,21 @@ async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): ) try: - source = await app.add_camera( + result = await app.config_store.add_camera( name=camera.name, source_type=camera.type, config=camera.config, + config_version=camera.config_version, ) return ConfigChangeResponse( restart_required=True, + config_version=result.config_version, camera=CameraResponse( - name=source.camera_name, - type=source.source_type, - healthy=source.is_healthy(), - last_heartbeat=source.last_heartbeat(), - config=source.get_config(), + name=camera.name, + type=camera.type, + healthy=False, + last_heartbeat=None, + config=camera.config, alert_policy=None, ), ) @@ -729,28 +759,26 @@ async def update_camera( detail=f"Camera '{camera_name}' not found", ) - if update.config: - await app.update_camera_config(camera_name, update.config) - - if update.alert_policy: - await app.update_camera_alert_policy(camera_name, update.alert_policy) + result = await app.config_store.update_camera( + camera_name=camera_name, + config=update.config, + alert_policy=update.alert_policy, + config_version=update.config_version, + ) - source = app.get_source(camera_name) return ConfigChangeResponse( restart_required=True, - camera=CameraResponse( - name=source.camera_name, - type=source.source_type, - healthy=source.is_healthy(), - last_heartbeat=source.last_heartbeat(), - config=source.get_config(), - alert_policy=app.get_camera_alert_policy(camera_name), - ), + config_version=result.config_version, + camera=result.camera, ) @router.delete("/{camera_name}", response_model=ConfigChangeResponse) -async def delete_camera(camera_name: str, app=Depends(get_homesec_app)): +async def delete_camera( + camera_name: str, + config_version: int, + app=Depends(get_homesec_app), +): """Remove a camera.""" source = app.get_source(camera_name) if not source: @@ -759,8 +787,15 @@ async def delete_camera(camera_name: str, app=Depends(get_homesec_app)): detail=f"Camera '{camera_name}' not found", ) - await app.remove_camera(camera_name) - return ConfigChangeResponse(restart_required=True, camera=None) + result = await app.config_store.remove_camera( + camera_name=camera_name, + config_version=config_version, + ) + return ConfigChangeResponse( + restart_required=True, + config_version=result.config_version, + camera=None, + ) @router.get("/{camera_name}/status") @@ -962,109 +997,15 @@ async def list_events( ) ``` -**New File:** `src/homesec/api/routes/websocket.py` - -```python -"""WebSocket routes for real-time updates.""" - -from __future__ import annotations - -import asyncio -import json -import logging -from typing import TYPE_CHECKING - -from fastapi import APIRouter, WebSocket, WebSocketDisconnect - -if TYPE_CHECKING: - from homesec.app import Application - -logger = logging.getLogger(__name__) - -router = APIRouter() - - -class ConnectionManager: - """Manages WebSocket connections.""" - - def __init__(self): - self.active_connections: list[WebSocket] = [] - - async def connect(self, websocket: WebSocket): - await websocket.accept() - self.active_connections.append(websocket) - - def disconnect(self, websocket: WebSocket): - self.active_connections.remove(websocket) - - async def broadcast(self, message: dict): - """Broadcast message to all connected clients.""" - for connection in self.active_connections: - try: - await connection.send_json(message) - except Exception: - pass # Connection might be closed - - -manager = ConnectionManager() - - -@router.websocket("/ws") -async def websocket_endpoint(websocket: WebSocket): - """WebSocket endpoint for real-time events.""" - await manager.connect(websocket) - - app: Application = websocket.app.state.homesec - - # Subscribe to app events - event_queue = asyncio.Queue() - - async def event_handler(event_type: str, data: dict): - await event_queue.put({"type": event_type, "data": data}) - - app.subscribe_events(event_handler) - - try: - while True: - # Wait for either client message or app event - done, pending = await asyncio.wait( - [ - asyncio.create_task(websocket.receive_text()), - asyncio.create_task(event_queue.get()), - ], - return_when=asyncio.FIRST_COMPLETED, - ) - - for task in done: - result = task.result() - - if isinstance(result, str): - # Message from client - try: - msg = json.loads(result) - if msg.get("type") == "ping": - await websocket.send_json({"type": "pong"}) - except json.JSONDecodeError: - pass - else: - # Event from app - await websocket.send_json(result) - - for task in pending: - task.cancel() - - except WebSocketDisconnect: - manager.disconnect(websocket) - app.unsubscribe_events(event_handler) -``` +Real-time updates use MQTT topics; no WebSocket endpoint in v1. ### 2.4 API Configuration **File:** `src/homesec/models/config.py` (add) ```python -class APIConfig(BaseModel): - """Configuration for the REST API.""" +class FastAPIServerConfig(BaseModel): + """Configuration for the FastAPI server.""" enabled: bool = True host: str = "0.0.0.0" @@ -1073,10 +1014,12 @@ class APIConfig(BaseModel): # Authentication (optional) auth_enabled: bool = False api_key_env: str | None = None - # Rate limiting - rate_limit_enabled: bool = True - rate_limit_requests: int = 100 - rate_limit_window_seconds: int = 60 + # Health endpoints + health_path: str = "/health" + api_health_path: str = "/api/v1/health" + +# In Config: +# - Replace `health: HealthConfig` with `server: FastAPIServerConfig` ``` ### 2.5 OpenAPI Documentation @@ -1087,10 +1030,11 @@ The FastAPI app automatically generates OpenAPI docs at `/api/v1/docs` (Swagger - [ ] All CRUD operations for cameras work - [ ] Config changes are validated, persisted, and return `restart_required: true` +- [ ] Stale `config_version` updates return 409 Conflict - [ ] Restart endpoint triggers graceful shutdown - [ ] Clip listing with filtering works - [ ] Event history API works -- [ ] WebSocket broadcasts real-time events +- [ ] MQTT event topics trigger HA refresh - [ ] OpenAPI documentation is accurate - [ ] CORS works for Home Assistant frontend - [ ] API authentication (optional) works @@ -1156,11 +1100,10 @@ panel_title: HomeSec # Port mappings ports: 8080/tcp: null # API (exposed via ingress) - 8554/tcp: 8554 # RTSP proxy (if implemented) # Volume mappings map: - - config:rw # /config - HA config directory + - addon_config:rw # /config - HomeSec managed config - media:rw # /media - Media storage - share:rw # /share - Shared data @@ -1171,6 +1114,7 @@ services: # Options schema schema: config_path: str? + override_path: str? log_level: list(debug|info|warning|error)? # Database database_url: str? @@ -1188,6 +1132,7 @@ schema: # Default options options: config_path: /config/homesec/config.yaml + override_path: /data/overrides.yaml log_level: info database_url: "" storage_type: local @@ -1261,6 +1206,7 @@ HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ # Read options CONFIG_PATH=$(bashio::config 'config_path') +OVERRIDE_PATH=$(bashio::config 'override_path') LOG_LEVEL=$(bashio::config 'log_level') DATABASE_URL=$(bashio::config 'database_url') STORAGE_TYPE=$(bashio::config 'storage_type') @@ -1281,6 +1227,7 @@ fi # Create config directory if needed mkdir -p "$(dirname "${CONFIG_PATH}")" +mkdir -p "$(dirname "${OVERRIDE_PATH}")" # Generate config if it doesn't exist if [[ ! -f "${CONFIG_PATH}" ]]; then @@ -1307,13 +1254,19 @@ notifiers: discovery: enabled: ${MQTT_DISCOVERY} -health: +server: enabled: true + host: 0.0.0.0 port: 8080 +EOF +fi -api: - enabled: true - port: 8080 +# Create override file if missing +if [[ ! -f "${OVERRIDE_PATH}" ]]; then + bashio::log.info "Creating override file at ${OVERRIDE_PATH}" + cat > "${OVERRIDE_PATH}" << EOF +version: 1 +config_version: 1 EOF fi @@ -1340,6 +1293,7 @@ bashio::log.info "Starting HomeSec..." # Run HomeSec exec python3 -m homesec.cli run \ --config "${CONFIG_PATH}" \ + --config "${OVERRIDE_PATH}" \ --log-level "${LOG_LEVEL}" ``` @@ -1357,10 +1311,6 @@ location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - # WebSocket support - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - # Timeouts for long-running connections proxy_connect_timeout 60s; proxy_send_timeout 60s; @@ -1398,7 +1348,6 @@ custom_components/homesec/ ├── config_flow.py # UI configuration flow ├── coordinator.py # Data update coordinator ├── entity.py # Base entity class -├── camera.py # Camera platform ├── sensor.py # Sensor platform ├── binary_sensor.py # Binary sensor platform ├── switch.py # Switch platform @@ -1422,6 +1371,7 @@ custom_components/homesec/ "codeowners": ["@lan17"], "config_flow": true, "dependencies": ["mqtt"], + "single_config_entry": true, "documentation": "https://github.com/lan17/homesec", "integration_type": "hub", "iot_class": "local_push", @@ -1467,7 +1417,6 @@ DIAGNOSTIC_SENSORS: Final = ["health", "last_heartbeat"] # Update intervals SCAN_INTERVAL_SECONDS: Final = 30 -WEBSOCKET_RECONNECT_DELAY: Final = 5 # Attributes ATTR_CLIP_ID: Final = "clip_id" @@ -1727,7 +1676,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DOMAIN, SCAN_INTERVAL_SECONDS, WEBSOCKET_RECONNECT_DELAY +from .const import DOMAIN, SCAN_INTERVAL_SECONDS _LOGGER = logging.getLogger(__name__) @@ -1755,8 +1704,7 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.api_key = api_key self.verify_ssl = verify_ssl self._session = async_get_clientsession(hass, verify_ssl=verify_ssl) - self._ws_task: asyncio.Task | None = None - self._event_callbacks: list[callable] = [] + self._mqtt_unsub: callable | None = None @property def base_url(self) -> str: @@ -1802,78 +1750,35 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): except aiohttp.ClientError as err: raise UpdateFailed(f"Error communicating with HomeSec: {err}") from err - async def async_start_websocket(self) -> None: - """Start WebSocket connection for real-time updates.""" - if self._ws_task is not None: + async def async_start_mqtt(self) -> None: + """Subscribe to HomeSec MQTT topics for push updates.""" + if self._mqtt_unsub is not None: return - self._ws_task = asyncio.create_task(self._websocket_loop()) - - async def async_stop_websocket(self) -> None: - """Stop WebSocket connection.""" - if self._ws_task is not None: - self._ws_task.cancel() - try: - await self._ws_task - except asyncio.CancelledError: - pass - self._ws_task = None - - async def _websocket_loop(self) -> None: - """Maintain WebSocket connection and handle events.""" - ws_url = f"ws://{self.host}:{self.port}/api/v1/ws" - while True: - try: - async with self._session.ws_connect(ws_url) as ws: - _LOGGER.info("Connected to HomeSec WebSocket") - - async for msg in ws: - if msg.type == aiohttp.WSMsgType.TEXT: - data = msg.json() - await self._handle_ws_event(data) - elif msg.type == aiohttp.WSMsgType.ERROR: - _LOGGER.error("WebSocket error: %s", ws.exception()) - break - - except aiohttp.ClientError as err: - _LOGGER.error("WebSocket connection error: %s", err) - - except asyncio.CancelledError: - _LOGGER.info("WebSocket task cancelled") - return - - _LOGGER.info( - "WebSocket disconnected, reconnecting in %s seconds", - WEBSOCKET_RECONNECT_DELAY, - ) - await asyncio.sleep(WEBSOCKET_RECONNECT_DELAY) - - async def _handle_ws_event(self, data: dict[str, Any]) -> None: - """Handle incoming WebSocket event.""" - event_type = data.get("type") - event_data = data.get("data", {}) - - _LOGGER.debug("Received WebSocket event: %s", event_type) + from homeassistant.components import mqtt - # Trigger immediate data refresh for certain events - if event_type in ["alert", "clip_recorded", "camera_status_changed"]: - await self.async_request_refresh() + async def _on_message(msg: mqtt.ReceiveMessage) -> None: + await self._handle_mqtt_event(msg.topic, msg.payload) - # Notify registered callbacks - for callback in self._event_callbacks: - try: - await callback(event_type, event_data) - except Exception: - _LOGGER.exception("Error in event callback") + self._mqtt_unsub = await mqtt.async_subscribe( + self.hass, + "homesec/+/alert", + _on_message, + ) - def register_event_callback(self, callback: callable) -> callable: - """Register a callback for WebSocket events.""" - self._event_callbacks.append(callback) + async def async_stop_mqtt(self) -> None: + """Stop MQTT subscription.""" + if self._mqtt_unsub is not None: + self._mqtt_unsub() + self._mqtt_unsub = None - def remove(): - self._event_callbacks.remove(callback) + async def _handle_mqtt_event(self, topic: str, payload: bytes) -> None: + """Handle incoming MQTT event.""" + _ = topic + _ = payload - return remove + # Trigger immediate data refresh on alerts + await self.async_request_refresh() # API Methods @@ -1941,6 +1846,18 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): return await response.json() ``` +In `async_setup_entry`, start MQTT subscription: + +```python +await coordinator.async_start_mqtt() +``` + +In `async_unload_entry`, call: + +```python +await coordinator.async_stop_mqtt() +``` + ### 4.6 Entity Platforms **File:** `custom_components/homesec/sensor.py` @@ -2339,7 +2256,7 @@ test_camera: - [ ] Config flow connects to HomeSec and discovers cameras - [ ] All entity platforms create entities correctly - [ ] DataUpdateCoordinator fetches data at correct intervals -- [ ] WebSocket connection provides real-time updates +- [ ] MQTT subscription triggers refresh on alerts - [ ] Services work correctly (add/remove camera, set policy, test) - [ ] Options flow allows reconfiguration - [ ] Diagnostics provide useful debug information @@ -2354,96 +2271,7 @@ test_camera: **Estimated Effort:** 5-7 days -### 5.1 Camera Entity with Live Stream - -**File:** `custom_components/homesec/camera.py` - -```python -"""Camera platform for HomeSec integration.""" - -from __future__ import annotations - -from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN -from .coordinator import HomesecCoordinator -from .entity import HomesecEntity - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up HomeSec cameras.""" - coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] - - entities = [ - HomesecCameraEntity(coordinator, camera["name"]) - for camera in coordinator.data.get("cameras", []) - ] - - async_add_entities(entities) - - -class HomesecCameraEntity(HomesecEntity, Camera): - """Representation of a HomeSec camera.""" - - _attr_supported_features = CameraEntityFeature.STREAM - - def __init__( - self, - coordinator: HomesecCoordinator, - camera_name: str, - ) -> None: - """Initialize the camera.""" - HomesecEntity.__init__(self, coordinator, camera_name) - Camera.__init__(self) - self._attr_unique_id = f"{camera_name}_camera" - self._attr_name = None # Use device name - - async def stream_source(self) -> str | None: - """Return the source of the stream.""" - camera = self._get_camera_data() - if not camera: - return None - - # Get RTSP URL from camera config - config = camera.get("config", {}) - rtsp_url = config.get("rtsp_url") - - if rtsp_url: - return rtsp_url - - # Fallback to HomeSec RTSP proxy if available - return f"rtsp://{self.coordinator.host}:8554/{self._camera_name}" - - async def async_camera_image( - self, width: int | None = None, height: int | None = None - ) -> bytes | None: - """Return a still image from the camera.""" - camera = self._get_camera_data() - if not camera: - return None - - # Request snapshot from HomeSec API - try: - async with self.coordinator._session.get( - f"{self.coordinator.base_url}/cameras/{self._camera_name}/snapshot", - headers=self.coordinator._headers, - ) as response: - if response.status == 200: - return await response.read() - except Exception: - pass - - return None -``` - -### 5.2 Custom Lovelace Card (Optional) +### 5.1 Custom Lovelace Card (Optional) **File:** `custom_components/homesec/www/homesec-camera-card.js` @@ -2532,13 +2360,12 @@ class HomesecCameraCard extends HTMLElement { customElements.define("homesec-camera-card", HomesecCameraCard); ``` -### 5.3 Event Timeline Panel (Optional) +### 5.2 Event Timeline Panel (Optional) Create a custom panel for viewing event history with timeline visualization. -### 5.4 Acceptance Criteria +### 5.3 Acceptance Criteria -- [ ] Camera entities show live streams - [ ] Snapshot images work - [ ] Custom Lovelace card displays detection overlays - [ ] Event timeline shows historical data @@ -2547,7 +2374,8 @@ Create a custom panel for viewing event history with timeline visualization. ## Testing Strategy -All new tests must include Given/When/Then comments (per `TESTING.md`). +All new tests must include Given/When/Then comments (per `TESTING.md`). Prefer behavioral +style tests that assert observable outcomes, not internal state. ### Unit Tests @@ -2580,8 +2408,9 @@ tests/ 2. **API Tests:** - Full CRUD cycle for cameras - - WebSocket event delivery + - MQTT event topic delivery triggers HA refresh - Authentication flows + - MQTT broker unreachable does not stall clip processing 3. **Add-on Tests:** - Installation on HA OS @@ -2595,7 +2424,7 @@ tests/ - [ ] Add/remove cameras - [ ] Verify entities update on alerts - [ ] Test automations with HomeSec triggers -- [ ] Verify camera streams in dashboard +- [ ] Verify snapshots and links (if enabled) in dashboard --- @@ -2606,8 +2435,9 @@ tests/ 1. Export current `config.yaml` 2. Install HomeSec add-on 3. Copy config to `/config/homesec/config.yaml` -4. Update database URL if using external Postgres -5. Start add-on +4. Create `/data/overrides.yaml` for HA-managed config +5. Update database URL if using external Postgres +6. Start add-on ### From MQTT-only to Full Integration @@ -2632,7 +2462,9 @@ tests/ ### Phase 2: REST API - `src/homesec/api/` - New package (server.py, routes/*, dependencies.py) - `src/homesec/config/manager.py` - Config persistence + restart signaling -- `src/homesec/models/config.py` - Add APIConfig +- `src/homesec/models/config.py` - Add FastAPIServerConfig +- `src/homesec/config/loader.py` - Support multiple YAML files + merge semantics +- `src/homesec/cli.py` - Accept repeated `--config` flags (order matters) - `src/homesec/app.py` - Integrate API server - `pyproject.toml` - Add fastapi, uvicorn dependencies @@ -2647,7 +2479,6 @@ tests/ - HACS repository configuration ### Phase 5: Advanced -- `custom_components/homesec/camera.py` - Camera platform - `custom_components/homesec/www/` - Lovelace cards --- diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 2f7e3315..ef993ca5 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -8,7 +8,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug 2. **Configure** cameras, alerts, and AI settings through the HA UI 3. **Monitor** camera health, detection stats, and pipeline status as HA entities 4. **Automate** based on detection events with rich metadata -5. **View** clips and live streams directly in HA dashboards +5. **View** clips directly in HA dashboards (RTSP handled by HA) --- @@ -18,6 +18,10 @@ HomeSec is already well-architected for Home Assistant integration with its plug - Required: runtime add/remove cameras and other config changes from HA. - API stack: FastAPI, async endpoints only, async SQLAlchemy only. - Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA may trigger restart. +- Config storage: **Override YAML file** is source of truth for dynamic config. Base YAML is bootstrap-only. +- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists replace. +- Single instance: HA integration assumes one HomeSec instance (`single_config_entry`). +- Secrets: never stored in HomeSec; config stores env var names; HA/add-on passes env vars at boot. - Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). - Tests: Given/When/Then comments required for all new tests. - P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). @@ -27,6 +31,8 @@ HomeSec is already well-architected for Home Assistant integration with its plug - No blocking work in API endpoints; file I/O and long operations must use `asyncio.to_thread`. - Avoid in-process hot-reload of pipeline components in v1; prefer restart after config changes. - Do not move heavy inference into Home Assistant (keep compute inside HomeSec runtime). +- Prefer behavioral tests that assert outcomes, not internal state. +- Prefer behavioral tests; avoid internal state assertions. --- @@ -45,7 +51,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug │ │ - Pipeline service │ │ - Config flow UI │ │ │ │ - RTSP sources │ │ - Entity platforms │ │ │ │ - YOLO/VLM │ │ - Services │ │ -│ │ - Storage backends │ │ - Event subscriptions │ │ +│ │ - Storage backends │ │ - MQTT subscriptions │ │ │ └─────────────────────┘ └─────────────────────────────┘ │ │ │ │ │ │ └────────┬───────────────────┘ │ @@ -53,8 +59,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Communication Layer │ │ │ │ - REST API (new) for config/control │ │ -│ │ - WebSocket (new) for real-time events │ │ -│ │ - MQTT for alerts (existing) │ │ +│ │ - MQTT for events + state topics │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` @@ -139,7 +144,17 @@ Enhance the existing MQTT notifier to publish discovery configs: ### Phase 2: REST API for Configuration -Add a new REST API to homesec for remote configuration. All endpoints are `async def` and use async SQLAlchemy only. +Add a new REST API to HomeSec for remote configuration. All endpoints are `async def` and use async SQLAlchemy only. + +Config model for Option B: + +- Base YAML is bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). +- API writes a machine-owned **override YAML** file for all dynamic config. +- HomeSec loads multiple YAML files left → right; rightmost wins. +- Dicts deep-merge; lists replace (override lists fully replace base lists). +- Override file default: `config/ha-overrides.yaml` (configurable via CLI). +- CLI accepts multiple `--config` flags; order matters. +- All config writes require `config_version` for optimistic concurrency. ```yaml # New endpoints @@ -154,10 +169,11 @@ POST /api/v1/cameras/{name}/test # Test camera connection GET /api/v1/clips # List recent clips GET /api/v1/clips/{id} # Get clip details GET /api/v1/events # Event history -WS /api/v1/ws # Real-time events +POST /api/v1/system/restart # Request graceful restart ``` -Config updates are validated with Pydantic, written to disk, and return `restart_required: true`. HA can then call a restart endpoint or restart the add-on. +Config updates are validated with Pydantic, written to the override YAML, and return `restart_required: true`. HA can then call a restart endpoint or restart the add-on. +Real-time updates use MQTT topics (no WebSocket in v1). ### Phase 3: Home Assistant Add-on @@ -172,14 +188,14 @@ slug: homesec arch: [amd64, aarch64] ports: 8080/tcp: 8080 # API/Health - 8554/tcp: 8554 # RTSP proxy (optional) map: - - config:rw # Store config - - media:rw # Store clips + - addon_config:rw # Store HomeSec config/overrides + - media:rw # Store clips services: - mqtt:need # Requires MQTT broker options: config_file: /config/homesec/config.yaml + override_file: /data/overrides.yaml ``` **Add-on features:** @@ -199,7 +215,6 @@ custom_components/homesec/ ├── config_flow.py # UI-based configuration ├── const.py # Constants ├── coordinator.py # DataUpdateCoordinator -├── camera.py # Camera entity platform ├── sensor.py # Sensor entities ├── binary_sensor.py # Motion sensors ├── switch.py # Enable/disable cameras @@ -213,12 +228,12 @@ custom_components/homesec/ | Platform | Entities | Description | |----------|----------|-------------| -| `camera` | Per-camera | Live RTSP stream proxy | +| `image` | Per-camera | Last snapshot (optional) | | `binary_sensor` | `motion`, `person_detected` | Detection states | | `sensor` | `last_activity`, `risk_level`, `clip_count` | Detection metadata | | `switch` | `camera_enabled`, `alerts_enabled` | Per-camera toggles | | `select` | `alert_sensitivity` | LOW/MEDIUM/HIGH | -| `image` | `last_snapshot` | Most recent detection frame | +| `device_tracker` | `camera_online` | Connectivity status (optional) | **Services:** @@ -268,18 +283,8 @@ Step 3: Notifications ### 1. Camera Streams in HA -Proxy RTSP streams through homesec with authentication: - -```python -class HomesecCamera(Camera): - """Representation of a HomeSec camera.""" - - _attr_supported_features = CameraEntityFeature.STREAM - - async def stream_source(self) -> str: - """Return the stream source URL.""" - return f"rtsp://{self._host}:8554/{self._camera_name}" -``` +HomeSec will not proxy RTSP streams. HA should use its own camera integration for RTSP +while HomeSec ingests the same stream for analysis. ### 2. Rich Event Data for Automations @@ -355,30 +360,13 @@ async def async_get_config_entry_diagnostics(hass, entry): ## Configuration Sync Strategy -### Option A: HA as Source of Truth - -HA integration manages config, pushes to homesec: - -``` -User edits in HA UI → Integration → REST API → HomeSec - ↓ - Restarts with new config -``` - -### Option B: HomeSec as Source of Truth - -HomeSec config is canonical, HA reads it: - -``` -User edits YAML → HomeSec → MQTT Discovery → HA entities created - → REST API → Integration reads state -``` - -### Option C: Hybrid (Recommended) +### Adopted Strategy (Override YAML) -- **Core config** (storage, database, VLM provider): HomeSec YAML -- **Camera config**: Editable from HA via API, persisted to YAML; restart required -- **Alert policies**: Editable from HA, stored in homesec; restart required +- **Base YAML**: bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). +- **Override YAML**: machine-owned, fully managed by HA via API. +- **Load order**: multiple YAML files loaded left → right; rightmost wins. +- **Merge semantics**: dicts deep-merge; lists replace (override lists fully replace base lists). +- **Restart**: required for all config changes. --- @@ -391,7 +379,7 @@ For custom integration distribution: { "name": "HomeSec", "render_readme": true, - "domains": ["camera", "sensor", "binary_sensor", "switch"], + "domains": ["sensor", "binary_sensor", "switch", "select", "image", "device_tracker"], "iot_class": "local_push" } ``` @@ -405,6 +393,7 @@ For custom integration distribution: "documentation": "https://github.com/lan17/homesec", "dependencies": ["mqtt"], "codeowners": ["@lan17"], + "single_config_entry": true, "iot_class": "local_push", "integration_type": "hub" } From aaeefd031b868aff3b3268c816662c4f66ef497c Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 17:42:25 -0800 Subject: [PATCH 05/31] docs: add bundled PostgreSQL and HA Events API to integration plan - Switch from MQTT to HA Events API for real-time event push - Add-on users get zero-config via SUPERVISOR_TOKEN - Standalone users provide HA URL + long-lived access token - Bundle PostgreSQL in add-on using s6-overlay for zero-config - Add Phase 2.5: HomeAssistantNotifier plugin implementation - Document s6-overlay service structure (postgres-init, postgres, homesec) - Add features list with P0/P1 priorities - Add user onboarding flows (add-on vs standalone) - Add add-on architecture diagram with bundled PostgreSQL - Document storage layout (/data/postgres, /media/homesec/clips) Co-Authored-By: Claude Opus 4.5 --- docs/home-assistant-implementation-plan.md | 645 ++++++++++++++---- docs/home-assistant-integration-brainstorm.md | 333 ++++++++- 2 files changed, 842 insertions(+), 136 deletions(-) diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index 95908f6e..0aa90e00 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -17,14 +17,21 @@ This document provides a comprehensive, step-by-step implementation plan for int - Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). - Tests: Given/When/Then comments required for all new tests. - P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). +- **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically; standalone users provide HA URL + token. +- **No MQTT required**: MQTT Discovery is optional for users who prefer it; primary path uses HA Events API. +- **409 Conflict UX**: Show error to user when config version is stale. +- **API during Postgres outage**: Return 503 Service Unavailable. +- **Camera ping**: RTSP ping implementation should include TODO for real connectivity test (not part of this integration work). +- **Delete clip**: Deletes from both local storage and cloud storage (existing pattern in cleanup_clips.py). ## Execution Order (Option A) 1. Phase 2: REST API for Configuration (control plane) -2. Phase 4: Native Home Assistant Integration -3. Phase 3: Home Assistant Add-on -4. Phase 1: MQTT Discovery Enhancement (optional parallel track) -5. Phase 5: Advanced Features +2. Phase 2.5: Home Assistant Notifier Plugin (real-time events) +3. Phase 4: Native Home Assistant Integration +4. Phase 3: Home Assistant Add-on +5. Phase 1: MQTT Discovery Enhancement (optional, for users who prefer MQTT) +6. Phase 5: Advanced Features --- @@ -32,22 +39,25 @@ This document provides a comprehensive, step-by-step implementation plan for int 1. [Decision Snapshot](#decision-snapshot-2026-02-01) 2. [Execution Order](#execution-order-option-a) -3. [Phase 1: MQTT Discovery Enhancement](#phase-1-mqtt-discovery-enhancement) -4. [Phase 2: REST API for Configuration](#phase-2-rest-api-for-configuration) -5. [Phase 3: Home Assistant Add-on](#phase-3-home-assistant-add-on) -6. [Phase 4: Native Home Assistant Integration](#phase-4-native-home-assistant-integration) -7. [Phase 5: Advanced Features](#phase-5-advanced-features) -8. [Testing Strategy](#testing-strategy) -9. [Migration Guide](#migration-guide) +3. [Phase 2: REST API for Configuration](#phase-2-rest-api-for-configuration) +4. [Phase 2.5: Home Assistant Notifier Plugin](#phase-25-home-assistant-notifier-plugin) +5. [Phase 4: Native Home Assistant Integration](#phase-4-native-home-assistant-integration) +6. [Phase 3: Home Assistant Add-on](#phase-3-home-assistant-add-on) +7. [Phase 1: MQTT Discovery Enhancement (Optional)](#phase-1-mqtt-discovery-enhancement-optional) +8. [Phase 5: Advanced Features](#phase-5-advanced-features) +9. [Testing Strategy](#testing-strategy) +10. [Migration Guide](#migration-guide) --- -## Phase 1: MQTT Discovery Enhancement +## Phase 1: MQTT Discovery Enhancement (Optional) -**Goal:** Auto-create Home Assistant entities from HomeSec without requiring manual HA configuration. +**Goal:** Auto-create Home Assistant entities from HomeSec without requiring the native integration. This is an optional feature for users who prefer MQTT over the primary HA Events API approach. **Estimated Effort:** 2-3 days +**Note:** This phase is optional. The primary integration path uses the HA Events API (Phase 2.5) which requires no MQTT broker. + ### 1.1 Configuration Model Updates **File:** `src/homesec/models/config.py` @@ -1034,10 +1044,283 @@ The FastAPI app automatically generates OpenAPI docs at `/api/v1/docs` (Swagger - [ ] Restart endpoint triggers graceful shutdown - [ ] Clip listing with filtering works - [ ] Event history API works -- [ ] MQTT event topics trigger HA refresh - [ ] OpenAPI documentation is accurate - [ ] CORS works for Home Assistant frontend - [ ] API authentication (optional) works +- [ ] Returns 503 when Postgres is unavailable + +--- + +## Phase 2.5: Home Assistant Notifier Plugin + +**Goal:** Enable real-time event push from HomeSec to Home Assistant without requiring MQTT. + +**Estimated Effort:** 2-3 days + +### 2.5.1 Configuration Model + +**File:** `src/homesec/models/config.py` + +```python +class HomeAssistantNotifierConfig(BaseModel): + """Configuration for Home Assistant notifier.""" + + # For standalone mode (not running as add-on) + # When running as HA add-on, SUPERVISOR_TOKEN is used automatically + url_env: str | None = None # e.g., "HA_URL" -> http://homeassistant.local:8123 + token_env: str | None = None # e.g., "HA_TOKEN" -> long-lived access token + + # Event configuration + event_prefix: str = "homesec" # Events will be homesec_alert, homesec_health, etc. +``` + +### 2.5.2 Home Assistant Notifier Implementation + +**New File:** `src/homesec/plugins/notifiers/home_assistant.py` + +```python +"""Home Assistant notifier - pushes events directly to HA Events API.""" + +from __future__ import annotations + +import logging +import os +from typing import TYPE_CHECKING + +import aiohttp + +from homesec.interfaces import Notifier +from homesec.models.config import HomeAssistantNotifierConfig +from homesec.plugins.registry import plugin, PluginType + +if TYPE_CHECKING: + from homesec.models.alert import Alert + +logger = logging.getLogger(__name__) + + +@plugin(plugin_type=PluginType.NOTIFIER, name="home_assistant") +class HomeAssistantNotifier(Notifier): + """Push events directly to Home Assistant via Events API.""" + + config_cls = HomeAssistantNotifierConfig + + def __init__(self, config: HomeAssistantNotifierConfig): + self.config = config + self._session: aiohttp.ClientSession | None = None + self._supervisor_mode = False + + async def start(self) -> None: + """Initialize the HTTP session and detect supervisor mode.""" + self._session = aiohttp.ClientSession() + + # Detect if running as HA add-on (SUPERVISOR_TOKEN is injected automatically) + if os.environ.get("SUPERVISOR_TOKEN"): + self._supervisor_mode = True + logger.info("HomeAssistantNotifier: Running in supervisor mode (zero-config)") + else: + # Standalone mode - validate config + if not self.config.url_env or not self.config.token_env: + raise ValueError( + "HomeAssistantNotifier requires url_env and token_env in standalone mode" + ) + logger.info("HomeAssistantNotifier: Running in standalone mode") + + async def stop(self) -> None: + """Close the HTTP session.""" + if self._session: + await self._session.close() + self._session = None + + def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: + """Get the URL and headers for the HA Events API.""" + if self._supervisor_mode: + url = f"http://supervisor/core/api/events/{self.config.event_prefix}_{event_type}" + headers = { + "Authorization": f"Bearer {os.environ['SUPERVISOR_TOKEN']}", + "Content-Type": "application/json", + } + else: + from homesec.config import resolve_env_var + base_url = resolve_env_var(self.config.url_env) + token = resolve_env_var(self.config.token_env) + url = f"{base_url}/api/events/{self.config.event_prefix}_{event_type}" + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + return url, headers + + async def notify(self, alert: Alert) -> None: + """Send alert to Home Assistant as an event.""" + if not self._session: + raise RuntimeError("HomeAssistantNotifier not started") + + url, headers = self._get_url_and_headers("alert") + + event_data = { + "camera": alert.camera_name, + "clip_id": alert.clip_id, + "activity_type": alert.activity_type, + "risk_level": alert.risk_level.value if alert.risk_level else None, + "summary": alert.summary, + "view_url": alert.view_url, + "storage_uri": alert.storage_uri, + "timestamp": alert.ts.isoformat(), + } + + # Add analysis details if present + if alert.analysis: + event_data["detected_objects"] = alert.analysis.detected_objects + event_data["analysis"] = alert.analysis.model_dump(mode="json") + + try: + async with self._session.post(url, json=event_data, headers=headers) as resp: + if resp.status >= 400: + body = await resp.text() + logger.error( + "Failed to send event to HA: %s %s - %s", + resp.status, + resp.reason, + body, + ) + else: + logger.debug("Sent homesec_alert event to HA for clip %s", alert.clip_id) + except aiohttp.ClientError as exc: + logger.error("Failed to connect to Home Assistant: %s", exc) + # Don't raise - notifications are best-effort + + async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: + """Publish camera health status to HA.""" + if not self._session: + return + + url, headers = self._get_url_and_headers("camera_health") + event_data = { + "camera": camera_name, + "healthy": healthy, + "status": "healthy" if healthy else "unhealthy", + } + + try: + async with self._session.post(url, json=event_data, headers=headers) as resp: + if resp.status >= 400: + logger.warning("Failed to send camera health to HA: %s", resp.status) + except aiohttp.ClientError: + pass # Best effort + + async def publish_clip_recorded(self, clip_id: str, camera_name: str) -> None: + """Publish clip recorded event to HA.""" + if not self._session: + return + + url, headers = self._get_url_and_headers("clip_recorded") + event_data = { + "clip_id": clip_id, + "camera": camera_name, + } + + try: + async with self._session.post(url, json=event_data, headers=headers) as resp: + if resp.status >= 400: + logger.warning("Failed to send clip_recorded to HA: %s", resp.status) + except aiohttp.ClientError: + pass # Best effort +``` + +### 2.5.3 Configuration Example + +**File:** `config/example.yaml` (add section) + +```yaml +notifiers: + # Home Assistant notifier (recommended for HA users) + - type: home_assistant + # When running as HA add-on, no configuration needed (uses SUPERVISOR_TOKEN) + # For standalone mode, provide HA URL and token: + # url_env: HA_URL # http://homeassistant.local:8123 + # token_env: HA_TOKEN # Long-lived access token from HA +``` + +### 2.5.4 Testing + +**New File:** `tests/unit/plugins/notifiers/test_home_assistant.py` + +```python +"""Tests for Home Assistant notifier.""" + +import os +from unittest.mock import AsyncMock, patch + +import pytest +from aiohttp import ClientResponseError + +from homesec.models.config import HomeAssistantNotifierConfig +from homesec.plugins.notifiers.home_assistant import HomeAssistantNotifier + + +class TestHomeAssistantNotifier: + """Tests for HomeAssistantNotifier.""" + + @pytest.fixture + def config(self): + return HomeAssistantNotifierConfig( + url_env="HA_URL", + token_env="HA_TOKEN", + ) + + @pytest.fixture + def notifier(self, config): + return HomeAssistantNotifier(config) + + async def test_supervisor_mode_detection(self, notifier): + # Given: SUPERVISOR_TOKEN is set + with patch.dict(os.environ, {"SUPERVISOR_TOKEN": "test_token"}): + # When: notifier starts + await notifier.start() + + # Then: supervisor mode is enabled + assert notifier._supervisor_mode is True + + await notifier.stop() + + async def test_standalone_mode_requires_config(self, config): + # Given: config without url_env + config.url_env = None + + # When: notifier starts without SUPERVISOR_TOKEN + notifier = HomeAssistantNotifier(config) + with patch.dict(os.environ, {}, clear=True): + # Then: ValueError is raised + with pytest.raises(ValueError, match="url_env and token_env"): + await notifier.start() + + async def test_notify_sends_event(self, notifier, alert_fixture): + # Given: notifier is started in standalone mode + with patch.dict(os.environ, {"HA_URL": "http://ha:8123", "HA_TOKEN": "token"}): + await notifier.start() + + # When: notify is called + with patch.object(notifier._session, "post", new_callable=AsyncMock) as mock_post: + mock_post.return_value.__aenter__.return_value.status = 200 + await notifier.notify(alert_fixture) + + # Then: event is posted to HA + mock_post.assert_called_once() + call_args = mock_post.call_args + assert "homesec_alert" in call_args[0][0] + + await notifier.stop() +``` + +### 2.5.5 Acceptance Criteria + +- [ ] Notifier auto-detects supervisor mode via `SUPERVISOR_TOKEN` +- [ ] Zero-config when running as HA add-on +- [ ] Standalone mode requires `url_env` and `token_env` +- [ ] Events are fired: `homesec_alert`, `homesec_camera_health`, `homesec_clip_recorded` +- [ ] Notification failures don't crash the pipeline (best-effort) +- [ ] Events contain all required metadata for HA integration --- @@ -1109,7 +1392,7 @@ map: # Services services: - - mqtt:want # Use HA's MQTT broker + - mqtt:want # Optional - only needed if user enables MQTT Discovery # Options schema schema: @@ -1125,6 +1408,8 @@ schema: # VLM vlm_enabled: bool? openai_api_key: password? + # MQTT Discovery (optional - primary path uses HA Events API) + mqtt_discovery: bool? openai_model: str? # MQTT Discovery mqtt_discovery: bool? @@ -1162,20 +1447,26 @@ watchdog: http://[HOST]:[PORT:8080]/api/v1/health ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.8 FROM ${BUILD_FROM} -# Install runtime dependencies +# Install runtime dependencies including PostgreSQL RUN apk add --no-cache \ python3 \ py3-pip \ ffmpeg \ - postgresql-client \ + postgresql16 \ + postgresql16-contrib \ opencv \ + curl \ && rm -rf /var/cache/apk/* +# Create PostgreSQL directories +RUN mkdir -p /run/postgresql /data/postgres \ + && chown -R postgres:postgres /run/postgresql /data/postgres + # Install HomeSec ARG HOMESEC_VERSION=1.2.2 RUN pip3 install --no-cache-dir homesec==${HOMESEC_VERSION} -# Copy root filesystem +# Copy root filesystem (s6-overlay services) COPY rootfs / # Set working directory @@ -1194,42 +1485,139 @@ HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ CMD curl -f http://localhost:8080/api/v1/health || exit 1 ``` -### 3.4 Startup Script +### 3.4 s6-overlay Service Structure -**File:** `homesec/rootfs/etc/services.d/homesec/run` +The add-on uses s6-overlay to run PostgreSQL and HomeSec as two services with proper dependency ordering. + +**Directory structure:** + +``` +rootfs/etc/s6-overlay/s6-rc.d/ +├── postgres/ +│ ├── type # Contains: longrun +│ ├── run # PostgreSQL startup script +│ └── dependencies.d/ +│ └── base # Depends on base setup +├── postgres-init/ +│ ├── type # Contains: oneshot +│ ├── up # Initialize DB if needed +│ └── dependencies.d/ +│ └── base +├── homesec/ +│ ├── type # Contains: longrun +│ ├── run # HomeSec startup script +│ └── dependencies.d/ +│ └── postgres # Waits for PostgreSQL +└── user/ + └── contents.d/ + ├── postgres-init + ├── postgres + └── homesec +``` + +### 3.5 PostgreSQL Initialization Service + +**File:** `rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up` + +```bash +#!/command/with-contenv bashio + +# Skip if already initialized +if [[ -f /data/postgres/data/PG_VERSION ]]; then + bashio::log.info "PostgreSQL already initialized" + exit 0 +fi + +bashio::log.info "Initializing PostgreSQL database..." + +# Create data directory +mkdir -p /data/postgres/data +chown -R postgres:postgres /data/postgres + +# Initialize database cluster +su postgres -c "initdb -D /data/postgres/data --encoding=UTF8 --locale=C" + +# Configure PostgreSQL for local connections only +cat >> /data/postgres/data/postgresql.conf << EOF +listen_addresses = 'localhost' +max_connections = 20 +shared_buffers = 128MB +EOF + +# Start PostgreSQL temporarily to create database +su postgres -c "pg_ctl -D /data/postgres/data start -w -o '-c listen_addresses=localhost'" + +# Create homesec database and user +su postgres -c "createdb homesec" +su postgres -c "psql -c \"ALTER USER postgres PASSWORD 'homesec';\"" + +# Stop PostgreSQL (will be started by longrun service) +su postgres -c "pg_ctl -D /data/postgres/data stop -w" + +bashio::log.info "PostgreSQL initialization complete" +``` + +### 3.6 PostgreSQL Service + +**File:** `rootfs/etc/s6-overlay/s6-rc.d/postgres/run` ```bash -#!/usr/bin/with-contenv bashio +#!/command/with-contenv bashio + +bashio::log.info "Starting PostgreSQL..." + +# Run PostgreSQL in foreground +exec su postgres -c "postgres -D /data/postgres/data" +``` + +### 3.7 HomeSec Service + +**File:** `rootfs/etc/s6-overlay/s6-rc.d/homesec/run` + +```bash +#!/command/with-contenv bashio # ============================================================================== -# HomeSec Add-on Startup Script +# HomeSec Service - runs after PostgreSQL is ready # ============================================================================== -# Read options +# Read add-on options CONFIG_PATH=$(bashio::config 'config_path') OVERRIDE_PATH=$(bashio::config 'override_path') LOG_LEVEL=$(bashio::config 'log_level') -DATABASE_URL=$(bashio::config 'database_url') +EXTERNAL_DB_URL=$(bashio::config 'database_url') STORAGE_TYPE=$(bashio::config 'storage_type') STORAGE_PATH=$(bashio::config 'storage_path') -VLM_ENABLED=$(bashio::config 'vlm_enabled') MQTT_DISCOVERY=$(bashio::config 'mqtt_discovery') -# Get MQTT credentials from HA if available -if bashio::services.available "mqtt"; then +# Wait for PostgreSQL to be ready (if using bundled) +if [[ -z "${EXTERNAL_DB_URL}" ]]; then + bashio::log.info "Waiting for bundled PostgreSQL..." + until pg_isready -h localhost -U postgres -q; do + sleep 1 + done + export DATABASE_URL="postgresql://postgres:homesec@localhost/homesec" + bashio::log.info "Bundled PostgreSQL is ready" +else + export DATABASE_URL="${EXTERNAL_DB_URL}" + bashio::log.info "Using external database: ${EXTERNAL_DB_URL%%@*}@..." +fi + +# Get MQTT credentials from HA if MQTT Discovery enabled +if [[ "${MQTT_DISCOVERY}" == "true" ]] && bashio::services.available "mqtt"; then MQTT_HOST=$(bashio::services mqtt "host") MQTT_PORT=$(bashio::services mqtt "port") MQTT_USER=$(bashio::services mqtt "username") MQTT_PASS=$(bashio::services mqtt "password") - export MQTT_HOST MQTT_PORT MQTT_USER MQTT_PASS - bashio::log.info "Using Home Assistant MQTT broker at ${MQTT_HOST}:${MQTT_PORT}" + bashio::log.info "MQTT Discovery enabled via ${MQTT_HOST}:${MQTT_PORT}" fi -# Create config directory if needed +# Create directories mkdir -p "$(dirname "${CONFIG_PATH}")" mkdir -p "$(dirname "${OVERRIDE_PATH}")" +mkdir -p "${STORAGE_PATH}" -# Generate config if it doesn't exist +# Generate base config if it doesn't exist if [[ ! -f "${CONFIG_PATH}" ]]; then bashio::log.info "Generating initial configuration at ${CONFIG_PATH}" cat > "${CONFIG_PATH}" << EOF @@ -1243,22 +1631,34 @@ storage: state_store: type: postgres - url_env: DATABASE_URL + dsn_env: DATABASE_URL notifiers: - - type: mqtt - host_env: MQTT_HOST - port_env: MQTT_PORT - username_env: MQTT_USER - password_env: MQTT_PASS - discovery: - enabled: ${MQTT_DISCOVERY} + # Primary: Push events to HA via Events API (uses SUPERVISOR_TOKEN automatically) + - type: home_assistant server: enabled: true host: 0.0.0.0 port: 8080 EOF + + # Optionally add MQTT notifier if user enabled MQTT Discovery + if [[ "${MQTT_DISCOVERY}" == "true" ]] && bashio::services.available "mqtt"; then + bashio::log.info "Adding MQTT Discovery notifier to config" + cat >> "${CONFIG_PATH}" << EOF + + # Optional: MQTT Discovery for users who prefer MQTT entities + - type: mqtt + host_env: MQTT_HOST + port_env: MQTT_PORT + auth: + username_env: MQTT_USER + password_env: MQTT_PASS + discovery: + enabled: true +EOF + fi fi # Create override file if missing @@ -1270,34 +1670,16 @@ config_version: 1 EOF fi -# Set up database if using local PostgreSQL -if [[ -z "${DATABASE_URL}" ]]; then - bashio::log.info "No database URL specified, using SQLite fallback" - export DATABASE_URL="sqlite:///config/homesec/homesec.db" -fi - -# Export secrets from config -if bashio::config.has_value 'dropbox_token'; then - export DROPBOX_TOKEN=$(bashio::config 'dropbox_token') -fi - -if bashio::config.has_value 'openai_api_key'; then - export OPENAI_API_KEY=$(bashio::config 'openai_api_key') -fi - -# Create storage directory -mkdir -p "${STORAGE_PATH}" - bashio::log.info "Starting HomeSec..." -# Run HomeSec +# Run HomeSec with both config files (base + overrides) exec python3 -m homesec.cli run \ --config "${CONFIG_PATH}" \ --config "${OVERRIDE_PATH}" \ --log-level "${LOG_LEVEL}" ``` -### 3.5 Ingress Configuration +### 3.8 Ingress Configuration **File:** `homesec/rootfs/etc/nginx/includes/ingress.conf` @@ -1318,7 +1700,7 @@ location / { } ``` -### 3.6 Acceptance Criteria +### 3.9 Acceptance Criteria - [ ] Add-on installs successfully from repository - [ ] Auto-configures MQTT from Home Assistant @@ -1370,7 +1752,7 @@ custom_components/homesec/ "name": "HomeSec", "codeowners": ["@lan17"], "config_flow": true, - "dependencies": ["mqtt"], + "dependencies": [], "single_config_entry": true, "documentation": "https://github.com/lan17/homesec", "integration_type": "hub", @@ -1704,7 +2086,7 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.api_key = api_key self.verify_ssl = verify_ssl self._session = async_get_clientsession(hass, verify_ssl=verify_ssl) - self._mqtt_unsub: callable | None = None + self._event_unsubs: list[callable] = [] @property def base_url(self) -> str: @@ -1750,35 +2132,50 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): except aiohttp.ClientError as err: raise UpdateFailed(f"Error communicating with HomeSec: {err}") from err - async def async_start_mqtt(self) -> None: - """Subscribe to HomeSec MQTT topics for push updates.""" - if self._mqtt_unsub is not None: + async def async_subscribe_events(self) -> None: + """Subscribe to HomeSec events fired via HA Events API.""" + if self._event_unsubs: return - from homeassistant.components import mqtt - - async def _on_message(msg: mqtt.ReceiveMessage) -> None: - await self._handle_mqtt_event(msg.topic, msg.payload) - - self._mqtt_unsub = await mqtt.async_subscribe( - self.hass, - "homesec/+/alert", - _on_message, + from homeassistant.core import Event + + async def _on_alert(event: Event) -> None: + """Handle homesec_alert event.""" + _LOGGER.debug("Received homesec_alert event: %s", event.data) + # Store latest alert data for entities to consume + camera = event.data.get("camera") + if camera: + self.data.setdefault("latest_alerts", {})[camera] = event.data + # Trigger immediate data refresh + await self.async_request_refresh() + + async def _on_camera_health(event: Event) -> None: + """Handle homesec_camera_health event.""" + _LOGGER.debug("Received homesec_camera_health event: %s", event.data) + await self.async_request_refresh() + + async def _on_clip_recorded(event: Event) -> None: + """Handle homesec_clip_recorded event.""" + _LOGGER.debug("Received homesec_clip_recorded event: %s", event.data) + # Optionally refresh, or just log + + # Subscribe to HomeSec events + self._event_unsubs.append( + self.hass.bus.async_listen("homesec_alert", _on_alert) ) + self._event_unsubs.append( + self.hass.bus.async_listen("homesec_camera_health", _on_camera_health) + ) + self._event_unsubs.append( + self.hass.bus.async_listen("homesec_clip_recorded", _on_clip_recorded) + ) + _LOGGER.info("Subscribed to HomeSec events") - async def async_stop_mqtt(self) -> None: - """Stop MQTT subscription.""" - if self._mqtt_unsub is not None: - self._mqtt_unsub() - self._mqtt_unsub = None - - async def _handle_mqtt_event(self, topic: str, payload: bytes) -> None: - """Handle incoming MQTT event.""" - _ = topic - _ = payload - - # Trigger immediate data refresh on alerts - await self.async_request_refresh() + async def async_unsubscribe_events(self) -> None: + """Unsubscribe from HomeSec events.""" + for unsub in self._event_unsubs: + unsub() + self._event_unsubs.clear() # API Methods @@ -2401,31 +2798,39 @@ tests/ ### Integration Tests -1. **MQTT Discovery Tests:** - - Publish discovery → verify entities appear in HA - - HA restart → verify discovery republishes - - Camera add → verify new entities created - -2. **API Tests:** +1. **API Tests:** - Full CRUD cycle for cameras - - MQTT event topic delivery triggers HA refresh - Authentication flows - - MQTT broker unreachable does not stall clip processing + - Returns 503 when Postgres is unavailable -3. **Add-on Tests:** +3. **HA Notifier Tests:** + - Supervisor mode detection (SUPERVISOR_TOKEN) + - Standalone mode requires url_env + token_env + - Events fire correctly: homesec_alert, homesec_camera_health, homesec_clip_recorded + - HA unreachable does not stall clip processing (best-effort) + +4. **Add-on Tests:** - Installation on HA OS - - MQTT auto-configuration - - Ingress access + - SUPERVISOR_TOKEN injected automatically + - HA Events API works (homesec_alert reaches HA) + - Ingress access to API ### Manual Testing Checklist - [ ] Install add-on from repository - [ ] Configure via HA UI - [ ] Add/remove cameras -- [ ] Verify entities update on alerts +- [ ] Verify entities update on alerts (via HA Events) - [ ] Test automations with HomeSec triggers - [ ] Verify snapshots and links (if enabled) in dashboard +### Optional: MQTT Discovery Tests (Phase 1) + +If user enables MQTT Discovery: +- [ ] Publish discovery → verify entities appear in HA +- [ ] HA restart → verify discovery republishes +- [ ] Camera add → verify new entities created + --- ## Migration Guide @@ -2451,14 +2856,6 @@ tests/ ## Appendix: File Change Summary -### Phase 1: MQTT Discovery -- `src/homesec/models/config.py` - Add MQTTDiscoveryConfig -- `src/homesec/plugins/notifiers/mqtt.py` - Enhance with discovery -- `src/homesec/plugins/notifiers/mqtt_discovery.py` - New file -- `src/homesec/app.py` - Register cameras with notifier -- `config/example.yaml` - Add discovery example -- `tests/unit/plugins/notifiers/test_mqtt_discovery.py` - New tests - ### Phase 2: REST API - `src/homesec/api/` - New package (server.py, routes/*, dependencies.py) - `src/homesec/config/manager.py` - Config persistence + restart signaling @@ -2468,15 +2865,29 @@ tests/ - `src/homesec/app.py` - Integrate API server - `pyproject.toml` - Add fastapi, uvicorn dependencies +### Phase 2.5: Home Assistant Notifier +- `src/homesec/models/config.py` - Add HomeAssistantNotifierConfig +- `src/homesec/plugins/notifiers/home_assistant.py` - New file (HA Events API notifier) +- `config/example.yaml` - Add home_assistant notifier example +- `tests/unit/plugins/notifiers/test_home_assistant.py` - New tests + +### Phase 4: Integration +- `custom_components/homesec/` - Full integration package (uses HA event subscriptions) +- HACS repository configuration + ### Phase 3: Add-on - New repository: `homesec-ha-addons/` -- `homesec/config.yaml` - Add-on manifest +- `homesec/config.yaml` - Add-on manifest (homeassistant_api: true for SUPERVISOR_TOKEN) - `homesec/Dockerfile` - Container build - `homesec/rootfs/` - Startup scripts, nginx config -### Phase 4: Integration -- `custom_components/homesec/` - Full integration package -- HACS repository configuration +### Phase 1: MQTT Discovery (Optional) +- `src/homesec/models/config.py` - Add MQTTDiscoveryConfig +- `src/homesec/plugins/notifiers/mqtt.py` - Enhance with discovery +- `src/homesec/plugins/notifiers/mqtt_discovery.py` - New file +- `src/homesec/app.py` - Register cameras with notifier +- `config/example.yaml` - Add discovery example +- `tests/unit/plugins/notifiers/test_mqtt_discovery.py` - New tests ### Phase 5: Advanced - `custom_components/homesec/www/` - Lovelace cards @@ -2487,12 +2898,16 @@ tests/ | Phase | Duration | Dependencies | |-------|----------|--------------| -| Phase 1: MQTT Discovery | 2-3 days | None | | Phase 2: REST API | 5-7 days | None | -| Phase 3: Add-on | 3-4 days | Phase 2 | +| Phase 2.5: HA Notifier | 2-3 days | None | +| Phase 4: Integration | 7-10 days | Phase 2, 2.5 | +| Phase 3: Add-on | 3-4 days | Phase 2, 2.5 | +| Phase 1: MQTT Discovery (optional) | 2-3 days | None | | Phase 4: Integration | 7-10 days | Phase 2 | | Phase 5: Advanced | 5-7 days | Phase 4 | -**Total: 22-31 days** +**Total: 21-30 days** (excluding optional MQTT Discovery) + +Execution order: Phase 2 → Phase 2.5 → Phase 4 → Phase 3. Phase 1 (MQTT Discovery) is optional. Phase 5 follows Phase 4. -Execution order for Option A: Phase 2 -> Phase 4 -> Phase 3, with Phase 1 optional/parallel. Phase 5 follows Phase 4. +Key benefit: No MQTT broker required. Add-on users get zero-config real-time events via `SUPERVISOR_TOKEN`. diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index ef993ca5..36cfa77b 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -2,9 +2,9 @@ ## Executive Summary -HomeSec is already well-architected for Home Assistant integration with its plugin system and existing MQTT notifier. The key opportunity is to go from "basic MQTT alerts" to a **first-class Home Assistant experience** where users can: +HomeSec is already well-architected for Home Assistant integration with its plugin system. The key opportunity is to provide a **first-class Home Assistant experience** where users can: -1. **Install** homesec directly from Home Assistant +1. **Install** homesec directly from Home Assistant (zero-config for add-on users) 2. **Configure** cameras, alerts, and AI settings through the HA UI 3. **Monitor** camera health, detection stats, and pipeline status as HA entities 4. **Automate** based on detection events with rich metadata @@ -25,6 +25,10 @@ HomeSec is already well-architected for Home Assistant integration with its plug - Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). - Tests: Given/When/Then comments required for all new tests. - P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). +- **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically; standalone users provide HA URL + token. +- **No MQTT required**: MQTT Discovery is optional for users who prefer it; primary path uses HA Events API. +- **409 Conflict UX**: Show error to user when config version is stale. +- **API during Postgres outage**: Return 503 Service Unavailable. ## Constraints and Non-Goals @@ -32,7 +36,250 @@ HomeSec is already well-architected for Home Assistant integration with its plug - Avoid in-process hot-reload of pipeline components in v1; prefer restart after config changes. - Do not move heavy inference into Home Assistant (keep compute inside HomeSec runtime). - Prefer behavioral tests that assert outcomes, not internal state. -- Prefer behavioral tests; avoid internal state assertions. + +--- + +## Features + +### Core Features (v1) + +| Feature | Description | Priority | +|---------|-------------|----------| +| **Zero-config Add-on Install** | User installs add-on, it works immediately with HA | P0 | +| **Real-time Alerts** | Detection events appear in HA instantly | P0 | +| **Camera Management** | Add/remove/configure cameras via HA UI | P0 | +| **Entity Creation** | Sensors, binary sensors for each camera | P0 | +| **Alert Policy Config** | Set risk thresholds and activity filters per camera | P1 | +| **Clip Browsing** | View recent clips and their analysis results | P1 | +| **Health Monitoring** | Camera online/offline status as entities | P1 | +| **HA Automations** | Trigger automations on detection events | P0 | + +### Entities Created Per Camera + +| Entity | Type | Description | +|--------|------|-------------| +| `binary_sensor.homesec_{camera}_motion` | Binary Sensor | Motion detected (on for 30s after alert) | +| `binary_sensor.homesec_{camera}_person` | Binary Sensor | Person detected | +| `sensor.homesec_{camera}_last_activity` | Sensor | Last activity type (person, vehicle, package, etc.) | +| `sensor.homesec_{camera}_risk_level` | Sensor | Risk level (LOW/MEDIUM/HIGH/CRITICAL) | +| `sensor.homesec_{camera}_health` | Sensor | healthy/unhealthy | +| `switch.homesec_{camera}_enabled` | Switch | Enable/disable camera processing | + +### Hub Entities + +| Entity | Type | Description | +|--------|------|-------------| +| `sensor.homesec_hub_status` | Sensor | online/offline | +| `sensor.homesec_clips_today` | Sensor | Number of clips recorded today | +| `sensor.homesec_alerts_today` | Sensor | Number of alerts fired today | + +### HA Events Fired + +| Event | Trigger | Data | +|-------|---------|------| +| `homesec_alert` | Detection alert | camera, clip_id, activity_type, risk_level, summary, view_url | +| `homesec_camera_health` | Camera status change | camera, healthy, status | +| `homesec_clip_recorded` | New clip recorded | clip_id, camera | + +### Future Features (v2+) + +- Custom Lovelace card with detection timeline +- Clip playback in HA dashboard +- Per-camera alert schedules (e.g., only alert at night) +- Integration with HA zones (e.g., suppress alerts when home) + +--- + +## User Onboarding Flows + +### Flow A: Home Assistant Add-on User (Recommended) + +This is the zero-config experience for HA OS / Supervised users. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Step 1: Install Add-on │ +├─────────────────────────────────────────────────────────────┤ +│ User goes to Settings → Add-ons → Add-on Store │ +│ Adds HomeSec repository URL │ +│ Clicks "Install" on HomeSec add-on │ +│ │ +│ Result: Add-on container starts with SUPERVISOR_TOKEN │ +│ HA Events API works automatically │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 2: Install Integration │ +├─────────────────────────────────────────────────────────────┤ +│ User goes to Settings → Devices & Services → Add Integration│ +│ Searches for "HomeSec" │ +│ Integration auto-discovers add-on at localhost:8080 │ +│ User clicks "Submit" │ +│ │ +│ Result: Integration connects, entities created │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 3: Add First Camera │ +├─────────────────────────────────────────────────────────────┤ +│ Integration prompts: "No cameras configured. Add one?" │ +│ User enters: │ +│ - Camera name: "front_door" │ +│ - Type: RTSP │ +│ - RTSP URL: rtsp://192.168.1.100:554/stream │ +│ Clicks "Add Camera" │ +│ │ +│ Result: Config saved, add-on restarts, camera starts │ +│ Entities appear: homesec_front_door_* │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 4: Configure Notifications (Optional) │ +├─────────────────────────────────────────────────────────────┤ +│ User creates automation in HA: │ +│ Trigger: Event "homesec_alert" │ +│ Condition: risk_level = HIGH or CRITICAL │ +│ Action: Send notification to mobile app │ +│ │ +│ Result: User gets push notifications on high-risk events │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 5: Done! │ +├─────────────────────────────────────────────────────────────┤ +│ HomeSec is running and integrated with HA │ +│ - Cameras recording and analyzing │ +│ - Entities updating in real-time │ +│ - Automations firing on detections │ +│ - Clips uploading to configured storage │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Time to first alert: ~5 minutes** + +### Flow B: Standalone Docker User + +For users running HomeSec outside HA (separate server, NAS, etc.). + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Step 1: Deploy HomeSec │ +├─────────────────────────────────────────────────────────────┤ +│ User runs HomeSec via Docker on their server │ +│ Configures cameras, storage, VLM in config.yaml │ +│ │ +│ (This is existing standalone setup) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 2: Create HA Long-Lived Token │ +├─────────────────────────────────────────────────────────────┤ +│ User goes to HA → Profile → Long-Lived Access Tokens │ +│ Creates token named "HomeSec" │ +│ Copies token value │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 3: Configure HomeSec for HA │ +├─────────────────────────────────────────────────────────────┤ +│ User adds to config.yaml: │ +│ │ +│ notifiers: │ +│ - type: home_assistant │ +│ url_env: HA_URL # http://homeassistant:8123 │ +│ token_env: HA_TOKEN # the long-lived token │ +│ │ +│ Sets environment variables and restarts HomeSec │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 4: Install Integration in HA │ +├─────────────────────────────────────────────────────────────┤ +│ User installs HomeSec integration via HACS │ +│ Enters HomeSec URL (e.g., http://nas:8080) │ +│ Integration connects and creates entities │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Step 5: Done! │ +├─────────────────────────────────────────────────────────────┤ +│ Same result as Flow A, but with manual token setup │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Time to first alert: ~15 minutes** (more manual steps) + +--- + +## Success Metrics + +| Metric | Target | +|--------|--------| +| Add-on install to first entity | < 2 minutes | +| Add-on install to first alert | < 5 minutes | +| Entity update latency (detection → HA) | < 1 second | +| Config change to active (restart) | < 30 seconds | + +--- + +## Add-on Architecture + +The HomeSec add-on is a Docker container managed by HA Supervisor. It bundles PostgreSQL for zero-config deployment. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Home Assistant OS │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────────────────────────┐ │ +│ │ HA Core │ │ HomeSec Add-on │ │ +│ │ (container) │ │ (container) │ │ +│ │ │ │ │ │ +│ │ - Frontend │ │ ┌────────────┐ ┌────────────┐ │ │ +│ │ - Automtic │ │ │ PostgreSQL │ │ HomeSec │ │ │ +│ │ - Integratns│ │ │ (s6 svc) │ │ (s6 svc) │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ │ │ Port 5432 │ │ Port 8080 │ │ │ +│ │ │◄──│──│ (internal) │ │ (ingress) │ │ │ +│ │ │ │ └────────────┘ └────────────┘ │ │ +│ └──────────────┘ │ │ │ +│ ▲ │ /data/postgres/ (DB storage) │ │ +│ │ │ /media/homesec/ (clips) │ │ +│ │ └──────────────────────────────────┘ │ +│ │ HA Events API │ +│ │ (SUPERVISOR_TOKEN) │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ HA Supervisor │ │ +│ │ - Manages all add-on containers │ │ +│ │ - Injects SUPERVISOR_TOKEN into HomeSec │ │ +│ │ - Handles restarts, updates, logs │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Why Bundle PostgreSQL? + +| Approach | User Experience | Recommendation | +|----------|-----------------|----------------| +| Bundled PostgreSQL | Zero-config, just works | **Recommended** | +| Separate PostgreSQL add-on | Two installs, manual config | Not recommended | +| SQLite | Simpler but limited concurrency | Not recommended | + +- Uses **s6-overlay** to run PostgreSQL + HomeSec as two services in one container +- PostgreSQL starts first, HomeSec waits for it to be ready +- Data persists in `/data/postgres/` (survives container restarts) +- No port exposure - PostgreSQL only accessible inside container +- Advanced users can still use external PostgreSQL via `database_url` option + +### Storage Layout + +| Path | Contents | Persistence | +|------|----------|-------------| +| `/data/postgres/` | PostgreSQL database files | Add-on private, persistent | +| `/data/overrides.yaml` | HA-managed config overrides | Add-on private, persistent | +| `/media/homesec/clips/` | Video clips (LocalStorage) | Shared with HA, persistent | +| `/config/homesec/` | Base config file | Shared with HA, persistent | --- @@ -46,33 +293,37 @@ HomeSec is already well-architected for Home Assistant integration with its plug ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │ │ HomeSec Add-on │ │ HomeSec Integration │ │ -│ │ (Docker container)│◄──►│ (HA Core component) │ │ +│ │ (Docker container)│───►│ (HA Core component) │ │ │ │ │ │ │ │ │ │ - Pipeline service │ │ - Config flow UI │ │ │ │ - RTSP sources │ │ - Entity platforms │ │ │ │ - YOLO/VLM │ │ - Services │ │ -│ │ - Storage backends │ │ - MQTT subscriptions │ │ +│ │ - Storage backends │ │ - Event subscriptions │ │ +│ │ - HA Notifier │ │ │ │ │ └─────────────────────┘ └─────────────────────────────┘ │ -│ │ │ │ -│ └────────┬───────────────────┘ │ -│ ▼ │ +│ │ ▲ │ +│ │ SUPERVISOR_TOKEN │ │ +│ └────────────────────────────┘ │ +│ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Communication Layer │ │ -│ │ - REST API (new) for config/control │ │ -│ │ - MQTT for events + state topics │ │ +│ │ - REST API for config/control (Integration → Add-on) │ │ +│ │ - HA Events API for real-time push (Add-on → HA Core) │ │ +│ │ POST /api/events/homesec_alert (auto-authenticated) │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` **Pros:** -- Best of both worlds: heavy processing isolated in add-on +- Zero-config for add-on users (SUPERVISOR_TOKEN is automatic) +- No MQTT broker required +- Real-time event push without polling - Full HA UI configuration via the integration - Works with HA OS, Supervised, and standalone Docker -- Clean separation of concerns **Cons:** - Two components to maintain -- Requires API layer between them +- Standalone users must provide HA URL + long-lived token --- @@ -173,7 +424,7 @@ POST /api/v1/system/restart # Request graceful restart ``` Config updates are validated with Pydantic, written to the override YAML, and return `restart_required: true`. HA can then call a restart endpoint or restart the add-on. -Real-time updates use MQTT topics (no WebSocket in v1). +Real-time updates use HA Events API (no WebSocket or MQTT required in v1). ### Phase 3: Home Assistant Add-on @@ -186,23 +437,26 @@ description: Self-hosted AI video security version: 1.2.2 slug: homesec arch: [amd64, aarch64] +homeassistant_api: true # Grants SUPERVISOR_TOKEN for HA Events API ports: 8080/tcp: 8080 # API/Health map: - addon_config:rw # Store HomeSec config/overrides - media:rw # Store clips services: - - mqtt:need # Requires MQTT broker + - mqtt:want # Optional - for MQTT Discovery if user prefers it options: config_file: /config/homesec/config.yaml override_file: /data/overrides.yaml ``` **Add-on features:** +- Zero-config HA integration via `SUPERVISOR_TOKEN` (automatic) +- Real-time event push to HA without MQTT - Bundled PostgreSQL (or use HA's) -- Auto-configure MQTT from HA's broker - Ingress support for web UI (if we build one) - Watchdog for auto-restart +- Optional MQTT Discovery for users who prefer it ### Phase 4: Native Home Assistant Integration @@ -391,7 +645,7 @@ For custom integration distribution: "name": "HomeSec", "version": "1.0.0", "documentation": "https://github.com/lan17/homesec", - "dependencies": ["mqtt"], + "dependencies": [], "codeowners": ["@lan17"], "single_config_entry": true, "iot_class": "local_push", @@ -401,15 +655,52 @@ For custom integration distribution: --- +## HomeAssistant Notifier Plugin + +New notifier plugin that pushes events directly to Home Assistant: + +```python +class HomeAssistantNotifierConfig(BaseModel): + """Configuration for HA notifier.""" + # For standalone mode (not running as add-on) + url_env: str | None = None # e.g., "HA_URL" -> http://homeassistant.local:8123 + token_env: str | None = None # e.g., "HA_TOKEN" -> long-lived access token + +class HomeAssistantNotifier(Notifier): + """Push events directly to Home Assistant via Events API.""" + + async def notify(self, alert: Alert) -> None: + # When running as add-on, use supervisor API (zero-config) + if os.environ.get("SUPERVISOR_TOKEN"): + url = "http://supervisor/core/api/events/homesec_alert" + headers = {"Authorization": f"Bearer {os.environ['SUPERVISOR_TOKEN']}"} + else: + # Standalone mode - use configured URL/token + url = f"{self._url}/api/events/homesec_alert" + headers = {"Authorization": f"Bearer {self._token}"} + + await self._session.post(url, json=alert.to_ha_event(), headers=headers) +``` + +**Event types fired:** +- `homesec_alert` - Detection alert with full metadata +- `homesec_camera_health` - Camera health status changes +- `homesec_clip_recorded` - New clip recorded + +The HA integration subscribes to these events and updates entities in real-time. + +--- + ## Summary: Recommended Roadmap | Phase | Effort | Value | Description | |-------|--------|-------|-------------| | **1. REST API** | Medium | High | Enable remote configuration and control plane | -| **2. Integration** | High | Very High | Full HA UI configuration | -| **3. Add-on** | Medium | High | One-click install for HA OS users | -| **4. MQTT Discovery (optional)** | Low | Medium | Auto-create entities for non-integration users | -| **5. Dashboard Cards** | Medium | Medium | Rich visualization | +| **2. HA Notifier** | Low | High | New notifier plugin for HA Events API | +| **3. Integration** | High | Very High | Full HA UI configuration + event subscriptions | +| **4. Add-on** | Medium | High | Zero-config install for HA OS users | +| **5. MQTT Discovery (optional)** | Low | Low | For users who prefer MQTT over native integration | +| **6. Dashboard Cards** | Medium | Medium | Rich visualization | --- From e28fb3de57371c85af4538f32023b3bba72a94cd Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 18:08:29 -0800 Subject: [PATCH 06/31] docs: improve HA integration plan for LLM implementability Key improvements: - Add complete repository structure under homeassistant/ folder - Add __init__.py with async_setup_entry/async_unload_entry - Add entity.py base class with device_info and helper methods - Add switch.py for camera enable/disable (stops RTSP connection) - Add motion sensor 30s auto-reset timer in coordinator - Add config_version tracking for optimistic concurrency - Add add-on auto-discovery in config flow with fallback - Add stats endpoint (/api/v1/stats) for hub entities - Add health endpoint with response schema - Fix timeline table duplicate entry - Update all file paths to use homeassistant/ folder structure Decisions documented: - Motion sensor: HA integration timer (30s configurable) - Switch behavior: stops RTSP connection when disabled - Add-on discovery: auto-detect + fallback to manual Co-Authored-By: Claude Opus 4.5 --- docs/home-assistant-implementation-plan.md | 757 ++++++++++++++++-- docs/home-assistant-integration-brainstorm.md | 109 ++- 2 files changed, 761 insertions(+), 105 deletions(-) diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index 0aa90e00..81f84ec3 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -35,6 +35,107 @@ This document provides a comprehensive, step-by-step implementation plan for int --- +## Repository Structure + +All code lives in the main `homesec` monorepo: + +``` +homesec/ +├── src/homesec/ # Main Python package (PyPI) +│ ├── api/ # NEW: REST API +│ │ ├── __init__.py +│ │ ├── server.py +│ │ ├── dependencies.py +│ │ └── routes/ +│ │ ├── __init__.py +│ │ ├── health.py +│ │ ├── config.py +│ │ ├── cameras.py +│ │ ├── clips.py +│ │ ├── events.py +│ │ ├── stats.py +│ │ └── system.py +│ ├── config/ # NEW: Config management +│ │ ├── __init__.py +│ │ ├── loader.py +│ │ └── manager.py +│ ├── plugins/ +│ │ └── notifiers/ +│ │ ├── home_assistant.py # NEW: HA Events API notifier +│ │ └── mqtt_discovery.py # NEW: MQTT discovery (optional) +│ └── ...existing code... +│ +├── homeassistant/ # ALL HA-specific code +│ ├── README.md +│ │ +│ ├── integration/ # Custom component (HACS) +│ │ ├── hacs.json +│ │ └── custom_components/ +│ │ └── homesec/ +│ │ ├── __init__.py +│ │ ├── manifest.json +│ │ ├── const.py +│ │ ├── config_flow.py +│ │ ├── coordinator.py +│ │ ├── entity.py +│ │ ├── binary_sensor.py +│ │ ├── sensor.py +│ │ ├── switch.py +│ │ ├── diagnostics.py +│ │ ├── services.yaml +│ │ ├── strings.json +│ │ └── translations/ +│ │ └── en.json +│ │ +│ └── addon/ # Add-on (HA Supervisor) +│ ├── repository.json +│ ├── README.md +│ └── homesec/ +│ ├── config.yaml +│ ├── build.yaml +│ ├── Dockerfile +│ ├── DOCS.md +│ ├── CHANGELOG.md +│ ├── icon.png +│ ├── logo.png +│ ├── rootfs/ +│ │ └── etc/ +│ │ ├── s6-overlay/ +│ │ │ └── s6-rc.d/ +│ │ │ ├── postgres-init/ +│ │ │ ├── postgres/ +│ │ │ ├── homesec/ +│ │ │ └── user/ +│ │ └── nginx/ +│ │ └── includes/ +│ │ └── ingress.conf +│ └── translations/ +│ └── en.yaml +│ +├── tests/ +│ ├── unit/ +│ │ ├── api/ +│ │ │ ├── test_routes_cameras.py +│ │ │ ├── test_routes_clips.py +│ │ │ └── ... +│ │ └── plugins/ +│ │ └── notifiers/ +│ │ ├── test_home_assistant.py +│ │ └── test_mqtt_discovery.py +│ └── integration/ +│ └── test_ha_integration.py +│ +├── docs/ +└── pyproject.toml # Add fastapi, uvicorn deps +``` + +**Distribution:** +- **PyPI**: `src/homesec/` published as `homesec` package +- **HACS**: Users point to `homeassistant/integration/` +- **Add-on Store**: Users add repo URL `https://github.com/lan17/homesec/homeassistant/addon` + +--- + ## Table of Contents 1. [Decision Snapshot](#decision-snapshot-2026-02-01) @@ -1007,7 +1108,158 @@ async def list_events( ) ``` -Real-time updates use MQTT topics; no WebSocket endpoint in v1. +**New File:** `src/homesec/api/routes/stats.py` + +```python +"""Statistics API routes.""" + +from __future__ import annotations + +from datetime import date, datetime, timedelta + +from fastapi import APIRouter, Depends +from pydantic import BaseModel + +from ..dependencies import get_homesec_app + +router = APIRouter(prefix="/stats") + + +class StatsResponse(BaseModel): + """Response model for statistics.""" + clips_today: int + clips_total: int + alerts_today: int + alerts_total: int + cameras_online: int + cameras_total: int + last_alert_at: datetime | None + last_clip_at: datetime | None + + +@router.get("", response_model=StatsResponse) +async def get_stats(app=Depends(get_homesec_app)): + """Get system-wide statistics.""" + today = date.today() + today_start = datetime.combine(today, datetime.min.time()) + + # Get clip counts + _, clips_today = await app.repository.list_clips(since=today_start, limit=0) + _, clips_total = await app.repository.list_clips(limit=0) + + # Get alert counts (notifications sent) + _, alerts_today = await app.repository.list_events( + event_type="notification_sent", + since=today_start, + limit=0, + ) + _, alerts_total = await app.repository.list_events( + event_type="notification_sent", + limit=0, + ) + + # Get camera health + cameras = app.get_all_sources() + cameras_online = sum(1 for c in cameras if c.is_healthy()) + + # Get last timestamps + recent_clips, _ = await app.repository.list_clips(limit=1) + recent_alerts, _ = await app.repository.list_events( + event_type="notification_sent", + limit=1, + ) + + return StatsResponse( + clips_today=clips_today, + clips_total=clips_total, + alerts_today=alerts_today, + alerts_total=alerts_total, + cameras_online=cameras_online, + cameras_total=len(cameras), + last_alert_at=recent_alerts[0].timestamp if recent_alerts else None, + last_clip_at=recent_clips[0].created_at if recent_clips else None, + ) +``` + +**New File:** `src/homesec/api/routes/health.py` + +```python +"""Health check API routes.""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends, Response, status +from pydantic import BaseModel + +from ..dependencies import get_homesec_app + +router = APIRouter() + + +class SourceHealth(BaseModel): + """Health status for a single source.""" + name: str + healthy: bool + last_heartbeat: float | None + last_heartbeat_age_s: float | None + + +class HealthResponse(BaseModel): + """Response model for health check.""" + status: str # "healthy", "degraded", "unhealthy" + version: str + uptime_s: float + sources: list[SourceHealth] + postgres_connected: bool + + +@router.get("/health", response_model=HealthResponse) +@router.get("/api/v1/health", response_model=HealthResponse) +async def health_check( + response: Response, + app=Depends(get_homesec_app), +): + """Health check endpoint.""" + sources = [] + all_healthy = True + any_healthy = False + + for source in app.get_all_sources(): + healthy = source.is_healthy() + if healthy: + any_healthy = True + else: + all_healthy = False + + sources.append(SourceHealth( + name=source.camera_name, + healthy=healthy, + last_heartbeat=source.last_heartbeat(), + last_heartbeat_age_s=source.last_heartbeat_age(), + )) + + # Check Postgres connectivity + postgres_connected = await app.repository.ping() + + # Determine overall status + if all_healthy and postgres_connected: + health_status = "healthy" + elif any_healthy: + health_status = "degraded" + else: + health_status = "unhealthy" + response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE + + return HealthResponse( + status=health_status, + version=app.version, + uptime_s=app.uptime_seconds(), + sources=sources, + postgres_connected=postgres_connected, + ) +``` + +Real-time updates use HA Events API; no WebSocket endpoint in v1. ### 2.4 API Configuration @@ -1353,7 +1605,7 @@ homesec-ha-addons/ ### 3.2 Add-on Manifest -**File:** `homesec/config.yaml` +**File:** `homeassistant/addon/homesec/config.yaml` Note: Update to current Home Assistant add-on schema: @@ -1440,7 +1692,7 @@ watchdog: http://[HOST]:[PORT:8080]/api/v1/health ### 3.3 Add-on Dockerfile -**File:** `homesec/Dockerfile` +**File:** `homeassistant/addon/homesec/Dockerfile` ```dockerfile # syntax=docker/dockerfile:1 @@ -1517,7 +1769,7 @@ rootfs/etc/s6-overlay/s6-rc.d/ ### 3.5 PostgreSQL Initialization Service -**File:** `rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up` +**File:** `homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up` ```bash #!/command/with-contenv bashio @@ -1559,7 +1811,7 @@ bashio::log.info "PostgreSQL initialization complete" ### 3.6 PostgreSQL Service -**File:** `rootfs/etc/s6-overlay/s6-rc.d/postgres/run` +**File:** `homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres/run` ```bash #!/command/with-contenv bashio @@ -1572,7 +1824,7 @@ exec su postgres -c "postgres -D /data/postgres/data" ### 3.7 HomeSec Service -**File:** `rootfs/etc/s6-overlay/s6-rc.d/homesec/run` +**File:** `homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/run` ```bash #!/command/with-contenv bashio @@ -1681,7 +1933,7 @@ exec python3 -m homesec.cli run \ ### 3.8 Ingress Configuration -**File:** `homesec/rootfs/etc/nginx/includes/ingress.conf` +**File:** `homeassistant/addon/homesec/rootfs/etc/nginx/includes/ingress.conf` ```nginx # Proxy to HomeSec API @@ -1744,7 +1996,7 @@ custom_components/homesec/ ### 4.2 Manifest -**File:** `custom_components/homesec/manifest.json` +**File:** `homeassistant/integration/custom_components/homesec/manifest.json` ```json { @@ -1765,7 +2017,7 @@ custom_components/homesec/ ### 4.3 Constants -**File:** `custom_components/homesec/const.py` +**File:** `homeassistant/integration/custom_components/homesec/const.py` ```python """Constants for HomeSec integration.""" @@ -1783,6 +2035,10 @@ CONF_VERIFY_SSL: Final = "verify_ssl" # Default values DEFAULT_PORT: Final = 8080 DEFAULT_VERIFY_SSL: Final = True +ADDON_HOSTNAME: Final = "localhost" # Add-on runs on same host as HA + +# Motion sensor +DEFAULT_MOTION_RESET_SECONDS: Final = 30 # Platforms PLATFORMS: Final = [ @@ -1812,7 +2068,9 @@ ATTR_DETECTED_OBJECTS: Final = "detected_objects" ### 4.4 Config Flow -**File:** `custom_components/homesec/config_flow.py` +**File:** `homeassistant/integration/custom_components/homesec/config_flow.py` + +The config flow automatically detects if the HomeSec add-on is running and offers one-click setup. ```python """Config flow for HomeSec integration.""" @@ -1835,6 +2093,7 @@ from .const import ( DEFAULT_PORT, CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL, + ADDON_HOSTNAME, ) _LOGGER = logging.getLogger(__name__) @@ -1879,6 +2138,16 @@ async def validate_connection( } +async def detect_addon(hass: HomeAssistant) -> bool: + """Check if HomeSec add-on is running.""" + try: + # Try to connect to add-on at localhost:8080 + await validate_connection(hass, ADDON_HOSTNAME, DEFAULT_PORT, verify_ssl=False) + return True + except (CannotConnect, InvalidAuth, Exception): + return False + + class HomesecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for HomeSec.""" @@ -1891,11 +2160,63 @@ class HomesecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._api_key: str | None = None self._verify_ssl: bool = DEFAULT_VERIFY_SSL self._cameras: list[dict] = [] + self._addon_detected: bool = False async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the initial step.""" + """Handle the initial step - check for add-on first.""" + # Check if add-on is running + if await detect_addon(self.hass): + self._addon_detected = True + return await self.async_step_addon() + + # No add-on found, show manual setup + return await self.async_step_manual(user_input) + + async def async_step_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle add-on auto-discovery confirmation.""" + errors = {} + + if user_input is not None: + # User confirmed, connect to add-on + self._host = ADDON_HOSTNAME + self._port = DEFAULT_PORT + self._verify_ssl = False + + try: + info = await validate_connection( + self.hass, self._host, self._port, verify_ssl=False + ) + self._cameras = info.get("cameras", []) + except CannotConnect: + errors["base"] = "cannot_connect" + # Fall back to manual setup + return await self.async_step_manual() + except Exception: + _LOGGER.exception("Unexpected exception connecting to add-on") + errors["base"] = "unknown" + return await self.async_step_manual() + else: + await self.async_set_unique_id(f"homesec_{self._host}_{self._port}") + self._abort_if_unique_id_configured() + return await self.async_step_cameras() + + # Show confirmation form + return self.async_show_form( + step_id="addon", + description_placeholders={ + "addon_url": f"http://{ADDON_HOSTNAME}:{DEFAULT_PORT}", + }, + errors=errors, + ) + + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle manual setup for standalone HomeSec.""" errors = {} if user_input is not None: @@ -1921,14 +2242,12 @@ class HomesecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - # Check if already configured await self.async_set_unique_id(f"homesec_{self._host}_{self._port}") self._abort_if_unique_id_configured() - return await self.async_step_cameras() return self.async_show_form( - step_id="user", + step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors, ) @@ -1938,28 +2257,25 @@ class HomesecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle camera configuration step.""" if user_input is not None: - # Create the config entry return self.async_create_entry( - title=f"HomeSec ({self._host})", + title=f"HomeSec ({'Add-on' if self._addon_detected else self._host})", data={ CONF_HOST: self._host, CONF_PORT: self._port, CONF_API_KEY: self._api_key, CONF_VERIFY_SSL: self._verify_ssl, + "addon": self._addon_detected, }, options={ "cameras": user_input.get("cameras", []), + "motion_reset_seconds": 30, # Configurable motion sensor reset }, ) - # Build camera selection schema camera_names = [c["name"] for c in self._cameras] schema = vol.Schema( { - vol.Optional( - "cameras", - default=camera_names, - ): vol.All( + vol.Optional("cameras", default=camera_names): vol.All( vol.Coerce(list), [vol.In(camera_names)], ), @@ -1969,9 +2285,7 @@ class HomesecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="cameras", data_schema=schema, - description_placeholders={ - "camera_count": str(len(self._cameras)), - }, + description_placeholders={"camera_count": str(len(self._cameras))}, ) @staticmethod @@ -1997,7 +2311,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - # Fetch current cameras from HomeSec coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] camera_names = [c["name"] for c in coordinator.data.get("cameras", [])] current_cameras = self.config_entry.options.get("cameras", camera_names) @@ -2006,10 +2319,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema( { - vol.Optional( - "cameras", - default=current_cameras, - ): vol.All( + vol.Optional("cameras", default=current_cameras): vol.All( vol.Coerce(list), [vol.In(camera_names)], ), @@ -2017,6 +2327,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): "scan_interval", default=self.config_entry.options.get("scan_interval", 30), ): vol.All(vol.Coerce(int), vol.Range(min=10, max=300)), + vol.Optional( + "motion_reset_seconds", + default=self.config_entry.options.get("motion_reset_seconds", 30), + ): vol.All(vol.Coerce(int), vol.Range(min=5, max=300)), } ), ) @@ -2038,7 +2352,7 @@ Integration behavior for config changes: ### 4.5 Data Coordinator -**File:** `custom_components/homesec/coordinator.py` +**File:** `homeassistant/integration/custom_components/homesec/coordinator.py` ```python """Data coordinator for HomeSec integration.""" @@ -2069,6 +2383,7 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): def __init__( self, hass: HomeAssistant, + config_entry: ConfigEntry, host: str, port: int, api_key: str | None = None, @@ -2085,8 +2400,11 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.port = port self.api_key = api_key self.verify_ssl = verify_ssl + self.config_entry = config_entry self._session = async_get_clientsession(hass, verify_ssl=verify_ssl) self._event_unsubs: list[callable] = [] + self._motion_timers: dict[str, callable] = {} # Camera -> cancel callback + self._config_version: int = 0 # Track for optimistic concurrency @property def base_url(self) -> str: @@ -2121,9 +2439,19 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): response.raise_for_status() cameras_data = await response.json() + # Fetch config for version tracking + async with self._session.get( + f"{self.base_url}/config", + headers=self._headers, + ) as response: + response.raise_for_status() + config_data = await response.json() + self._config_version = config_data.get("config_version", 0) + return { "health": health, "cameras": cameras_data.get("cameras", []), + "config_version": self._config_version, "connected": True, } @@ -2138,16 +2466,41 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): return from homeassistant.core import Event + from homeassistant.helpers.event import async_call_later async def _on_alert(event: Event) -> None: - """Handle homesec_alert event.""" + """Handle homesec_alert event with motion timer.""" _LOGGER.debug("Received homesec_alert event: %s", event.data) - # Store latest alert data for entities to consume camera = event.data.get("camera") - if camera: - self.data.setdefault("latest_alerts", {})[camera] = event.data + if not camera: + return + + # Store latest alert data + self.data.setdefault("latest_alerts", {})[camera] = event.data + + # Set motion active for this camera + self.data.setdefault("motion_active", {})[camera] = True + + # Cancel any existing reset timer for this camera + cancel_key = f"motion_reset_{camera}" + if cancel_key in self._motion_timers: + self._motion_timers[cancel_key]() # Cancel existing timer + + # Schedule motion reset after configured duration + reset_seconds = self.config_entry.options.get("motion_reset_seconds", 30) + + async def reset_motion(_now: datetime) -> None: + """Reset motion sensor after timeout.""" + self.data.setdefault("motion_active", {})[camera] = False + self._motion_timers.pop(cancel_key, None) + self.async_set_updated_data(self.data) + + self._motion_timers[cancel_key] = async_call_later( + self.hass, reset_seconds, reset_motion + ) + # Trigger immediate data refresh - await self.async_request_refresh() + self.async_set_updated_data(self.data) async def _on_camera_health(event: Event) -> None: """Handle homesec_camera_health event.""" @@ -2157,7 +2510,6 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _on_clip_recorded(event: Event) -> None: """Handle homesec_clip_recorded event.""" _LOGGER.debug("Received homesec_clip_recorded event: %s", event.data) - # Optionally refresh, or just log # Subscribe to HomeSec events self._event_unsubs.append( @@ -2195,14 +2547,24 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): camera_type: str, config: dict, ) -> dict: - """Add a new camera.""" + """Add a new camera. Uses optimistic concurrency with config_version.""" + payload = { + "name": name, + "type": camera_type, + "config": config, + "config_version": self._config_version, + } async with self._session.post( f"{self.base_url}/cameras", headers=self._headers, - json={"name": name, "type": camera_type, "config": config}, + json=payload, ) as response: + if response.status == 409: + raise ConfigVersionConflict("Config was modified, please refresh") response.raise_for_status() - return await response.json() + result = await response.json() + self._config_version = result.get("config_version", self._config_version) + return result async def async_update_camera( self, @@ -2210,8 +2572,8 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): config: dict | None = None, alert_policy: dict | None = None, ) -> dict: - """Update camera configuration.""" - payload = {} + """Update camera configuration. Uses optimistic concurrency.""" + payload = {"config_version": self._config_version} if config is not None: payload["config"] = config if alert_policy is not None: @@ -2222,16 +2584,36 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): headers=self._headers, json=payload, ) as response: + if response.status == 409: + raise ConfigVersionConflict("Config was modified, please refresh") response.raise_for_status() - return await response.json() + result = await response.json() + self._config_version = result.get("config_version", self._config_version) + return result async def async_delete_camera(self, camera_name: str) -> None: - """Delete a camera.""" + """Delete a camera. Uses optimistic concurrency.""" async with self._session.delete( f"{self.base_url}/cameras/{camera_name}", headers=self._headers, + params={"config_version": self._config_version}, ) as response: + if response.status == 409: + raise ConfigVersionConflict("Config was modified, please refresh") response.raise_for_status() + result = await response.json() + self._config_version = result.get("config_version", self._config_version) + + async def async_set_camera_enabled(self, camera_name: str, enabled: bool) -> dict: + """Enable or disable a camera (stops RTSP connection when disabled).""" + return await self.async_update_camera( + camera_name, + config={"enabled": enabled}, + ) + + +class ConfigVersionConflict(Exception): + """Raised when config_version is stale (409 Conflict).""" async def async_test_camera(self, camera_name: str) -> dict: """Test camera connection.""" @@ -2243,21 +2625,150 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): return await response.json() ``` -In `async_setup_entry`, start MQTT subscription: +### 4.6 Integration Setup + +**File:** `homeassistant/integration/custom_components/homesec/__init__.py` ```python -await coordinator.async_start_mqtt() +"""HomeSec integration for Home Assistant.""" + +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_API_KEY, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL +from .coordinator import HomesecCoordinator + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SWITCH, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up HomeSec from a config entry.""" + coordinator = HomesecCoordinator( + hass, + entry, + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + api_key=entry.data.get(CONF_API_KEY), + verify_ssl=entry.data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ) + + # Fetch initial data + await coordinator.async_config_entry_first_refresh() + + # Subscribe to HomeSec events (via HA Events API) + await coordinator.async_subscribe_events() + + # Store coordinator + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + # Set up platforms + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + # Register update listener for options changes + entry.async_on_unload(entry.add_update_listener(async_update_options)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + # Unsubscribe from events + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + await coordinator.async_unsubscribe_events() + + # Cancel any pending motion timers + for cancel in coordinator._motion_timers.values(): + cancel() + coordinator._motion_timers.clear() + + # Unload platforms + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) ``` -In `async_unload_entry`, call: +### 4.7 Base Entity + +**File:** `homeassistant/integration/custom_components/homesec/entity.py` ```python -await coordinator.async_stop_mqtt() +"""Base entity for HomeSec integration.""" + +from __future__ import annotations + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import HomesecCoordinator + + +class HomesecEntity(CoordinatorEntity[HomesecCoordinator]): + """Base class for HomeSec entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: HomesecCoordinator, + camera_name: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._camera_name = camera_name + + @property + def device_info(self) -> DeviceInfo: + """Return device info for this entity.""" + return DeviceInfo( + identifiers={(DOMAIN, self._camera_name)}, + name=f"HomeSec {self._camera_name}", + manufacturer="HomeSec", + model="AI Security Camera", + sw_version=self.coordinator.data.get("health", {}).get("version", "unknown"), + via_device=(DOMAIN, "homesec_hub"), + ) + + def _get_camera_data(self) -> dict | None: + """Get camera data from coordinator.""" + cameras = self.coordinator.data.get("cameras", []) + for camera in cameras: + if camera.get("name") == self._camera_name: + return camera + return None + + def _is_motion_active(self) -> bool: + """Check if motion is currently active for this camera.""" + return self.coordinator.data.get("motion_active", {}).get(self._camera_name, False) + + def _get_latest_alert(self) -> dict | None: + """Get the latest alert for this camera.""" + return self.coordinator.data.get("latest_alerts", {}).get(self._camera_name) ``` -### 4.6 Entity Platforms +### 4.8 Entity Platforms -**File:** `custom_components/homesec/sensor.py` +**File:** `homeassistant/integration/custom_components/homesec/sensor.py` ```python """Sensor platform for HomeSec integration.""" @@ -2382,7 +2893,7 @@ class HomesecCameraSensor(HomesecEntity, SensorEntity): return attrs ``` -**File:** `custom_components/homesec/binary_sensor.py` +**File:** `homeassistant/integration/custom_components/homesec/binary_sensor.py` ```python """Binary sensor platform for HomeSec integration.""" @@ -2465,18 +2976,110 @@ class HomesecCameraBinarySensor(HomesecEntity, BinarySensorEntity): key = self.entity_description.key if key == "motion": - return camera.get("motion_detected", False) + # Use timer-based motion state from coordinator + return self._is_motion_active() elif key == "person": - return camera.get("person_detected", False) + # Person detected based on latest alert + alert = self._get_latest_alert() + if alert and self._is_motion_active(): + return alert.get("activity_type") == "person" + return False elif key == "online": return camera.get("healthy", False) return None ``` -### 4.7 Services +**File:** `homeassistant/integration/custom_components/homesec/switch.py` -**File:** `custom_components/homesec/services.yaml` +```python +"""Switch platform for HomeSec integration.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import HomesecCoordinator +from .entity import HomesecEntity + +CAMERA_SWITCHES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key="enabled", + name="Enabled", + icon="mdi:video", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up HomeSec switches.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SwitchEntity] = [] + + for camera in coordinator.data.get("cameras", []): + camera_name = camera["name"] + for description in CAMERA_SWITCHES: + entities.append( + HomesecCameraSwitch(coordinator, camera_name, description) + ) + + async_add_entities(entities) + + +class HomesecCameraSwitch(HomesecEntity, SwitchEntity): + """Representation of a HomeSec camera switch.""" + + def __init__( + self, + coordinator: HomesecCoordinator, + camera_name: str, + description: SwitchEntityDescription, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator, camera_name) + self.entity_description = description + self._attr_unique_id = f"{camera_name}_{description.key}" + + @property + def is_on(self) -> bool | None: + """Return true if the switch is on.""" + camera = self._get_camera_data() + if not camera: + return None + + key = self.entity_description.key + + if key == "enabled": + # Camera is enabled if it's in the config and not explicitly disabled + return camera.get("config", {}).get("enabled", True) + + return None + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on (enable camera, starts RTSP connection).""" + await self.coordinator.async_set_camera_enabled(self._camera_name, True) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off (disable camera, stops RTSP connection).""" + await self.coordinator.async_set_camera_enabled(self._camera_name, False) + await self.coordinator.async_request_refresh() +``` + +### 4.9 Services + +**File:** `homeassistant/integration/custom_components/homesec/services.yaml` ```yaml add_camera: @@ -2554,9 +3157,9 @@ test_camera: integration: homesec ``` -### 4.8 Translations +### 4.10 Translations -**File:** `custom_components/homesec/translations/en.json` +**File:** `homeassistant/integration/custom_components/homesec/translations/en.json` ```json { @@ -2648,7 +3251,7 @@ test_camera: } ``` -### 4.9 Acceptance Criteria +### 4.11 Acceptance Criteria - [ ] Config flow connects to HomeSec and discovers cameras - [ ] All entity platforms create entities correctly @@ -2670,7 +3273,7 @@ test_camera: ### 5.1 Custom Lovelace Card (Optional) -**File:** `custom_components/homesec/www/homesec-camera-card.js` +**File:** `homeassistant/integration/custom_components/homesec/www/homesec-camera-card.js` ```javascript class HomesecCameraCard extends HTMLElement { @@ -2858,6 +3461,8 @@ If user enables MQTT Discovery: ### Phase 2: REST API - `src/homesec/api/` - New package (server.py, routes/*, dependencies.py) +- `src/homesec/api/routes/stats.py` - Stats endpoint for hub entities +- `src/homesec/api/routes/health.py` - Health endpoint with response schema - `src/homesec/config/manager.py` - Config persistence + restart signaling - `src/homesec/models/config.py` - Add FastAPIServerConfig - `src/homesec/config/loader.py` - Support multiple YAML files + merge semantics @@ -2871,15 +3476,26 @@ If user enables MQTT Discovery: - `config/example.yaml` - Add home_assistant notifier example - `tests/unit/plugins/notifiers/test_home_assistant.py` - New tests -### Phase 4: Integration -- `custom_components/homesec/` - Full integration package (uses HA event subscriptions) -- HACS repository configuration - -### Phase 3: Add-on -- New repository: `homesec-ha-addons/` -- `homesec/config.yaml` - Add-on manifest (homeassistant_api: true for SUPERVISOR_TOKEN) -- `homesec/Dockerfile` - Container build -- `homesec/rootfs/` - Startup scripts, nginx config +### Phase 4: Integration (in `homeassistant/integration/`) +- `homeassistant/integration/hacs.json` - HACS configuration +- `homeassistant/integration/custom_components/homesec/__init__.py` - Setup entry +- `homeassistant/integration/custom_components/homesec/manifest.json` - Metadata +- `homeassistant/integration/custom_components/homesec/const.py` - Constants +- `homeassistant/integration/custom_components/homesec/config_flow.py` - Add-on auto-discovery +- `homeassistant/integration/custom_components/homesec/coordinator.py` - Data + event subscriptions + motion timers +- `homeassistant/integration/custom_components/homesec/entity.py` - Base entity class +- `homeassistant/integration/custom_components/homesec/sensor.py` - Sensors +- `homeassistant/integration/custom_components/homesec/binary_sensor.py` - Motion (30s auto-reset) +- `homeassistant/integration/custom_components/homesec/switch.py` - Camera enable/disable (stops RTSP) +- `homeassistant/integration/custom_components/homesec/services.yaml` - Service definitions +- `homeassistant/integration/custom_components/homesec/translations/en.json` - Strings + +### Phase 3: Add-on (in `homeassistant/addon/`) +- `homeassistant/addon/repository.json` - Add-on repository manifest +- `homeassistant/addon/homesec/config.yaml` - Add-on manifest (homeassistant_api: true) +- `homeassistant/addon/homesec/Dockerfile` - Container build with PostgreSQL 16 +- `homeassistant/addon/homesec/rootfs/` - s6-overlay services (postgres-init, postgres, homesec) +- `homeassistant/addon/homesec/rootfs/etc/nginx/` - Ingress config ### Phase 1: MQTT Discovery (Optional) - `src/homesec/models/config.py` - Add MQTTDiscoveryConfig @@ -2890,7 +3506,7 @@ If user enables MQTT Discovery: - `tests/unit/plugins/notifiers/test_mqtt_discovery.py` - New tests ### Phase 5: Advanced -- `custom_components/homesec/www/` - Lovelace cards +- `homeassistant/integration/custom_components/homesec/www/` - Lovelace cards --- @@ -2903,11 +3519,10 @@ If user enables MQTT Discovery: | Phase 4: Integration | 7-10 days | Phase 2, 2.5 | | Phase 3: Add-on | 3-4 days | Phase 2, 2.5 | | Phase 1: MQTT Discovery (optional) | 2-3 days | None | -| Phase 4: Integration | 7-10 days | Phase 2 | | Phase 5: Advanced | 5-7 days | Phase 4 | -**Total: 21-30 days** (excluding optional MQTT Discovery) +**Total: 20-27 days** (excluding optional MQTT Discovery) -Execution order: Phase 2 → Phase 2.5 → Phase 4 → Phase 3. Phase 1 (MQTT Discovery) is optional. Phase 5 follows Phase 4. +Execution order: Phase 2 → Phase 2.5 → Phase 4 → Phase 3 → Phase 5. Phase 1 (MQTT Discovery) is optional. Key benefit: No MQTT broker required. Add-on users get zero-config real-time events via `SUPERVISOR_TOKEN`. diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 36cfa77b..a7af724a 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -39,6 +39,41 @@ HomeSec is already well-architected for Home Assistant integration with its plug --- +## Repository Structure + +All Home Assistant code lives in the main `homesec` monorepo under `homeassistant/`: + +``` +homesec/ +├── src/homesec/ # Main Python package (PyPI) +│ ├── api/ # REST API for HA integration +│ ├── config/ # Config management +│ └── plugins/notifiers/ +│ └── home_assistant.py # HA Events API notifier +│ +├── homeassistant/ # ALL HA-specific code +│ ├── integration/ # Custom component (HACS) +│ │ ├── hacs.json +│ │ └── custom_components/homesec/ +│ │ +│ └── addon/ # Add-on (HA Supervisor) +│ ├── repository.json +│ └── homesec/ +│ ├── config.yaml +│ ├── Dockerfile +│ └── rootfs/ # s6-overlay services +│ +├── tests/ +└── docs/ +``` + +**Distribution:** +- **PyPI**: `src/homesec/` published as `homesec` package +- **HACS**: Users point to `homeassistant/integration/` +- **Add-on Store**: Users add `https://github.com/lan17/homesec/homeassistant/addon` + +--- + ## Features ### Core Features (v1) @@ -428,54 +463,60 @@ Real-time updates use HA Events API (no WebSocket or MQTT required in v1). ### Phase 3: Home Assistant Add-on -Create a Home Assistant add-on for easy installation: +Create a Home Assistant add-on in the monorepo: -```yaml -# config.yaml (add-on manifest) -name: HomeSec -description: Self-hosted AI video security -version: 1.2.2 -slug: homesec -arch: [amd64, aarch64] -homeassistant_api: true # Grants SUPERVISOR_TOKEN for HA Events API -ports: - 8080/tcp: 8080 # API/Health -map: - - addon_config:rw # Store HomeSec config/overrides - - media:rw # Store clips -services: - - mqtt:want # Optional - for MQTT Discovery if user prefers it -options: - config_file: /config/homesec/config.yaml - override_file: /data/overrides.yaml +``` +homeassistant/addon/ +├── repository.json +├── README.md +└── homesec/ + ├── config.yaml # Add-on manifest + ├── build.yaml + ├── Dockerfile # Includes PostgreSQL 16 + ├── DOCS.md + ├── CHANGELOG.md + ├── icon.png # 512x512 + ├── logo.png # 256x256 + ├── rootfs/ + │ └── etc/ + │ ├── s6-overlay/ # PostgreSQL + HomeSec services + │ └── nginx/ # Ingress config + └── translations/ + └── en.yaml ``` **Add-on features:** - Zero-config HA integration via `SUPERVISOR_TOKEN` (automatic) - Real-time event push to HA without MQTT -- Bundled PostgreSQL (or use HA's) +- Bundled PostgreSQL via s6-overlay (zero-config database) - Ingress support for web UI (if we build one) - Watchdog for auto-restart - Optional MQTT Discovery for users who prefer it +Users add the add-on repo via: `https://github.com/lan17/homesec/homeassistant/addon` + ### Phase 4: Native Home Assistant Integration -Build a custom integration (`custom_components/homesec/`): +Build a custom integration in the monorepo: ``` -custom_components/homesec/ -├── __init__.py # Setup, config entry -├── manifest.json # Integration metadata -├── config_flow.py # UI-based configuration -├── const.py # Constants -├── coordinator.py # DataUpdateCoordinator -├── sensor.py # Sensor entities -├── binary_sensor.py # Motion sensors -├── switch.py # Enable/disable cameras -├── services.yaml # Service definitions -├── strings.json # UI strings -└── translations/ - └── en.json +homeassistant/integration/ +├── hacs.json +└── custom_components/ + └── homesec/ + ├── __init__.py # Setup, config entry + ├── manifest.json # Integration metadata + ├── config_flow.py # UI-based configuration (auto-detects add-on) + ├── const.py # Constants + ├── coordinator.py # DataUpdateCoordinator + event subscriptions + ├── entity.py # Base entity class + ├── sensor.py # Sensor entities + ├── binary_sensor.py # Motion sensors (30s auto-reset timer) + ├── switch.py # Enable/disable cameras (stops RTSP) + ├── services.yaml # Service definitions + ├── strings.json # UI strings + └── translations/ + └── en.json ``` **Entity Platforms:** From acf22d3e90155b1175846657b1edc39e017f7489 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 18:16:46 -0800 Subject: [PATCH 07/31] docs: fix repository.json location and add-on URL format - Move repository.json to repo root (required for HA add-on discovery) - Fix Add-on Store URL from invalid path to proper repo URL - Update directory tree in section 4.1 to show full path - Ensure consistency between brainstorm and implementation plan Co-Authored-By: Claude Opus 4.5 --- docs/home-assistant-implementation-plan.md | 47 ++++++++++++------- docs/home-assistant-integration-brainstorm.md | 9 ++-- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index 81f84ec3..af8f55d6 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -41,6 +41,7 @@ All code lives in the main `homesec` monorepo: ``` homesec/ +├── repository.json # HA Add-on repo manifest (must be at root) ├── src/homesec/ # Main Python package (PyPI) │ ├── api/ # NEW: REST API │ │ ├── __init__.py @@ -88,7 +89,6 @@ homesec/ │ │ └── en.json │ │ │ └── addon/ # Add-on (HA Supervisor) -│ ├── repository.json │ ├── README.md │ └── homesec/ │ ├── config.yaml @@ -132,7 +132,7 @@ homesec/ **Distribution:** - **PyPI**: `src/homesec/` published as `homesec` package - **HACS**: Users point to `homeassistant/integration/` -- **Add-on Store**: Users add repo URL `https://github.com/lan17/homesec/homeassistant/addon` +- **Add-on Store**: Users add repo URL `https://github.com/lan17/homesec` (repository.json at repo root points to `homeassistant/addon/homesec/`) --- @@ -1584,21 +1584,27 @@ class TestHomeAssistantNotifier: ### 3.1 Add-on Repository Structure -**New Repository:** `homesec-ha-addons` +**Location:** `homeassistant/addon/` in the main homesec monorepo. + +Users add the add-on via: `https://github.com/lan17/homesec` + +Note: `repository.json` must be at the repo root (not in `homeassistant/addon/`) for Home Assistant to discover it. The file points to `homeassistant/addon/homesec/` as the add-on location. ``` -homesec-ha-addons/ +homeassistant/addon/ ├── README.md -├── repository.json └── homesec/ ├── config.yaml # Add-on manifest ├── Dockerfile # Container build ├── build.yaml # Build configuration - ├── run.sh # Startup script ├── DOCS.md # Documentation ├── CHANGELOG.md # Version history ├── icon.png # Add-on icon (512x512) ├── logo.png # Add-on logo (256x256) + ├── rootfs/ # s6-overlay services + │ └── etc/ + │ ├── s6-overlay/ + │ └── nginx/ └── translations/ └── en.yaml # UI strings ``` @@ -1660,10 +1666,9 @@ schema: # VLM vlm_enabled: bool? openai_api_key: password? - # MQTT Discovery (optional - primary path uses HA Events API) - mqtt_discovery: bool? + # VLM model openai_model: str? - # MQTT Discovery + # MQTT Discovery (optional - primary path uses HA Events API) mqtt_discovery: bool? # Default options @@ -1954,13 +1959,16 @@ location / { ### 3.9 Acceptance Criteria -- [ ] Add-on installs successfully from repository -- [ ] Auto-configures MQTT from Home Assistant +- [ ] Add-on installs successfully from monorepo URL +- [ ] Bundled PostgreSQL starts and initializes correctly +- [ ] HomeSec waits for PostgreSQL before starting +- [ ] SUPERVISOR_TOKEN enables zero-config HA Events API - [ ] Ingress provides access to API/UI - [ ] Configuration options work correctly - [ ] Watchdog restarts on failure - [ ] Logs are accessible in HA - [ ] Works on both amd64 and aarch64 +- [ ] Optional: MQTT Discovery works if user enables it --- @@ -1972,10 +1980,10 @@ location / { ### 4.1 Integration Structure -**Directory:** `custom_components/homesec/` +**Directory:** `homeassistant/integration/custom_components/homesec/` ``` -custom_components/homesec/ +homeassistant/integration/custom_components/homesec/ ├── __init__.py # Setup and entry points ├── manifest.json # Integration metadata ├── const.py # Constants @@ -2361,10 +2369,11 @@ from __future__ import annotations import asyncio import logging -from datetime import timedelta +from datetime import datetime, timedelta from typing import Any import aiohttp +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import ( @@ -3253,10 +3262,14 @@ test_camera: ### 4.11 Acceptance Criteria -- [ ] Config flow connects to HomeSec and discovers cameras +- [ ] Config flow auto-detects add-on and connects to HomeSec +- [ ] Config flow falls back to manual setup for standalone users - [ ] All entity platforms create entities correctly - [ ] DataUpdateCoordinator fetches data at correct intervals -- [ ] MQTT subscription triggers refresh on alerts +- [ ] HA Events subscription triggers refresh on alerts (homesec_alert, homesec_camera_health) +- [ ] Motion sensor auto-resets after configurable timeout (default 30s) +- [ ] Camera switch enables/disables RTSP connection +- [ ] Config version tracking prevents conflicts (409 on stale version) - [ ] Services work correctly (add/remove camera, set policy, test) - [ ] Options flow allows reconfiguration - [ ] Diagnostics provide useful debug information @@ -3491,7 +3504,7 @@ If user enables MQTT Discovery: - `homeassistant/integration/custom_components/homesec/translations/en.json` - Strings ### Phase 3: Add-on (in `homeassistant/addon/`) -- `homeassistant/addon/repository.json` - Add-on repository manifest +- `repository.json` - Add-on repository manifest (at repo root, points to `homeassistant/addon/homesec/`) - `homeassistant/addon/homesec/config.yaml` - Add-on manifest (homeassistant_api: true) - `homeassistant/addon/homesec/Dockerfile` - Container build with PostgreSQL 16 - `homeassistant/addon/homesec/rootfs/` - s6-overlay services (postgres-init, postgres, homesec) diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index a7af724a..b6e14d20 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -45,6 +45,7 @@ All Home Assistant code lives in the main `homesec` monorepo under `homeassistan ``` homesec/ +├── repository.json # HA Add-on repo manifest (must be at root) ├── src/homesec/ # Main Python package (PyPI) │ ├── api/ # REST API for HA integration │ ├── config/ # Config management @@ -57,7 +58,6 @@ homesec/ │ │ └── custom_components/homesec/ │ │ │ └── addon/ # Add-on (HA Supervisor) -│ ├── repository.json │ └── homesec/ │ ├── config.yaml │ ├── Dockerfile @@ -70,7 +70,7 @@ homesec/ **Distribution:** - **PyPI**: `src/homesec/` published as `homesec` package - **HACS**: Users point to `homeassistant/integration/` -- **Add-on Store**: Users add `https://github.com/lan17/homesec/homeassistant/addon` +- **Add-on Store**: Users add `https://github.com/lan17/homesec` (repository.json at root points to `homeassistant/addon/homesec/`) --- @@ -467,7 +467,6 @@ Create a Home Assistant add-on in the monorepo: ``` homeassistant/addon/ -├── repository.json ├── README.md └── homesec/ ├── config.yaml # Add-on manifest @@ -485,6 +484,8 @@ homeassistant/addon/ └── en.yaml ``` +Note: `repository.json` lives at the repo root (not in `homeassistant/addon/`) for HA to discover it. + **Add-on features:** - Zero-config HA integration via `SUPERVISOR_TOKEN` (automatic) - Real-time event push to HA without MQTT @@ -493,7 +494,7 @@ homeassistant/addon/ - Watchdog for auto-restart - Optional MQTT Discovery for users who prefer it -Users add the add-on repo via: `https://github.com/lan17/homesec/homeassistant/addon` +Users add the add-on repo via: `https://github.com/lan17/homesec` ### Phase 4: Native Home Assistant Integration From cb4a093236a91ee4179716e0aef97a9a78c664ba Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 18:22:01 -0800 Subject: [PATCH 08/31] docs: align HA integration plan with plugin config boundary refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update config examples and API models to match the new backend + config pattern from the plugin config boundary cleanup refactor (#18): - Notifiers: type → backend + config - Storage: type → backend + config - Camera: type → source_backend, config → source_config - State store: removed type field (just dsn_env) - Alert policy: per-camera overrides now in alert_policy.config.overrides - Services: updated field names to match new schema Co-Authored-By: Claude Opus 4.5 --- docs/home-assistant-implementation-plan.md | 154 +++++++++--------- docs/home-assistant-integration-brainstorm.md | 7 +- 2 files changed, 78 insertions(+), 83 deletions(-) diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index af8f55d6..cf71a848 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -466,22 +466,23 @@ for entry in self._notifier_entries: ```yaml notifiers: - - type: mqtt - host: localhost - port: 1883 - auth: - username_env: MQTT_USERNAME - password_env: MQTT_PASSWORD - topic_template: "homecam/alerts/{camera_name}" - qos: 1 - retain: false - discovery: - enabled: true - prefix: homeassistant - node_id: homesec - device_name: HomeSec - subscribe_to_birth: true - birth_topic: homeassistant/status + - backend: mqtt + config: + host: localhost + port: 1883 + auth: + username_env: MQTT_USERNAME + password_env: MQTT_PASSWORD + topic_template: "homecam/alerts/{camera_name}" + qos: 1 + retain: false + discovery: + enabled: true + prefix: homeassistant + node_id: homesec + device_name: HomeSec + subscribe_to_birth: true + birth_topic: homeassistant/status ``` ### 1.6 Testing @@ -747,26 +748,24 @@ router = APIRouter(prefix="/cameras") class CameraCreate(BaseModel): """Request model for creating a camera.""" name: str - type: str # rtsp, ftp, local_folder - config: dict # Type-specific configuration + source_backend: str # rtsp, ftp, local_folder + source_config: dict # Backend-specific configuration config_version: int class CameraUpdate(BaseModel): """Request model for updating a camera.""" - config: dict | None = None - alert_policy: dict | None = None + source_config: dict | None = None config_version: int class CameraResponse(BaseModel): """Response model for camera.""" name: str - type: str + source_backend: str healthy: bool last_heartbeat: float | None - config: dict - alert_policy: dict | None + source_config: dict class CameraListResponse(BaseModel): @@ -791,11 +790,10 @@ async def list_cameras(app=Depends(get_homesec_app)): source = app.get_source(camera.name) cameras.append(CameraResponse( name=camera.name, - type=camera.source.type, + source_backend=camera.source.backend, healthy=source.is_healthy() if source else False, last_heartbeat=source.last_heartbeat() if source else None, - config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), - alert_policy=app.get_camera_alert_policy(camera.name), + source_config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), )) return CameraListResponse(cameras=cameras, total=len(cameras)) @@ -813,11 +811,10 @@ async def get_camera(camera_name: str, app=Depends(get_homesec_app)): source = app.get_source(camera_name) return CameraResponse( name=camera.name, - type=camera.source.type, + source_backend=camera.source.backend, healthy=source.is_healthy() if source else False, last_heartbeat=source.last_heartbeat() if source else None, - config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), - alert_policy=app.get_camera_alert_policy(camera_name), + source_config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), ) @@ -833,8 +830,8 @@ async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): try: result = await app.config_store.add_camera( name=camera.name, - source_type=camera.type, - config=camera.config, + source_backend=camera.source_backend, + source_config=camera.source_config, config_version=camera.config_version, ) return ConfigChangeResponse( @@ -842,11 +839,10 @@ async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): config_version=result.config_version, camera=CameraResponse( name=camera.name, - type=camera.type, + source_backend=camera.source_backend, healthy=False, last_heartbeat=None, - config=camera.config, - alert_policy=None, + source_config=camera.source_config, ), ) except ValueError as e: @@ -862,7 +858,7 @@ async def update_camera( update: CameraUpdate, app=Depends(get_homesec_app), ): - """Update a camera's configuration.""" + """Update a camera's source configuration.""" source = app.get_source(camera_name) if not source: raise HTTPException( @@ -872,8 +868,7 @@ async def update_camera( result = await app.config_store.update_camera( camera_name=camera_name, - config=update.config, - alert_policy=update.alert_policy, + source_config=update.source_config, config_version=update.config_version, ) @@ -1487,11 +1482,12 @@ class HomeAssistantNotifier(Notifier): ```yaml notifiers: # Home Assistant notifier (recommended for HA users) - - type: home_assistant - # When running as HA add-on, no configuration needed (uses SUPERVISOR_TOKEN) - # For standalone mode, provide HA URL and token: - # url_env: HA_URL # http://homeassistant.local:8123 - # token_env: HA_TOKEN # Long-lived access token from HA + - backend: home_assistant + config: + # When running as HA add-on, no configuration needed (uses SUPERVISOR_TOKEN) + # For standalone mode, provide HA URL and token: + # url_env: HA_URL # http://homeassistant.local:8123 + # token_env: HA_TOKEN # Long-lived access token from HA ``` ### 2.5.4 Testing @@ -1883,16 +1879,17 @@ version: 1 cameras: [] storage: - type: ${STORAGE_TYPE} - path: ${STORAGE_PATH} + backend: ${STORAGE_TYPE} + config: + path: ${STORAGE_PATH} state_store: - type: postgres dsn_env: DATABASE_URL notifiers: # Primary: Push events to HA via Events API (uses SUPERVISOR_TOKEN automatically) - - type: home_assistant + - backend: home_assistant + config: {} server: enabled: true @@ -1906,14 +1903,15 @@ EOF cat >> "${CONFIG_PATH}" << EOF # Optional: MQTT Discovery for users who prefer MQTT entities - - type: mqtt - host_env: MQTT_HOST - port_env: MQTT_PORT - auth: - username_env: MQTT_USER - password_env: MQTT_PASS - discovery: - enabled: true + - backend: mqtt + config: + host_env: MQTT_HOST + port_env: MQTT_PORT + auth: + username_env: MQTT_USER + password_env: MQTT_PASS + discovery: + enabled: true EOF fi fi @@ -2553,14 +2551,14 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def async_add_camera( self, name: str, - camera_type: str, - config: dict, + source_backend: str, + source_config: dict, ) -> dict: """Add a new camera. Uses optimistic concurrency with config_version.""" payload = { "name": name, - "type": camera_type, - "config": config, + "source_backend": source_backend, + "source_config": source_config, "config_version": self._config_version, } async with self._session.post( @@ -2578,15 +2576,12 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def async_update_camera( self, camera_name: str, - config: dict | None = None, - alert_policy: dict | None = None, + source_config: dict | None = None, ) -> dict: - """Update camera configuration. Uses optimistic concurrency.""" + """Update camera source configuration. Uses optimistic concurrency.""" payload = {"config_version": self._config_version} - if config is not None: - payload["config"] = config - if alert_policy is not None: - payload["alert_policy"] = alert_policy + if source_config is not None: + payload["source_config"] = source_config async with self._session.put( f"{self.base_url}/cameras/{camera_name}", @@ -2617,7 +2612,7 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Enable or disable a camera (stops RTSP connection when disabled).""" return await self.async_update_camera( camera_name, - config={"enabled": enabled}, + source_config={"enabled": enabled}, ) @@ -3102,9 +3097,9 @@ add_camera: example: "front_door" selector: text: - type: - name: Type - description: Camera source type + source_backend: + name: Source Backend + description: Camera source backend type required: true example: "rtsp" selector: @@ -3115,7 +3110,7 @@ add_camera: - "local_folder" rtsp_url: name: RTSP URL - description: RTSP stream URL (for RTSP type) + description: RTSP stream URL (for rtsp backend) example: "rtsp://192.168.1.100:554/stream" selector: text: @@ -3129,7 +3124,7 @@ remove_camera: set_alert_policy: name: Set Alert Policy - description: Configure alert policy for a camera + description: Configure alert policy override for a camera (stored in alert_policy.config.overrides) target: device: integration: homesec @@ -3141,22 +3136,21 @@ set_alert_policy: selector: select: options: - - "LOW" - - "MEDIUM" - - "HIGH" - - "CRITICAL" - activity_types: + - "low" + - "medium" + - "high" + - "critical" + notify_on_activity_types: name: Activity Types description: Activity types that trigger alerts selector: select: multiple: true options: - - "person" - - "vehicle" - - "animal" - - "package" + - "person_at_door" + - "delivery" - "suspicious" + - "animal" test_camera: name: Test Camera diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index b6e14d20..d95a5402 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -220,9 +220,10 @@ For users running HomeSec outside HA (separate server, NAS, etc.). │ User adds to config.yaml: │ │ │ │ notifiers: │ -│ - type: home_assistant │ -│ url_env: HA_URL # http://homeassistant:8123 │ -│ token_env: HA_TOKEN # the long-lived token │ +│ - backend: home_assistant │ +│ config: │ +│ url_env: HA_URL # http://homeassistant:8123 │ +│ token_env: HA_TOKEN # the long-lived token │ │ │ │ Sets environment variables and restarts HomeSec │ └─────────────────────────────────────────────────────────────┘ From 948a74024f8237ef9c030f8750fc49a75a413b13 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 18:28:15 -0800 Subject: [PATCH 09/31] docs: comprehensive review - fix bugs, simplify v1 scope Critical fixes: - Add prerequisites section for core HomeSec changes needed (enabled field, health monitoring, stats methods) - Fix switch implementation: add error handling, correct field path - Remove unimplemented platforms from v1 (camera, image, select) - Remove services requiring new API endpoints (set_alert_policy, test_camera) Simplifications: - Mark Phase 1 (MQTT Discovery) as out-of-scope for v1 with warning - Simplify services to just add_camera and remove_camera - Update acceptance criteria to reflect v1 scope - Add v1 scope column to brainstorm entity platforms table Code quality: - Add HomeAssistantError import and _LOGGER to switch.py - Add try/catch with proper error messages in switch methods - Add docstring prerequisite note in switch.py Co-Authored-By: Claude Opus 4.5 --- docs/home-assistant-implementation-plan.md | 121 +++++++++--------- docs/home-assistant-integration-brainstorm.md | 40 +++--- 2 files changed, 81 insertions(+), 80 deletions(-) diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index cf71a848..be5b4744 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -24,6 +24,24 @@ This document provides a comprehensive, step-by-step implementation plan for int - **Camera ping**: RTSP ping implementation should include TODO for real connectivity test (not part of this integration work). - **Delete clip**: Deletes from both local storage and cloud storage (existing pattern in cleanup_clips.py). +## Prerequisites (Changes to Core HomeSec) + +Before implementing the HA integration, these changes are needed in the core HomeSec codebase: + +1. **Add `enabled` field to CameraConfig** (`src/homesec/models/config.py`): + ```python + class CameraConfig(BaseModel): + name: str + enabled: bool = True # NEW: Allow disabling camera via API + source: CameraSourceConfig + ``` + +2. **Add camera health monitoring**: The Application needs to periodically check camera health and call `notifier.publish_camera_health()` when status changes. + +3. **Add stats methods to ClipRepository**: + - `count_clips_since(since: datetime) -> int` + - `count_alerts_since(since: datetime) -> int` + ## Execution Order (Option A) 1. Phase 2: REST API for Configuration (control plane) @@ -151,13 +169,15 @@ homesec/ --- -## Phase 1: MQTT Discovery Enhancement (Optional) +## Phase 1: MQTT Discovery Enhancement (Optional - Future) + +> **⚠️ OUT OF SCOPE FOR V1**: This phase is optional and should be implemented only after the core integration (Phases 2-4) is complete and stable. The primary integration path uses the HA Events API which requires no MQTT broker. -**Goal:** Auto-create Home Assistant entities from HomeSec without requiring the native integration. This is an optional feature for users who prefer MQTT over the primary HA Events API approach. +**Goal:** Auto-create Home Assistant entities from HomeSec without requiring the native integration. This is for users who prefer MQTT over the primary HA Events API approach. **Estimated Effort:** 2-3 days -**Note:** This phase is optional. The primary integration path uses the HA Events API (Phase 2.5) which requires no MQTT broker. +**Warning:** If both MQTT Discovery AND the native integration are enabled, you will have duplicate entities. Users should choose one approach, not both. ### 1.1 Configuration Model Updates @@ -2046,12 +2066,10 @@ ADDON_HOSTNAME: Final = "localhost" # Add-on runs on same host as HA # Motion sensor DEFAULT_MOTION_RESET_SECONDS: Final = 30 -# Platforms +# Platforms (v1 - core functionality only) +# TODO v2: Add "camera", "image", "select" platforms PLATFORMS: Final = [ "binary_sensor", - "camera", - "image", - "select", "sensor", "switch", ] @@ -2997,18 +3015,26 @@ class HomesecCameraBinarySensor(HomesecEntity, BinarySensorEntity): **File:** `homeassistant/integration/custom_components/homesec/switch.py` ```python -"""Switch platform for HomeSec integration.""" +"""Switch platform for HomeSec integration. + +Prerequisite: Add `enabled: bool = True` field to CameraConfig in +src/homesec/models/config.py. The switch toggles this field via the API. +""" from __future__ import annotations +import logging from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) from .coordinator import HomesecCoordinator from .entity import HomesecEntity @@ -3065,20 +3091,28 @@ class HomesecCameraSwitch(HomesecEntity, SwitchEntity): key = self.entity_description.key if key == "enabled": - # Camera is enabled if it's in the config and not explicitly disabled - return camera.get("config", {}).get("enabled", True) + # NOTE: Requires `enabled: bool` field on CameraConfig (see prerequisite below) + return camera.get("enabled", True) return None async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on (enable camera, starts RTSP connection).""" - await self.coordinator.async_set_camera_enabled(self._camera_name, True) - await self.coordinator.async_request_refresh() + try: + await self.coordinator.async_set_camera_enabled(self._camera_name, True) + await self.coordinator.async_request_refresh() + except Exception as err: + _LOGGER.error("Failed to enable camera %s: %s", self._camera_name, err) + raise HomeAssistantError(f"Failed to enable camera: {err}") from err async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off (disable camera, stops RTSP connection).""" - await self.coordinator.async_set_camera_enabled(self._camera_name, False) - await self.coordinator.async_request_refresh() + try: + await self.coordinator.async_set_camera_enabled(self._camera_name, False) + await self.coordinator.async_request_refresh() + except Exception as err: + _LOGGER.error("Failed to disable camera %s: %s", self._camera_name, err) + raise HomeAssistantError(f"Failed to disable camera: {err}") from err ``` ### 4.9 Services @@ -3122,42 +3156,16 @@ remove_camera: device: integration: homesec -set_alert_policy: - name: Set Alert Policy - description: Configure alert policy override for a camera (stored in alert_policy.config.overrides) - target: - device: - integration: homesec - fields: - min_risk_level: - name: Minimum Risk Level - description: Minimum risk level to trigger alerts - required: true - selector: - select: - options: - - "low" - - "medium" - - "high" - - "critical" - notify_on_activity_types: - name: Activity Types - description: Activity types that trigger alerts - selector: - select: - multiple: true - options: - - "person_at_door" - - "delivery" - - "suspicious" - - "animal" - -test_camera: - name: Test Camera - description: Test camera connection - target: - device: - integration: homesec +# TODO v2: Add these services (require additional API endpoints) +# set_alert_policy: +# name: Set Alert Policy +# description: Configure alert policy override for a camera +# (requires PUT /api/v1/config/alert_policy endpoint) +# +# test_camera: +# name: Test Camera +# description: Test camera connection +# (requires POST /api/v1/cameras/{name}/test endpoint) ``` ### 4.10 Translations @@ -3241,14 +3249,6 @@ test_camera: "remove_camera": { "name": "Remove Camera", "description": "Remove a camera from HomeSec" - }, - "set_alert_policy": { - "name": "Set Alert Policy", - "description": "Configure alert policy for a camera" - }, - "test_camera": { - "name": "Test Camera", - "description": "Test camera connection" } } } @@ -3262,13 +3262,14 @@ test_camera: - [ ] DataUpdateCoordinator fetches data at correct intervals - [ ] HA Events subscription triggers refresh on alerts (homesec_alert, homesec_camera_health) - [ ] Motion sensor auto-resets after configurable timeout (default 30s) -- [ ] Camera switch enables/disables RTSP connection +- [ ] Camera switch enables/disables camera (requires `enabled` field prerequisite) - [ ] Config version tracking prevents conflicts (409 on stale version) -- [ ] Services work correctly (add/remove camera, set policy, test) +- [ ] Services work correctly (add_camera, remove_camera) - [ ] Options flow allows reconfiguration - [ ] Diagnostics provide useful debug information - [ ] All strings are translatable - [ ] HACS installation works +- [ ] Error handling shows user-friendly messages on API failures --- diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index d95a5402..6acb0a23 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -523,35 +523,35 @@ homeassistant/integration/ **Entity Platforms:** -| Platform | Entities | Description | -|----------|----------|-------------| -| `image` | Per-camera | Last snapshot (optional) | -| `binary_sensor` | `motion`, `person_detected` | Detection states | -| `sensor` | `last_activity`, `risk_level`, `clip_count` | Detection metadata | -| `switch` | `camera_enabled`, `alerts_enabled` | Per-camera toggles | -| `select` | `alert_sensitivity` | LOW/MEDIUM/HIGH | -| `device_tracker` | `camera_online` | Connectivity status (optional) | +| Platform | Entities | Description | v1 Scope | +|----------|----------|-------------|----------| +| `binary_sensor` | `motion` | Motion detected (30s auto-reset) | ✓ | +| `sensor` | `last_activity`, `risk_level`, `health` | Detection metadata | ✓ | +| `switch` | `camera_enabled` | Enable/disable camera | ✓ | +| `image` | Per-camera | Last snapshot | v2 | +| `select` | `alert_sensitivity` | LOW/MEDIUM/HIGH | v2 | -**Services:** +**Services (v1):** ```yaml homesec.add_camera: description: Add a new camera fields: name: Camera identifier - rtsp_url: RTSP stream URL + source_backend: rtsp, ftp, or local_folder + rtsp_url: RTSP stream URL (for rtsp backend) -homesec.set_alert_policy: - description: Configure alert policy for camera - fields: - camera: Camera name - min_risk_level: Minimum risk level to alert - activity_types: List of activity types to alert on +homesec.remove_camera: + description: Remove a camera + target: + device: Camera device +``` -homesec.process_clip: - description: Manually process a video clip - fields: - file_path: Path to video file +**Services (v2 - Future):** + +```yaml +# homesec.set_alert_policy - requires new API endpoint +# homesec.test_camera - requires new API endpoint ``` **Config Flow:** From 916ff207d251da5dd81e7f87a56fad8e19a4bca1 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 19:35:20 -0800 Subject: [PATCH 10/31] docs: split HA integration plan into per-phase documents Restructure the implementation plan for better LLM implementability: - Split monolithic plan into 6 separate phase documents - Add Phase 0 (Prerequisites) for core HomeSec changes - Replace full code implementations with interfaces + constraints - Add clear test expectations with fixture names - Add verification commands and Definition of Done per phase - Clarify list merge semantics as union (not replace) Each phase document is self-contained with: - Goal and dependencies - Interfaces/protocols (not full implementation) - Constraints and file changes - Test expectations (behavioral, Given/When/Then) - Verification steps - Definition of Done checklist Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-0-prerequisites.md | 232 ++ docs/ha-phase-1-rest-api.md | 405 ++ docs/ha-phase-2-ha-notifier.md | 263 ++ docs/ha-phase-3-addon.md | 352 ++ docs/ha-phase-4-ha-integration.md | 514 +++ docs/ha-phase-5-advanced.md | 118 + docs/home-assistant-implementation-plan.md | 3553 +---------------- docs/home-assistant-integration-brainstorm.md | 40 +- 8 files changed, 1967 insertions(+), 3510 deletions(-) create mode 100644 docs/ha-phase-0-prerequisites.md create mode 100644 docs/ha-phase-1-rest-api.md create mode 100644 docs/ha-phase-2-ha-notifier.md create mode 100644 docs/ha-phase-3-addon.md create mode 100644 docs/ha-phase-4-ha-integration.md create mode 100644 docs/ha-phase-5-advanced.md diff --git a/docs/ha-phase-0-prerequisites.md b/docs/ha-phase-0-prerequisites.md new file mode 100644 index 00000000..6870986e --- /dev/null +++ b/docs/ha-phase-0-prerequisites.md @@ -0,0 +1,232 @@ +# Phase 0: Core Prerequisites + +**Goal**: Prepare the HomeSec core codebase for HA integration by adding required fields and interfaces. + +**Estimated Effort**: 2-3 days + +**Dependencies**: None + +--- + +## Overview + +Before implementing the HA integration, these changes are needed in the core HomeSec codebase: + +1. Add `enabled` field to `CameraConfig` +2. Add `publish_camera_health()` to Notifier interface +3. Implement camera health monitoring loop in Application +4. Add stats methods to ClipRepository + +--- + +## 0.1 Add `enabled` Field to CameraConfig + +**File**: `src/homesec/models/config.py` + +### Interface + +```python +class CameraConfig(BaseModel): + """Camera configuration and clip source selection.""" + + name: str + enabled: bool = True # NEW: Allow disabling camera via API + source: CameraSourceConfig +``` + +### Constraints + +- Default is `True` (backwards compatible) +- When `enabled=False`, Application must not start the source +- API can toggle this field; requires restart to take effect + +--- + +## 0.2 Add `publish_camera_health()` to Notifier Interface + +**File**: `src/homesec/interfaces.py` + +### Interface + +```python +class Notifier(Protocol): + """Notification service interface.""" + + async def start(self) -> None: ... + async def shutdown(self) -> None: ... + async def notify(self, alert: Alert) -> None: ... + + # NEW: Camera health status change + async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: + """Publish camera health status change. + + Called by Application when a camera transitions between healthy/unhealthy. + Implementations should be best-effort (don't raise on failure). + """ + ... +``` + +### Constraints + +- Must be async +- Must be best-effort (failures don't crash pipeline) +- Called on health state transitions, not on every health check +- Existing notifiers (MQTT, SendGrid) can implement as no-op initially + +--- + +## 0.3 Update MultiplexNotifier + +**File**: `src/homesec/notifiers/multiplex.py` + +### Interface + +```python +class MultiplexNotifier: + """Routes notifications to multiple backends.""" + + async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: + """Broadcast camera health to all notifiers.""" + # Fan out to all notifiers, log errors but don't raise +``` + +### Constraints + +- Must fan out to all configured notifiers +- Must handle individual notifier failures gracefully +- Should log errors but continue to other notifiers + +--- + +## 0.4 Camera Health Monitoring in Application + +**File**: `src/homesec/app.py` + +### Interface + +```python +class Application: + """Main application orchestrator.""" + + async def _monitor_camera_health(self) -> None: + """Background task that monitors camera health and publishes changes. + + Runs every N seconds (configurable), checks is_healthy() on each source, + and calls notifier.publish_camera_health() when state changes. + """ + ... + + async def _start_sources(self) -> None: + """Start all enabled camera sources. + + Must check camera.enabled before starting each source. + """ + ... +``` + +### Constraints + +- Health check interval should be configurable (default: 30s) +- Only publish on state transitions (healthy→unhealthy or unhealthy→healthy) +- Track previous health state per camera +- Must respect `CameraConfig.enabled` - don't start disabled cameras +- Health monitoring task should be cancelled on shutdown + +--- + +## 0.5 Add Stats Methods to ClipRepository + +**File**: `src/homesec/repository.py` (or appropriate location) + +### Interface + +```python +class ClipRepository(Protocol): + """Repository for clip and event data.""" + + # Existing methods... + + # NEW: Stats methods for API + async def count_clips_since(self, since: datetime) -> int: + """Count clips created since the given timestamp.""" + ... + + async def count_alerts_since(self, since: datetime) -> int: + """Count alert events (notification_sent) since the given timestamp.""" + ... +``` + +### Constraints + +- Must use async SQLAlchemy +- Should be efficient (use COUNT, not fetch all) +- `count_alerts_since` counts events where `event_type='notification_sent'` + +--- + +## File Changes Summary + +| File | Change | +|------|--------| +| `src/homesec/models/config.py` | Add `enabled: bool = True` to `CameraConfig` | +| `src/homesec/interfaces.py` | Add `publish_camera_health()` to `Notifier` protocol | +| `src/homesec/notifiers/multiplex.py` | Implement `publish_camera_health()` fan-out | +| `src/homesec/plugins/notifiers/mqtt.py` | Add no-op `publish_camera_health()` | +| `src/homesec/plugins/notifiers/sendgrid_email.py` | Add no-op `publish_camera_health()` | +| `src/homesec/app.py` | Add health monitoring loop, respect `enabled` field | +| `src/homesec/repository.py` | Add `count_clips_since()`, `count_alerts_since()` | + +--- + +## Test Expectations + +### Fixtures Needed + +- `camera_config_enabled` - CameraConfig with enabled=True +- `camera_config_disabled` - CameraConfig with enabled=False +- `mock_notifier` - Notifier that records calls to `publish_camera_health()` +- `mock_source_healthy` - ClipSource where `is_healthy()` returns True +- `mock_source_unhealthy` - ClipSource where `is_healthy()` returns False + +### Test Cases + +**CameraConfig.enabled** +- Given a config with `enabled=False`, when Application starts, then source is not started +- Given a config with `enabled=True` (or missing), when Application starts, then source is started + +**publish_camera_health** +- Given a camera transitions from healthy to unhealthy, when health monitor runs, then `publish_camera_health(camera, False)` is called +- Given a camera stays healthy, when health monitor runs, then `publish_camera_health` is NOT called +- Given MultiplexNotifier with 3 notifiers and one fails, when `publish_camera_health` is called, then other notifiers still receive the call + +**count_clips_since / count_alerts_since** +- Given 5 clips created today and 10 yesterday, when `count_clips_since(today_start)` is called, then returns 5 +- Given 0 alerts, when `count_alerts_since(any_date)` is called, then returns 0 + +--- + +## Verification + +```bash +# Run unit tests +pytest tests/unit/test_app.py -v -k "health" +pytest tests/unit/models/test_config.py -v -k "enabled" +pytest tests/unit/notifiers/test_multiplex.py -v -k "camera_health" + +# Verify CameraConfig accepts enabled field +python -c "from homesec.models.config import CameraConfig; print(CameraConfig(name='test', enabled=False, source={'backend': 'rtsp', 'config': {}}))" +``` + +--- + +## Definition of Done + +- [ ] `CameraConfig` has `enabled: bool = True` field +- [ ] `Notifier` protocol includes `publish_camera_health()` +- [ ] `MultiplexNotifier` fans out `publish_camera_health()` to all notifiers +- [ ] Existing notifiers (MQTT, SendGrid) have no-op implementations +- [ ] Application skips starting sources where `enabled=False` +- [ ] Application monitors camera health and calls `publish_camera_health()` on transitions +- [ ] `ClipRepository` has `count_clips_since()` and `count_alerts_since()` +- [ ] All tests pass +- [ ] Existing functionality unchanged (backwards compatible) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md new file mode 100644 index 00000000..7271eebe --- /dev/null +++ b/docs/ha-phase-1-rest-api.md @@ -0,0 +1,405 @@ +# Phase 1: REST API for Configuration + +**Goal**: Enable remote configuration and monitoring of HomeSec via HTTP API. + +**Estimated Effort**: 5-7 days + +**Dependencies**: Phase 0 (Prerequisites) + +--- + +## Overview + +This phase adds a FastAPI-based REST API to HomeSec for: +- Camera CRUD operations +- Clip listing and management +- Event history +- System stats and health +- Configuration management with optimistic concurrency + +--- + +## 1.1 API Framework Setup + +### Files + +- `src/homesec/api/__init__.py` +- `src/homesec/api/server.py` +- `src/homesec/api/dependencies.py` + +### Interfaces + +**APIServer** (`server.py`) +```python +def create_app(app_instance: Application) -> FastAPI: + """Create the FastAPI application. + + - Stores Application reference in app.state.homesec + - Configures CORS from server_config.cors_origins + - Registers all route modules + """ + ... + +class APIServer: + """Manages the API server lifecycle.""" + + def __init__(self, app: FastAPI, host: str, port: int): ... + async def start(self) -> None: ... + async def stop(self) -> None: ... +``` + +**Dependencies** (`dependencies.py`) +```python +async def get_homesec_app(request: Request) -> Application: + """Get the HomeSec Application instance from request state. + + Raises HTTPException 503 if not initialized. + """ + ... + +async def verify_api_key(request: Request, app=Depends(get_homesec_app)) -> None: + """Verify API key if authentication is enabled. + + - Checks app.config.server.auth_enabled + - Expects Authorization: Bearer + - Raises HTTPException 401 on failure + """ + ... +``` + +### Constraints + +- All endpoints must be `async def` +- No blocking operations (use `asyncio.to_thread` for file I/O) +- CORS origins configurable via `FastAPIServerConfig.cors_origins` +- Port configurable, replaces old `HealthConfig` + +--- + +## 1.2 Config Management + +### Files + +- `src/homesec/config/manager.py` +- `src/homesec/config/loader.py` + +### Interfaces + +**ConfigManager** (`manager.py`) +```python +class ConfigUpdateResult(BaseModel): + """Result of a config update operation.""" + config_version: int + restart_required: bool = True + +class ConfigManager: + """Manages configuration persistence with optimistic concurrency.""" + + def __init__(self, base_paths: list[Path], override_path: Path): ... + + @property + def config_version(self) -> int: + """Current config version for optimistic concurrency.""" + ... + + def get_config(self) -> Config: + """Get the current merged configuration.""" + ... + + async def add_camera( + self, + name: str, + enabled: bool, + source_backend: str, + source_config: dict, + config_version: int, + ) -> ConfigUpdateResult: + """Add a new camera to the override config. + + Raises: + ValueError: If camera name already exists + ConfigVersionConflict: If config_version is stale + """ + ... + + async def update_camera( + self, + camera_name: str, + enabled: bool | None, + source_config: dict | None, + config_version: int, + ) -> ConfigUpdateResult: + """Update an existing camera in the override config. + + Raises: + ValueError: If camera doesn't exist + ConfigVersionConflict: If config_version is stale + """ + ... + + async def remove_camera( + self, + camera_name: str, + config_version: int, + ) -> ConfigUpdateResult: + """Remove a camera from the override config. + + Raises: + ValueError: If camera doesn't exist + ConfigVersionConflict: If config_version is stale + """ + ... + +class ConfigVersionConflict(Exception): + """Raised when config_version doesn't match current version.""" + pass +``` + +**ConfigLoader** (`loader.py`) +```python +def load_configs(paths: list[Path]) -> Config: + """Load and merge multiple YAML config files. + + Merge semantics: + - Files loaded left to right, rightmost wins + - Dicts: deep merge (recursive) + - Lists: merge (union, preserving order, no duplicates) + + The override file may contain a top-level `config_version` key. + """ + ... + +def deep_merge(base: dict, override: dict) -> dict: + """Deep merge two dicts. + + - Nested dicts are merged recursively + - Lists are merged (union) + - Scalars: override wins + """ + ... +``` + +### Constraints + +- Override file written atomically (write temp, fsync, rename) +- `config_version` stored in override file, incremented on each write +- All file I/O via `asyncio.to_thread` +- Base config is read-only; all mutations go to override file +- Override file is machine-owned (no comment preservation needed) + +--- + +## 1.3 API Routes + +### Files + +- `src/homesec/api/routes/__init__.py` +- `src/homesec/api/routes/cameras.py` +- `src/homesec/api/routes/clips.py` +- `src/homesec/api/routes/events.py` +- `src/homesec/api/routes/stats.py` +- `src/homesec/api/routes/health.py` +- `src/homesec/api/routes/config.py` +- `src/homesec/api/routes/system.py` + +### Route Summary + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/health` | Health check | +| GET | `/api/v1/config` | Config summary and version | +| GET | `/api/v1/cameras` | List cameras | +| GET | `/api/v1/cameras/{name}` | Get camera | +| POST | `/api/v1/cameras` | Create camera | +| PUT | `/api/v1/cameras/{name}` | Update camera | +| DELETE | `/api/v1/cameras/{name}` | Delete camera | +| GET | `/api/v1/cameras/{name}/status` | Camera status | +| POST | `/api/v1/cameras/{name}/test` | Test camera connection | +| GET | `/api/v1/clips` | List clips (paginated, filterable) | +| GET | `/api/v1/clips/{id}` | Get clip | +| DELETE | `/api/v1/clips/{id}` | Delete clip | +| POST | `/api/v1/clips/{id}/reprocess` | Reprocess clip | +| GET | `/api/v1/events` | List events (filterable) | +| GET | `/api/v1/stats` | System statistics | +| POST | `/api/v1/system/restart` | Request graceful restart | +| GET | `/api/v1/system/health/detailed` | Detailed health with error codes | + +### Request/Response Models + +**Camera Models** +```python +class CameraCreate(BaseModel): + name: str + enabled: bool = True + source_backend: str # rtsp, ftp, local_folder + source_config: dict + config_version: int + +class CameraUpdate(BaseModel): + enabled: bool | None = None + source_config: dict | None = None + config_version: int + +class CameraResponse(BaseModel): + name: str + enabled: bool + source_backend: str + healthy: bool + last_heartbeat: float | None + source_config: dict # Secrets are env var names, not values + +class ConfigChangeResponse(BaseModel): + restart_required: bool = True + config_version: int + camera: CameraResponse | None = None +``` + +### Constraints + +- All config-mutating endpoints return `restart_required: True` +- All config-mutating endpoints require `config_version` for optimistic concurrency +- Return 409 Conflict when `config_version` is stale +- Return 503 Service Unavailable when Postgres is down +- Pagination: `page` (1-indexed), `page_size` (default 50, max 100) + +--- + +## 1.4 Server Configuration + +**File**: `src/homesec/models/config.py` + +### Interface + +```python +class FastAPIServerConfig(BaseModel): + """Configuration for the FastAPI server (replaces HealthConfig).""" + + enabled: bool = True + host: str = "0.0.0.0" + port: int = 8080 + cors_origins: list[str] = Field(default_factory=lambda: ["*"]) + auth_enabled: bool = False + api_key_env: str | None = None # Env var name, not the key itself + + def get_api_key(self) -> str | None: + """Resolve API key from environment variable.""" + ... + +# Update Config class: +# - Replace `health: HealthConfig` with `server: FastAPIServerConfig` +``` + +--- + +## 1.5 CLI Updates + +**File**: `src/homesec/cli.py` + +### Interface + +```python +# Support multiple --config flags +# Example: homesec run --config base.yaml --config overrides.yaml + +@click.option( + "--config", + "config_paths", + multiple=True, + type=click.Path(exists=True), + help="Config file(s). Can be specified multiple times. Later files override earlier.", +) +``` + +### Constraints + +- Order matters: files loaded left to right +- Default override path: `config/ha-overrides.yaml` (if exists) +- Override path can be explicitly passed as last `--config` + +--- + +## File Changes Summary + +| File | Change | +|------|--------| +| `src/homesec/api/__init__.py` | New package | +| `src/homesec/api/server.py` | FastAPI app factory and server | +| `src/homesec/api/dependencies.py` | Request dependencies | +| `src/homesec/api/routes/*.py` | All route modules | +| `src/homesec/config/manager.py` | Config persistence | +| `src/homesec/config/loader.py` | Multi-file config loading | +| `src/homesec/models/config.py` | Add `FastAPIServerConfig` | +| `src/homesec/cli.py` | Support multiple `--config` flags | +| `src/homesec/app.py` | Integrate API server | +| `pyproject.toml` | Add fastapi, uvicorn dependencies | + +--- + +## Test Expectations + +### Fixtures Needed + +- `test_client` - FastAPI TestClient with mocked Application +- `mock_config_store` - ConfigManager that operates in-memory +- `mock_repository` - ClipRepository with canned data +- `sample_camera_config` - Valid CameraConfig for RTSP +- `sample_clip` - Clip with all fields populated + +### Test Cases + +**Camera CRUD** +- Given no cameras, when POST /cameras with valid data, then 201 and camera created +- Given camera "front", when GET /cameras/front, then returns camera data +- Given camera "front", when DELETE /cameras/front with correct version, then 200 +- Given camera "front", when DELETE /cameras/front with stale version, then 409 Conflict + +**Config Version** +- Given config_version=5, when update with version=4, then 409 Conflict +- Given config_version=5, when update with version=5, then 200 and new version=6 + +**Health** +- Given Postgres is up, when GET /health, then status="healthy" +- Given Postgres is down, when GET /health, then 503 and status="unhealthy" + +**Clips** +- Given 100 clips, when GET /clips?page=2&page_size=10, then returns clips 11-20 + +--- + +## Verification + +```bash +# Run API tests +pytest tests/unit/api/ -v + +# Start server manually +homesec run --config config/example.yaml + +# Test endpoints +curl http://localhost:8080/api/v1/health +curl http://localhost:8080/api/v1/cameras +curl http://localhost:8080/api/v1/config + +# Check OpenAPI docs +open http://localhost:8080/docs +``` + +--- + +## Definition of Done + +- [ ] FastAPI server starts and serves requests +- [ ] All CRUD operations for cameras work +- [ ] Config changes are validated and persisted to override file +- [ ] Config changes return `restart_required: true` +- [ ] Stale `config_version` returns 409 Conflict +- [ ] `/api/v1/system/restart` triggers graceful shutdown +- [ ] Clip listing with pagination and filtering works +- [ ] Event history API works +- [ ] Stats endpoint returns correct counts +- [ ] OpenAPI documentation is accurate at `/docs` +- [ ] CORS works for configured origins +- [ ] API authentication (when enabled) works +- [ ] Returns 503 when Postgres is unavailable +- [ ] Multiple `--config` flags work in CLI +- [ ] All tests pass diff --git a/docs/ha-phase-2-ha-notifier.md b/docs/ha-phase-2-ha-notifier.md new file mode 100644 index 00000000..10a8ae2a --- /dev/null +++ b/docs/ha-phase-2-ha-notifier.md @@ -0,0 +1,263 @@ +# Phase 2: Home Assistant Notifier Plugin + +**Goal**: Enable real-time event push from HomeSec to Home Assistant without requiring MQTT. + +**Estimated Effort**: 2-3 days + +**Dependencies**: None (can be done in parallel with Phase 1) + +--- + +## Overview + +This phase adds a new notifier plugin that pushes events directly to Home Assistant via the HA Events API. When running as an add-on, it uses `SUPERVISOR_TOKEN` for zero-config authentication. + +--- + +## 2.1 Configuration Model + +**File**: `src/homesec/models/config.py` + +### Interface + +```python +class HomeAssistantNotifierConfig(BaseModel): + """Configuration for Home Assistant notifier. + + When running as HA add-on, SUPERVISOR_TOKEN is used automatically. + For standalone mode, url_env and token_env must be provided. + """ + + # Standalone mode only (ignored when SUPERVISOR_TOKEN present) + url_env: str | None = None # e.g., "HA_URL" -> http://homeassistant.local:8123 + token_env: str | None = None # e.g., "HA_TOKEN" -> long-lived access token + + # Event configuration + event_prefix: str = "homesec" # Events: homesec_alert, homesec_camera_health, etc. +``` + +### Constraints + +- `url_env` and `token_env` store env var names, not actual values +- When `SUPERVISOR_TOKEN` env var exists, use supervisor mode automatically +- In supervisor mode, url_env and token_env are ignored + +--- + +## 2.2 Notifier Implementation + +**File**: `src/homesec/plugins/notifiers/home_assistant.py` + +### Interface + +```python +@plugin(plugin_type=PluginType.NOTIFIER, name="home_assistant") +class HomeAssistantNotifier(Notifier): + """Push events directly to Home Assistant via Events API.""" + + config_cls = HomeAssistantNotifierConfig + + def __init__(self, config: HomeAssistantNotifierConfig): ... + + async def start(self) -> None: + """Initialize HTTP session and detect supervisor mode. + + Supervisor mode: SUPERVISOR_TOKEN env var exists + Standalone mode: requires url_env and token_env in config + + Raises: + ValueError: If standalone mode and url_env/token_env missing + """ + ... + + async def shutdown(self) -> None: + """Close HTTP session.""" + ... + + async def notify(self, alert: Alert) -> None: + """Send alert to Home Assistant as homesec_alert event. + + Event data includes: + - camera: str + - clip_id: str + - activity_type: str + - risk_level: str + - summary: str + - view_url: str | None + - storage_uri: str | None + - timestamp: ISO8601 string + - detected_objects: list[str] (if analysis present) + """ + ... + + async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: + """Send camera health event to Home Assistant. + + Event: homesec_camera_health + Data: {camera: str, healthy: bool, status: "healthy"|"unhealthy"} + """ + ... + + async def publish_clip_recorded(self, clip_id: str, camera_name: str) -> None: + """Send clip recorded event to Home Assistant. + + Event: homesec_clip_recorded + Data: {clip_id: str, camera: str} + """ + ... +``` + +### Internal Methods + +```python +def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: + """Get the URL and headers for the HA Events API. + + Supervisor mode: + URL: http://supervisor/core/api/events/{prefix}_{event_type} + Auth: Bearer {SUPERVISOR_TOKEN} + + Standalone mode: + URL: {resolved_url}/api/events/{prefix}_{event_type} + Auth: Bearer {resolved_token} + """ + ... +``` + +### Constraints + +- All event publishing is best-effort (log errors, don't raise) +- Use aiohttp for HTTP requests +- Events API endpoint: `POST /api/events/{event_type}` +- Supervisor URL: `http://supervisor/core/api/events/...` +- Event names: `{event_prefix}_alert`, `{event_prefix}_camera_health`, `{event_prefix}_clip_recorded` + +--- + +## 2.3 Events Reference + +### homesec_alert + +Fired when an alert is generated after VLM analysis. + +```json +{ + "camera": "front_door", + "clip_id": "abc123", + "activity_type": "person_at_door", + "risk_level": "medium", + "summary": "Person approached front door and rang doorbell", + "view_url": "https://dropbox.com/...", + "storage_uri": "dropbox:///clips/abc123.mp4", + "timestamp": "2026-02-01T10:30:00Z", + "detected_objects": ["person"] +} +``` + +### homesec_camera_health + +Fired when camera health status changes. + +```json +{ + "camera": "front_door", + "healthy": false, + "status": "unhealthy" +} +``` + +### homesec_clip_recorded + +Fired when a new clip is recorded (before analysis). + +```json +{ + "clip_id": "abc123", + "camera": "front_door" +} +``` + +--- + +## 2.4 Configuration Example + +**File**: `config/example.yaml` (add section) + +```yaml +notifiers: + # Home Assistant notifier (recommended for HA users) + - backend: home_assistant + config: + # When running as HA add-on, no configuration needed (uses SUPERVISOR_TOKEN) + # For standalone mode, provide HA URL and token: + # url_env: HA_URL # http://homeassistant.local:8123 + # token_env: HA_TOKEN # Long-lived access token from HA + event_prefix: homesec # Optional, default is "homesec" +``` + +--- + +## File Changes Summary + +| File | Change | +|------|--------| +| `src/homesec/models/config.py` | Add `HomeAssistantNotifierConfig` | +| `src/homesec/plugins/notifiers/home_assistant.py` | New notifier plugin | +| `config/example.yaml` | Add home_assistant notifier example | + +--- + +## Test Expectations + +### Fixtures Needed + +- `ha_notifier_config` - HomeAssistantNotifierConfig with url_env and token_env +- `ha_notifier` - Initialized HomeAssistantNotifier +- `sample_alert` - Alert with all fields populated +- `mock_aiohttp_session` - Mocked aiohttp.ClientSession + +### Test Cases + +**Supervisor Mode Detection** +- Given SUPERVISOR_TOKEN env var is set, when notifier starts, then supervisor mode is enabled +- Given SUPERVISOR_TOKEN not set and config has url_env/token_env, when notifier starts, then standalone mode is enabled +- Given SUPERVISOR_TOKEN not set and config missing url_env, when notifier starts, then ValueError raised + +**Event Sending** +- Given notifier in standalone mode, when notify() called with alert, then POST to {url}/api/events/homesec_alert +- Given notifier in supervisor mode, when notify() called, then POST to http://supervisor/core/api/events/homesec_alert +- Given HA returns 401, when notify() called, then error logged but no exception raised +- Given HA unreachable, when notify() called, then error logged but no exception raised + +**Camera Health** +- Given notifier started, when publish_camera_health("front", False), then POST homesec_camera_health event with healthy=false + +--- + +## Verification + +```bash +# Run notifier tests +pytest tests/unit/plugins/notifiers/test_home_assistant.py -v + +# Manual testing (requires HA instance) +# 1. Start HomeSec with home_assistant notifier configured +# 2. Trigger a clip recording +# 3. Check HA Developer Tools > Events for homesec_* events + +# Test with HA dev tools +# In HA: Developer Tools > Events > Listen to event > "homesec_alert" +``` + +--- + +## Definition of Done + +- [ ] Notifier auto-detects supervisor mode via `SUPERVISOR_TOKEN` +- [ ] Zero-config works when running as HA add-on +- [ ] Standalone mode requires `url_env` and `token_env` +- [ ] Events fired: `homesec_alert`, `homesec_camera_health`, `homesec_clip_recorded` +- [ ] Notification failures don't crash the pipeline (best-effort) +- [ ] Events contain all required metadata +- [ ] Config example added +- [ ] All tests pass diff --git a/docs/ha-phase-3-addon.md b/docs/ha-phase-3-addon.md new file mode 100644 index 00000000..45ccba8f --- /dev/null +++ b/docs/ha-phase-3-addon.md @@ -0,0 +1,352 @@ +# Phase 3: Home Assistant Add-on + +**Goal**: Provide one-click installation for Home Assistant OS/Supervised users. + +**Estimated Effort**: 3-4 days + +**Dependencies**: Phase 1 (REST API), Phase 2 (HA Notifier) + +--- + +## Overview + +This phase creates a Home Assistant add-on that: +- Bundles PostgreSQL for zero-config database +- Uses s6-overlay for service management +- Provides ingress access to the API +- Auto-configures the HA notifier via SUPERVISOR_TOKEN + +--- + +## 3.1 Repository Structure + +**Location**: `homeassistant/addon/` in the main homesec monorepo. + +Users add the add-on via: `https://github.com/lan17/homesec` + +``` +homeassistant/addon/ +├── README.md +└── homesec/ + ├── config.yaml # Add-on manifest + ├── Dockerfile # Container build + ├── build.yaml # Build configuration + ├── DOCS.md # Documentation + ├── CHANGELOG.md # Version history + ├── icon.png # Add-on icon (512x512) + ├── logo.png # Add-on logo (256x256) + ├── rootfs/ # s6-overlay services + │ └── etc/ + │ ├── s6-overlay/ + │ │ └── s6-rc.d/ + │ │ ├── postgres-init/ + │ │ ├── postgres/ + │ │ ├── homesec/ + │ │ └── user/ + │ └── nginx/ + │ └── includes/ + │ └── ingress.conf + └── translations/ + └── en.yaml # UI strings +``` + +**Note**: `repository.json` must be at the repo root (not in `homeassistant/addon/`) for Home Assistant to discover it. + +--- + +## 3.2 Add-on Manifest + +**File**: `homeassistant/addon/homesec/config.yaml` + +### Interface + +```yaml +name: HomeSec +version: "1.2.2" +slug: homesec +description: Self-hosted AI video security pipeline +url: https://github.com/lan17/homesec +arch: + - amd64 + - aarch64 +init: false # We use s6-overlay +homeassistant_api: true # Access HA API +hassio_api: true # Access Supervisor API +host_network: false +ingress: true +ingress_port: 8080 +ingress_stream: true +panel_icon: mdi:cctv +panel_title: HomeSec + +ports: + 8080/tcp: null # API (exposed via ingress) + +map: + - addon_config:rw # /config - HomeSec managed config + - media:rw # /media - Media storage + - share:rw # /share - Shared data + +schema: + config_path: str? + override_path: str? + log_level: list(debug|info|warning|error)? + database_url: str? # External DB (optional) + storage_type: list(local|dropbox)? + storage_path: str? + dropbox_token: password? + vlm_enabled: bool? + openai_api_key: password? + openai_model: str? + +options: + config_path: /config/homesec/config.yaml + override_path: /data/overrides.yaml + log_level: info + database_url: "" + storage_type: local + storage_path: /media/homesec/clips + vlm_enabled: false + +startup: services +stage: stable +advanced: true +privileged: [] +apparmor: true + +# Watchdog for auto-restart +watchdog: http://[HOST]:[PORT:8080]/api/v1/health +``` + +### Constraints + +- Must support both amd64 and aarch64 +- `homeassistant_api: true` injects SUPERVISOR_TOKEN +- Ingress provides secure access without port exposure +- Watchdog ensures auto-restart on failure + +--- + +## 3.3 Repository Manifest + +**File**: `repository.json` (at repo root) + +```json +{ + "name": "HomeSec", + "url": "https://github.com/lan17/homesec", + "maintainer": "lan17", + "addons": [ + { + "name": "HomeSec", + "slug": "homesec", + "description": "Self-hosted AI video security pipeline", + "url": "https://github.com/lan17/homesec", + "path": "homeassistant/addon/homesec" + } + ] +} +``` + +--- + +## 3.4 Dockerfile + +**File**: `homeassistant/addon/homesec/Dockerfile` + +### Interface + +```dockerfile +ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.8 +FROM ${BUILD_FROM} + +# Install: python3, ffmpeg, postgresql16, opencv, curl +# Install: homesec from PyPI +# Copy: rootfs (s6-overlay services) +# Set: HEALTHCHECK +``` + +### Constraints + +- Use `ghcr.io/hassio-addons/base` for s6-overlay support +- PostgreSQL 16 for bundled database +- ffmpeg for video processing +- opencv for YOLO filter (if using) +- Install homesec from PyPI (specific version) + +--- + +## 3.5 s6-overlay Services + +### Service Dependency Order + +``` +base → postgres-init (oneshot) → postgres (longrun) → homesec (longrun) +``` + +### postgres-init (oneshot) + +**File**: `rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up` + +Initializes PostgreSQL if not already initialized: +- Creates `/data/postgres/data` directory +- Runs `initdb` +- Creates `homesec` database +- Sets postgres password + +### postgres (longrun) + +**File**: `rootfs/etc/s6-overlay/s6-rc.d/postgres/run` + +Runs PostgreSQL in foreground: +```bash +exec su postgres -c "postgres -D /data/postgres/data" +``` + +### homesec (longrun) + +**File**: `rootfs/etc/s6-overlay/s6-rc.d/homesec/run` + +Waits for PostgreSQL, generates config if missing, runs HomeSec: +- Reads options from `/data/options.json` via Bashio +- Waits for `pg_isready` +- Generates initial config if not exists +- Runs `python3 -m homesec.cli run --config ... --config ...` + +### Constraints + +- Use Bashio for reading options and logging +- HomeSec must wait for PostgreSQL before starting +- Generate default config with home_assistant notifier pre-configured +- Use bundled PostgreSQL unless `database_url` option is set + +--- + +## 3.6 Ingress Configuration + +**File**: `rootfs/etc/nginx/includes/ingress.conf` + +```nginx +location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; +} +``` + +--- + +## 3.7 Default Generated Config + +When HomeSec starts for the first time, it generates: + +**File**: `/config/homesec/config.yaml` + +```yaml +version: 1 + +cameras: [] + +storage: + backend: local # or dropbox based on options + config: + path: /media/homesec/clips + +state_store: + dsn_env: DATABASE_URL + +notifiers: + - backend: home_assistant + config: {} # Uses SUPERVISOR_TOKEN automatically + +server: + enabled: true + host: 0.0.0.0 + port: 8080 +``` + +--- + +## File Changes Summary + +| File | Change | +|------|--------| +| `repository.json` | New file at repo root | +| `homeassistant/addon/README.md` | Add-on documentation | +| `homeassistant/addon/homesec/config.yaml` | Add-on manifest | +| `homeassistant/addon/homesec/Dockerfile` | Container build | +| `homeassistant/addon/homesec/build.yaml` | Build config | +| `homeassistant/addon/homesec/DOCS.md` | User documentation | +| `homeassistant/addon/homesec/CHANGELOG.md` | Version history | +| `homeassistant/addon/homesec/icon.png` | 512x512 icon | +| `homeassistant/addon/homesec/logo.png` | 256x256 logo | +| `homeassistant/addon/homesec/rootfs/...` | s6-overlay services | +| `homeassistant/addon/homesec/translations/en.yaml` | UI strings | + +--- + +## Test Expectations + +**Note**: Add-on testing requires running Home Assistant. Automated testing is limited. + +### Manual Test Cases + +**Installation** +- Given HA OS or Supervised, when add repo URL and install, then add-on appears in Supervisor +- Given add-on installed, when start, then PostgreSQL and HomeSec start successfully + +**PostgreSQL** +- Given fresh install, when add-on starts first time, then postgres-init creates database +- Given existing install, when add-on restarts, then postgres-init skips (already initialized) + +**Configuration** +- Given fresh install, when add-on starts, then default config generated at /config/homesec/config.yaml +- Given database_url option set, when add-on starts, then uses external database instead of bundled + +**Ingress** +- Given add-on running, when open from sidebar, then API accessible via ingress +- Given add-on running, when call /api/v1/health via ingress, then returns healthy + +**Events** +- Given add-on running, when clip triggers alert, then homesec_alert event appears in HA + +--- + +## Verification + +```bash +# Build add-on locally (requires Docker) +cd homeassistant/addon/homesec +docker build -t homesec-addon . + +# Test s6-overlay scripts syntax +shellcheck rootfs/etc/s6-overlay/s6-rc.d/*/run +shellcheck rootfs/etc/s6-overlay/s6-rc.d/*/up + +# Install in HA (manual) +# 1. Add repository: https://github.com/lan17/homesec +# 2. Install HomeSec add-on +# 3. Check logs in Supervisor +# 4. Open ingress panel +``` + +--- + +## Definition of Done + +- [ ] Add-on installs successfully from monorepo URL +- [ ] Bundled PostgreSQL starts and initializes correctly +- [ ] HomeSec waits for PostgreSQL before starting +- [ ] SUPERVISOR_TOKEN enables zero-config HA Events API +- [ ] Ingress provides access to API +- [ ] Configuration options (log_level, storage_type, etc.) work +- [ ] Watchdog restarts on failure +- [ ] Logs accessible in HA Supervisor +- [ ] Works on both amd64 and aarch64 +- [ ] Default config includes home_assistant notifier diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md new file mode 100644 index 00000000..3708f69f --- /dev/null +++ b/docs/ha-phase-4-ha-integration.md @@ -0,0 +1,514 @@ +# Phase 4: Native Home Assistant Integration + +**Goal**: Full UI-based configuration and deep entity integration in Home Assistant. + +**Estimated Effort**: 7-10 days + +**Dependencies**: Phase 1 (REST API), Phase 2 (HA Notifier) + +--- + +## Overview + +This phase creates a custom Home Assistant integration that: +- Auto-detects the HomeSec add-on +- Creates entities for cameras (sensors, binary sensors, switches) +- Creates hub-level entities (system health, alerts today, clips today) +- Subscribes to HomeSec events for real-time updates +- Provides services for camera management + +--- + +## 4.1 Integration Structure + +**Directory**: `homeassistant/integration/custom_components/homesec/` + +``` +homeassistant/integration/custom_components/homesec/ +├── __init__.py # Setup and entry points +├── manifest.json # Integration metadata +├── const.py # Constants +├── config_flow.py # UI configuration flow +├── coordinator.py # Data update coordinator +├── entity.py # Base entity classes +├── sensor.py # Sensor platform +├── binary_sensor.py # Binary sensor platform +├── switch.py # Switch platform +├── diagnostics.py # Diagnostic data +├── services.yaml # Service definitions +├── strings.json # UI strings +└── translations/ + └── en.json # English translations +``` + +--- + +## 4.2 Manifest + +**File**: `manifest.json` + +```json +{ + "domain": "homesec", + "name": "HomeSec", + "codeowners": ["@lan17"], + "config_flow": true, + "dependencies": [], + "single_config_entry": true, + "documentation": "https://github.com/lan17/homesec", + "integration_type": "hub", + "iot_class": "local_push", + "issue_tracker": "https://github.com/lan17/homesec/issues", + "requirements": ["aiohttp>=3.8.0"], + "version": "1.0.0" +} +``` + +### Constraints + +- `single_config_entry: true` - only one HomeSec instance per HA +- `iot_class: local_push` - we use HA Events for real-time updates +- `integration_type: hub` - we create sub-devices for cameras + +--- + +## 4.3 Constants + +**File**: `const.py` + +```python +DOMAIN = "homesec" + +# Config keys +CONF_HOST = "host" +CONF_PORT = "port" +CONF_API_KEY = "api_key" +CONF_VERIFY_SSL = "verify_ssl" + +# Defaults +DEFAULT_PORT = 8080 +DEFAULT_VERIFY_SSL = True +ADDON_HOSTNAME = "localhost" + +# Motion sensor +DEFAULT_MOTION_RESET_SECONDS = 30 + +# Platforms +PLATFORMS = ["binary_sensor", "sensor", "switch"] + +# Update intervals +SCAN_INTERVAL_SECONDS = 30 +``` + +--- + +## 4.4 Config Flow + +**File**: `config_flow.py` + +### Interface + +```python +class HomesecConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for HomeSec.""" + + async def async_step_user(self, user_input=None) -> FlowResult: + """Initial step - check for add-on first, then manual.""" + ... + + async def async_step_addon(self, user_input=None) -> FlowResult: + """Handle add-on auto-discovery confirmation.""" + ... + + async def async_step_manual(self, user_input=None) -> FlowResult: + """Handle manual setup for standalone HomeSec.""" + ... + + async def async_step_cameras(self, user_input=None) -> FlowResult: + """Handle camera selection step.""" + ... + + +class OptionsFlowHandler(OptionsFlow): + """Handle options flow for HomeSec.""" + + async def async_step_init(self, user_input=None) -> FlowResult: + """Manage options: cameras, scan_interval, motion_reset_seconds.""" + ... + + +async def validate_connection(hass, host, port, api_key=None, verify_ssl=True) -> dict: + """Validate connection to HomeSec API. + + Returns: {"title": str, "version": str, "cameras": list} + Raises: CannotConnect, InvalidAuth + """ + ... + +async def detect_addon(hass) -> bool: + """Check if HomeSec add-on is running at localhost:8080.""" + ... +``` + +### Flow Steps + +1. **user**: Check for add-on, redirect to addon or manual +2. **addon**: Confirm add-on connection (one-click setup) +3. **manual**: Enter host, port, API key +4. **cameras**: Select which cameras to add to HA + +### Constraints + +- Store `addon: bool` in config entry data to track mode +- Unique ID: `homesec_{host}_{port}` +- Options: cameras list, scan_interval, motion_reset_seconds + +--- + +## 4.5 Data Coordinator + +**File**: `coordinator.py` + +### Interface + +```python +class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator to manage HomeSec data updates.""" + + def __init__(self, hass, config_entry, host, port, api_key=None, verify_ssl=True): + ... + + @property + def base_url(self) -> str: ... + + @property + def config_version(self) -> int: + """Current config version for optimistic concurrency.""" + ... + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch data from HomeSec API. + + Returns: + { + "health": {...}, + "cameras": [...], + "config_version": int, + "stats": {...}, + "connected": bool, + "motion_active": {camera: bool}, # Event-driven + "latest_alerts": {camera: {...}}, # Event-driven + "recent_alerts": [{...}, ...], # Last 20 alerts + } + """ + ... + + async def async_subscribe_events(self) -> None: + """Subscribe to HomeSec events fired via HA Events API. + + Events: homesec_alert, homesec_camera_health, homesec_clip_recorded + """ + ... + + async def async_unsubscribe_events(self) -> None: + """Unsubscribe from HomeSec events.""" + ... + + # API Methods (use config_version for optimistic concurrency) + async def async_add_camera(self, name, source_backend, source_config) -> dict: ... + async def async_update_camera(self, camera_name, source_config=None) -> dict: ... + async def async_delete_camera(self, camera_name) -> None: ... + async def async_set_camera_enabled(self, camera_name, enabled: bool) -> dict: ... + async def async_test_camera(self, camera_name) -> dict: ... + + +class ConfigVersionConflict(Exception): + """Raised when config_version is stale (409 Conflict).""" + pass +``` + +### Motion Timer Logic + +When `homesec_alert` event received: +1. Set `motion_active[camera] = True` +2. Cancel any existing reset timer for this camera +3. Schedule new timer for `motion_reset_seconds` (default 30) +4. When timer fires, set `motion_active[camera] = False` + +### Constraints + +- Preserve `motion_active`, `latest_alerts`, `recent_alerts` across polling updates +- Store last 20 alerts in `recent_alerts` (newest first) +- Handle 503 gracefully (Postgres unavailable) +- Handle 409 by raising `ConfigVersionConflict` + +--- + +## 4.6 Entity Base Classes + +**File**: `entity.py` + +### Interface + +```python +class HomesecHubEntity(CoordinatorEntity[HomesecCoordinator]): + """Base class for HomeSec Hub entities (system-wide sensors).""" + + @property + def device_info(self) -> DeviceInfo: + """Return device info for the HomeSec Hub.""" + # identifiers: (DOMAIN, "homesec_hub") + # name: "HomeSec Hub" + # model: "AI Security Hub" + ... + + +class HomesecCameraEntity(CoordinatorEntity[HomesecCoordinator]): + """Base class for HomeSec camera entities.""" + + def __init__(self, coordinator, camera_name): ... + + @property + def device_info(self) -> DeviceInfo: + """Return device info for this camera.""" + # identifiers: (DOMAIN, camera_name) + # name: f"HomeSec {camera_name}" + # via_device: (DOMAIN, "homesec_hub") + ... + + def _get_camera_data(self) -> dict | None: + """Get camera data from coordinator.""" + ... + + def _is_motion_active(self) -> bool: + """Check if motion is currently active for this camera.""" + ... + + def _get_latest_alert(self) -> dict | None: + """Get the latest alert for this camera.""" + ... +``` + +### Device Hierarchy + +``` +HomeSec Hub (identifiers: homesec_hub) +├── HomeSec front_door (identifiers: front_door, via_device: homesec_hub) +├── HomeSec back_yard (identifiers: back_yard, via_device: homesec_hub) +└── ... +``` + +--- + +## 4.7 Entity Platforms + +### Sensors (sensor.py) + +**Hub Sensors** (HomesecHubEntity): +- `cameras_online` - Count of healthy cameras +- `alerts_today` - Alerts count today +- `clips_today` - Clips count today +- `system_health` - "healthy" / "degraded" / "unhealthy" +- `recent_alerts` - Special sensor with last 20 alerts in attributes + +**Camera Sensors** (HomesecCameraEntity): +- `last_activity` - Activity type from latest alert +- `risk_level` - Risk level from latest alert +- `health` - "healthy" / "unhealthy" + +### Binary Sensors (binary_sensor.py) + +**Camera Binary Sensors** (HomesecCameraEntity): +- `motion` - Motion detected (auto-resets after timeout) +- `person` - Person detected (based on latest alert activity_type) +- `online` - Camera connectivity + +### Switches (switch.py) + +**Camera Switches** (HomesecCameraEntity): +- `enabled` - Enable/disable camera (calls API, requires restart) + +--- + +## 4.8 Recent Alerts Sensor + +**Special sensor for dashboard filtering.** + +```python +class HomesecRecentAlertsSensor(HomesecHubEntity, SensorEntity): + """Sensor showing recent alerts with full metadata for filtering.""" + + @property + def native_value(self) -> int: + """Return count of alerts today.""" + ... + + @property + def extra_state_attributes(self) -> dict: + """Return recent alerts as attributes for dashboard filtering. + + Returns: + { + "alerts": [...], # List of alert dicts + "alerts_count": int, + "last_alert_at": str | None, + "by_risk_level": {"low": 2, "medium": 1, ...}, + "by_camera": {"front_door": 2, ...}, + "by_activity_type": {"person_at_door": 1, ...}, + } + """ + ... +``` + +### Dashboard Usage + +```yaml +# Example Lovelace template +{% for alert in state_attr('sensor.homesec_recent_alerts', 'alerts') %} + {% if alert.risk_level in ['high', 'critical'] %} + - Camera: {{ alert.camera }} + Activity: {{ alert.activity_type }} + Time: {{ alert.timestamp }} + {% endif %} +{% endfor %} +``` + +--- + +## 4.9 Services + +**File**: `services.yaml` + +```yaml +add_camera: + name: Add Camera + description: Add a new camera to HomeSec + fields: + name: + required: true + selector: + text: + source_backend: + required: true + selector: + select: + options: ["rtsp", "ftp", "local_folder"] + rtsp_url: + selector: + text: + +remove_camera: + name: Remove Camera + description: Remove a camera from HomeSec + target: + device: + integration: homesec +``` + +**Note**: Service handlers to be implemented in `__init__.py`. + +--- + +## 4.10 Translations + +**File**: `translations/en.json` + +Provides translations for: +- Config flow steps (user, addon, manual, cameras) +- Error messages (cannot_connect, invalid_auth) +- Options flow +- Entity names +- Service names + +--- + +## File Changes Summary + +| File | Change | +|------|--------| +| `homeassistant/integration/hacs.json` | HACS configuration | +| `custom_components/homesec/__init__.py` | Setup entry, services | +| `custom_components/homesec/manifest.json` | Integration metadata | +| `custom_components/homesec/const.py` | Constants | +| `custom_components/homesec/config_flow.py` | Config + options flow | +| `custom_components/homesec/coordinator.py` | Data coordinator | +| `custom_components/homesec/entity.py` | Base entity classes | +| `custom_components/homesec/sensor.py` | Sensor platform | +| `custom_components/homesec/binary_sensor.py` | Binary sensor platform | +| `custom_components/homesec/switch.py` | Switch platform | +| `custom_components/homesec/diagnostics.py` | Diagnostic data | +| `custom_components/homesec/services.yaml` | Service definitions | +| `custom_components/homesec/strings.json` | UI strings | +| `custom_components/homesec/translations/en.json` | English translations | + +--- + +## Test Expectations + +### Fixtures Needed + +- `mock_homesec_api` - Mocked aiohttp responses for HomeSec API +- `mock_coordinator` - HomesecCoordinator with canned data +- `sample_camera_data` - Camera response from API +- `sample_alert_event` - homesec_alert event data + +### Test Cases + +**Config Flow** +- Given add-on running at localhost:8080, when start config flow, then addon step shown +- Given no add-on, when start config flow, then manual step shown +- Given invalid API key, when submit manual step, then invalid_auth error +- Given valid connection, when complete flow, then config entry created + +**Coordinator** +- Given healthy API, when update, then data contains health, cameras, stats +- Given 503 from API, when update, then UpdateFailed raised with message +- Given homesec_alert event, when received, then motion_active set and timer scheduled +- Given motion timer expires, when callback runs, then motion_active cleared + +**Entities** +- Given camera "front" online, when check binary_sensor.front_online, then is_on=True +- Given alert for "front", when check sensor.front_last_activity, then shows activity_type +- Given switch turned off, when toggle, then API called with enabled=False + +**Services** +- Given add_camera service called, when valid data, then API POST /cameras called + +--- + +## Verification + +```bash +# Copy integration to HA config +cp -r homeassistant/integration/custom_components/homesec ~/.homeassistant/custom_components/ + +# Restart HA and check logs +# Add integration via UI: Settings > Devices & Services > Add Integration > HomeSec + +# Verify entities created +# Check Developer Tools > States for homesec.* entities + +# Test motion sensor +# Trigger alert and verify binary_sensor.{camera}_motion turns on then off +``` + +--- + +## Definition of Done + +- [ ] Config flow auto-detects add-on and offers one-click setup +- [ ] Config flow falls back to manual for standalone +- [ ] All entity platforms create entities correctly +- [ ] Hub device created with system-wide sensors +- [ ] Camera devices created with via_device to hub +- [ ] Recent Alerts sensor stores last 20 alerts with filter metadata +- [ ] DataUpdateCoordinator fetches at correct intervals +- [ ] HA Events subscription triggers refresh on alerts +- [ ] Motion sensor auto-resets after configurable timeout +- [ ] Camera switch enables/disables via API +- [ ] Config version tracking prevents conflicts (409 handling) +- [ ] Services work (add_camera, remove_camera) +- [ ] Options flow allows reconfiguration +- [ ] All strings translatable +- [ ] HACS installation works +- [ ] Error handling shows user-friendly messages diff --git a/docs/ha-phase-5-advanced.md b/docs/ha-phase-5-advanced.md new file mode 100644 index 00000000..9bdce7ca --- /dev/null +++ b/docs/ha-phase-5-advanced.md @@ -0,0 +1,118 @@ +# Phase 5: Advanced Features + +**Goal**: Premium features for power users. + +**Estimated Effort**: 5-7 days + +**Dependencies**: Phase 4 (HA Integration) + +--- + +## Overview + +This phase adds optional advanced features: +- Custom Lovelace card for camera view with detection overlays +- Event timeline panel +- Snapshot/thumbnail support + +**Note**: This phase is optional and can be deferred or skipped. + +--- + +## 5.1 Custom Lovelace Card + +**File**: `homeassistant/integration/custom_components/homesec/www/homesec-camera-card.js` + +### Interface + +A custom card that displays: +- Camera snapshot/stream +- Motion and person detection badges +- Latest activity info and risk level +- Link to clip viewer + +### Usage + +```yaml +type: custom:homesec-camera-card +entity: camera.front_door # or sensor.front_door_last_activity +``` + +### Constraints + +- Must register with `customElements.define` +- Must implement `setConfig()` and `set hass()` +- Should update when related entities change +- Card size: 4 (standard camera card size) + +--- + +## 5.2 Event Timeline Panel (Optional) + +Custom panel for viewing event history with timeline visualization. + +### Features + +- Timeline view of alerts +- Filter by camera, risk level, date range +- Click to view clip +- Integration with HA's history + +### Constraints + +- Requires additional API endpoint or uses existing `/api/v1/events` +- May require frontend build tooling (Lit, etc.) + +--- + +## 5.3 Snapshot Support (Optional) + +Add snapshot/thumbnail entities for each camera. + +### Features + +- `image.{camera}_snapshot` - Latest snapshot +- `image.{camera}_last_alert` - Thumbnail from last alert +- Periodic snapshot refresh + +### Constraints + +- Requires API endpoint to serve snapshots +- May need to store thumbnails during clip processing +- Consider storage implications + +--- + +## File Changes Summary + +| File | Change | +|------|--------| +| `custom_components/homesec/www/homesec-camera-card.js` | Lovelace card | +| `custom_components/homesec/www/homesec-timeline.js` | Timeline panel (optional) | +| `custom_components/homesec/image.py` | Image platform (optional) | + +--- + +## Test Expectations + +### Manual Test Cases + +**Lovelace Card** +- Given card configured with camera entity, when view dashboard, then camera image displayed +- Given motion active, when view card, then motion badge highlighted +- Given new alert, when view card, then activity and risk info updated + +**Timeline (if implemented)** +- Given events exist, when open timeline, then events displayed chronologically +- Given filter by camera, when applied, then only that camera's events shown + +--- + +## Definition of Done + +- [ ] Custom Lovelace card renders camera with overlays +- [ ] Card updates when entities change +- [ ] Card shows detection badges (motion, person) +- [ ] Card displays latest activity info +- [ ] (Optional) Timeline panel shows event history +- [ ] (Optional) Snapshot images work diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index be5b4744..deae1114 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -1,55 +1,42 @@ -# HomeSec + Home Assistant: Detailed Implementation Plan +# HomeSec + Home Assistant: Implementation Plan -This document provides a comprehensive, step-by-step implementation plan for integrating HomeSec with Home Assistant. Each phase includes specific file changes, code examples, testing strategies, and acceptance criteria. +This document provides an overview of the Home Assistant integration for HomeSec. Detailed implementation for each phase is in separate documents. --- ## Decision Snapshot (2026-02-01) -- Chosen approach: Add-on + native integration with HomeSec as the runtime. -- Required: runtime add/remove cameras and other config changes from HA. -- API stack: FastAPI, async endpoints only, async SQLAlchemy only. -- Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA can trigger restart. -- Config storage: **Override YAML file** is source of truth for dynamic config. Base YAML is bootstrap-only. -- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists replace. -- Single instance: HA integration assumes one HomeSec instance (`single_config_entry`). -- Secrets: never stored in HomeSec; config stores env var names; HA/add-on passes env vars at boot. -- Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). -- Tests: Given/When/Then comments required for all new tests. -- P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). -- **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically; standalone users provide HA URL + token. -- **No MQTT required**: MQTT Discovery is optional for users who prefer it; primary path uses HA Events API. -- **409 Conflict UX**: Show error to user when config version is stale. -- **API during Postgres outage**: Return 503 Service Unavailable. -- **Camera ping**: RTSP ping implementation should include TODO for real connectivity test (not part of this integration work). -- **Delete clip**: Deletes from both local storage and cloud storage (existing pattern in cleanup_clips.py). +- **Approach**: Add-on + native integration with HomeSec as the runtime +- **API stack**: FastAPI, async endpoints only, async SQLAlchemy only +- **Config storage**: Override YAML file is source of truth for dynamic config. Base YAML is bootstrap-only. +- **Config merge**: Multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge (recursive), lists merge (union). +- **Single instance**: HA integration assumes one HomeSec instance (`single_config_entry`) +- **Secrets**: Never stored in HomeSec config; only env var names are persisted +- **Repository pattern**: API reads/writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access) +- **Tests**: Given/When/Then comments required for all new tests +- **P0 priority**: Recording + uploading must keep working even if Postgres is down +- **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically +- **No MQTT required**: Integration uses HA Events API directly. Existing MQTT notifier remains for Node-RED/other systems +- **409 Conflict UX**: Show error to user when config version is stale +- **API during Postgres outage**: Return 503 Service Unavailable +- **Restart acceptable**: API writes validated config to disk and returns `restart_required`; HA can trigger restart -## Prerequisites (Changes to Core HomeSec) - -Before implementing the HA integration, these changes are needed in the core HomeSec codebase: - -1. **Add `enabled` field to CameraConfig** (`src/homesec/models/config.py`): - ```python - class CameraConfig(BaseModel): - name: str - enabled: bool = True # NEW: Allow disabling camera via API - source: CameraSourceConfig - ``` +--- -2. **Add camera health monitoring**: The Application needs to periodically check camera health and call `notifier.publish_camera_health()` when status changes. +## Execution Order -3. **Add stats methods to ClipRepository**: - - `count_clips_since(since: datetime) -> int` - - `count_alerts_since(since: datetime) -> int` +| Phase | Document | Dependencies | Estimated Effort | +|-------|----------|--------------|------------------| +| 0 | [Prerequisites](./ha-phase-0-prerequisites.md) | None | 2-3 days | +| 1 | [REST API](./ha-phase-1-rest-api.md) | Phase 0 | 5-7 days | +| 2 | [HA Notifier](./ha-phase-2-ha-notifier.md) | None | 2-3 days | +| 3 | [Add-on](./ha-phase-3-addon.md) | Phase 1, 2 | 3-4 days | +| 4 | [HA Integration](./ha-phase-4-ha-integration.md) | Phase 1, 2 | 7-10 days | +| 5 | [Advanced Features](./ha-phase-5-advanced.md) | Phase 4 | 5-7 days | -## Execution Order (Option A) +**Total: 24-34 days** -1. Phase 2: REST API for Configuration (control plane) -2. Phase 2.5: Home Assistant Notifier Plugin (real-time events) -3. Phase 4: Native Home Assistant Integration -4. Phase 3: Home Assistant Add-on -5. Phase 1: MQTT Discovery Enhancement (optional, for users who prefer MQTT) -6. Phase 5: Advanced Features +**Parallel work possible**: Phase 1 and Phase 2 can be done in parallel. --- @@ -59,3478 +46,84 @@ All code lives in the main `homesec` monorepo: ``` homesec/ -├── repository.json # HA Add-on repo manifest (must be at root) +├── repository.json # HA Add-on repo manifest (at repo root) ├── src/homesec/ # Main Python package (PyPI) -│ ├── api/ # NEW: REST API +│ ├── api/ # Phase 1: REST API │ │ ├── __init__.py │ │ ├── server.py │ │ ├── dependencies.py │ │ └── routes/ -│ │ ├── __init__.py -│ │ ├── health.py -│ │ ├── config.py -│ │ ├── cameras.py -│ │ ├── clips.py -│ │ ├── events.py -│ │ ├── stats.py -│ │ └── system.py -│ ├── config/ # NEW: Config management +│ ├── config/ # Phase 1: Config management │ │ ├── __init__.py │ │ ├── loader.py │ │ └── manager.py │ ├── plugins/ │ │ └── notifiers/ -│ │ ├── home_assistant.py # NEW: HA Events API notifier -│ │ └── mqtt_discovery.py # NEW: MQTT discovery (optional) +│ │ └── home_assistant.py # Phase 2: HA Events API notifier │ └── ...existing code... │ -├── homeassistant/ # ALL HA-specific code -│ ├── README.md -│ │ -│ ├── integration/ # Custom component (HACS) -│ │ ├── hacs.json -│ │ └── custom_components/ -│ │ └── homesec/ -│ │ ├── __init__.py -│ │ ├── manifest.json -│ │ ├── const.py -│ │ ├── config_flow.py -│ │ ├── coordinator.py -│ │ ├── entity.py -│ │ ├── binary_sensor.py -│ │ ├── sensor.py -│ │ ├── switch.py -│ │ ├── diagnostics.py -│ │ ├── services.yaml -│ │ ├── strings.json -│ │ └── translations/ -│ │ └── en.json -│ │ -│ └── addon/ # Add-on (HA Supervisor) -│ ├── README.md +├── homeassistant/ # All HA-specific code +│ ├── integration/ # Phase 4: Custom component (HACS) +│ │ └── custom_components/homesec/ +│ └── addon/ # Phase 3: Add-on (HA Supervisor) │ └── homesec/ -│ ├── config.yaml -│ ├── build.yaml -│ ├── Dockerfile -│ ├── DOCS.md -│ ├── CHANGELOG.md -│ ├── icon.png -│ ├── logo.png -│ ├── rootfs/ -│ │ └── etc/ -│ │ ├── s6-overlay/ -│ │ │ └── s6-rc.d/ -│ │ │ ├── postgres-init/ -│ │ │ ├── postgres/ -│ │ │ ├── homesec/ -│ │ │ └── user/ -│ │ └── nginx/ -│ │ └── includes/ -│ │ └── ingress.conf -│ └── translations/ -│ └── en.yaml -│ -├── tests/ -│ ├── unit/ -│ │ ├── api/ -│ │ │ ├── test_routes_cameras.py -│ │ │ ├── test_routes_clips.py -│ │ │ └── ... -│ │ └── plugins/ -│ │ └── notifiers/ -│ │ ├── test_home_assistant.py -│ │ └── test_mqtt_discovery.py -│ └── integration/ -│ └── test_ha_integration.py │ -├── docs/ -└── pyproject.toml # Add fastapi, uvicorn deps +└── tests/ + ├── unit/ + │ ├── api/ + │ └── plugins/notifiers/ + └── integration/ ``` **Distribution:** - **PyPI**: `src/homesec/` published as `homesec` package - **HACS**: Users point to `homeassistant/integration/` -- **Add-on Store**: Users add repo URL `https://github.com/lan17/homesec` (repository.json at repo root points to `homeassistant/addon/homesec/`) - ---- - -## Table of Contents - -1. [Decision Snapshot](#decision-snapshot-2026-02-01) -2. [Execution Order](#execution-order-option-a) -3. [Phase 2: REST API for Configuration](#phase-2-rest-api-for-configuration) -4. [Phase 2.5: Home Assistant Notifier Plugin](#phase-25-home-assistant-notifier-plugin) -5. [Phase 4: Native Home Assistant Integration](#phase-4-native-home-assistant-integration) -6. [Phase 3: Home Assistant Add-on](#phase-3-home-assistant-add-on) -7. [Phase 1: MQTT Discovery Enhancement (Optional)](#phase-1-mqtt-discovery-enhancement-optional) -8. [Phase 5: Advanced Features](#phase-5-advanced-features) -9. [Testing Strategy](#testing-strategy) -10. [Migration Guide](#migration-guide) +- **Add-on Store**: Users add repo URL (repository.json at root points to `homeassistant/addon/homesec/`) --- -## Phase 1: MQTT Discovery Enhancement (Optional - Future) - -> **⚠️ OUT OF SCOPE FOR V1**: This phase is optional and should be implemented only after the core integration (Phases 2-4) is complete and stable. The primary integration path uses the HA Events API which requires no MQTT broker. - -**Goal:** Auto-create Home Assistant entities from HomeSec without requiring the native integration. This is for users who prefer MQTT over the primary HA Events API approach. - -**Estimated Effort:** 2-3 days - -**Warning:** If both MQTT Discovery AND the native integration are enabled, you will have duplicate entities. Users should choose one approach, not both. - -### 1.1 Configuration Model Updates - -**File:** `src/homesec/models/config.py` - -Add MQTT discovery configuration to the existing `MQTTConfig`: - -```python -class MQTTDiscoveryConfig(BaseModel): - """Configuration for Home Assistant MQTT Discovery.""" - - enabled: bool = False - prefix: str = "homeassistant" # HA discovery prefix - node_id: str = "homesec" # Unique node identifier - device_name: str = "HomeSec" # Display name in HA - device_manufacturer: str = "HomeSec" - device_model: str = "AI Security Pipeline" - # Republish discovery on HA restart - subscribe_to_birth: bool = True - birth_topic: str = "homeassistant/status" - - -class MQTTConfig(BaseModel): - """Extended MQTT configuration.""" - - host: str - port: int = 1883 - auth: MQTTAuthConfig | None = None - topic_template: str = "homecam/alerts/{camera_name}" - qos: int = 1 - retain: bool = False - # New: Discovery settings - discovery: MQTTDiscoveryConfig = MQTTDiscoveryConfig() -``` - -### 1.2 Discovery Message Builder - -**New File:** `src/homesec/plugins/notifiers/mqtt_discovery.py` - -```python -"""MQTT Discovery message builder for Home Assistant.""" - -from __future__ import annotations - -import json -from dataclasses import dataclass -from typing import Any - -from homesec.models.config import MQTTDiscoveryConfig - - -@dataclass -class DiscoveryEntity: - """Represents a single HA entity discovery config.""" - - component: str # sensor, binary_sensor, camera, etc. - object_id: str # Unique ID within component - config: dict[str, Any] # Discovery payload - - -class MQTTDiscoveryBuilder: - """Builds MQTT discovery messages for Home Assistant.""" - - def __init__(self, config: MQTTDiscoveryConfig, version: str): - self.config = config - self.version = version - - def _device_info(self, camera_name: str) -> dict[str, Any]: - """Generate device info block for grouping entities.""" - return { - "identifiers": [f"{self.config.node_id}_{camera_name}"], - "name": f"{self.config.device_name} {camera_name}", - "manufacturer": self.config.device_manufacturer, - "model": self.config.device_model, - "sw_version": self.version, - "via_device": f"{self.config.node_id}_hub", - } - - def _hub_device_info(self) -> dict[str, Any]: - """Generate device info for the HomeSec hub.""" - return { - "identifiers": [f"{self.config.node_id}_hub"], - "name": self.config.device_name, - "manufacturer": self.config.device_manufacturer, - "model": f"{self.config.device_model} Hub", - "sw_version": self.version, - } - - def build_camera_entities(self, camera_name: str) -> list[DiscoveryEntity]: - """Build all discovery entities for a single camera.""" - entities = [] - base_topic = f"homesec/{camera_name}" - - # 1. Binary Sensor: Motion Detected - entities.append(DiscoveryEntity( - component="binary_sensor", - object_id=f"{camera_name}_motion", - config={ - "name": "Motion", - "unique_id": f"homesec_{camera_name}_motion", - "device_class": "motion", - "state_topic": f"{base_topic}/motion", - "payload_on": "ON", - "payload_off": "OFF", - "device": self._device_info(camera_name), - } - )) - - # 2. Binary Sensor: Person Detected - entities.append(DiscoveryEntity( - component="binary_sensor", - object_id=f"{camera_name}_person", - config={ - "name": "Person Detected", - "unique_id": f"homesec_{camera_name}_person", - "device_class": "occupancy", - "state_topic": f"{base_topic}/person", - "payload_on": "ON", - "payload_off": "OFF", - "device": self._device_info(camera_name), - } - )) - - # 3. Sensor: Last Activity Type - entities.append(DiscoveryEntity( - component="sensor", - object_id=f"{camera_name}_activity", - config={ - "name": "Last Activity", - "unique_id": f"homesec_{camera_name}_activity", - "state_topic": f"{base_topic}/activity", - "value_template": "{{ value_json.activity_type }}", - "json_attributes_topic": f"{base_topic}/activity", - "json_attributes_template": "{{ value_json | tojson }}", - "icon": "mdi:motion-sensor", - "device": self._device_info(camera_name), - } - )) - - # 4. Sensor: Risk Level - entities.append(DiscoveryEntity( - component="sensor", - object_id=f"{camera_name}_risk", - config={ - "name": "Risk Level", - "unique_id": f"homesec_{camera_name}_risk", - "state_topic": f"{base_topic}/risk", - "icon": "mdi:shield-alert", - "device": self._device_info(camera_name), - } - )) - - # 5. Sensor: Camera Health - entities.append(DiscoveryEntity( - component="sensor", - object_id=f"{camera_name}_health", - config={ - "name": "Health", - "unique_id": f"homesec_{camera_name}_health", - "state_topic": f"{base_topic}/health", - "icon": "mdi:heart-pulse", - "device": self._device_info(camera_name), - } - )) - - # 6. Sensor: Last Clip URL - entities.append(DiscoveryEntity( - component="sensor", - object_id=f"{camera_name}_last_clip", - config={ - "name": "Last Clip", - "unique_id": f"homesec_{camera_name}_last_clip", - "state_topic": f"{base_topic}/clip", - "value_template": "{{ value_json.view_url }}", - "json_attributes_topic": f"{base_topic}/clip", - "icon": "mdi:filmstrip", - "device": self._device_info(camera_name), - } - )) - - # 7. Image: Last Snapshot - entities.append(DiscoveryEntity( - component="image", - object_id=f"{camera_name}_snapshot", - config={ - "name": "Last Snapshot", - "unique_id": f"homesec_{camera_name}_snapshot", - "image_topic": f"{base_topic}/snapshot", - "device": self._device_info(camera_name), - } - )) - - # 8. Device Trigger: Alert - entities.append(DiscoveryEntity( - component="device_automation", - object_id=f"{camera_name}_alert_trigger", - config={ - "automation_type": "trigger", - "type": "alert", - "subtype": "security_alert", - "topic": f"{base_topic}/alert", - "device": self._device_info(camera_name), - } - )) - - return entities - - def build_hub_entities(self) -> list[DiscoveryEntity]: - """Build discovery entities for the HomeSec hub.""" - entities = [] - base_topic = "homesec/hub" - - # Hub Health - entities.append(DiscoveryEntity( - component="sensor", - object_id="hub_status", - config={ - "name": "Status", - "unique_id": "homesec_hub_status", - "state_topic": f"{base_topic}/status", - "icon": "mdi:server", - "device": self._hub_device_info(), - } - )) - - # Total Clips Today - entities.append(DiscoveryEntity( - component="sensor", - object_id="hub_clips_today", - config={ - "name": "Clips Today", - "unique_id": "homesec_hub_clips_today", - "state_topic": f"{base_topic}/stats", - "value_template": "{{ value_json.clips_today }}", - "icon": "mdi:filmstrip-box-multiple", - "device": self._hub_device_info(), - } - )) - - # Total Alerts Today - entities.append(DiscoveryEntity( - component="sensor", - object_id="hub_alerts_today", - config={ - "name": "Alerts Today", - "unique_id": "homesec_hub_alerts_today", - "state_topic": f"{base_topic}/stats", - "value_template": "{{ value_json.alerts_today }}", - "icon": "mdi:bell-alert", - "device": self._hub_device_info(), - } - )) - - return entities - - def get_discovery_topic(self, entity: DiscoveryEntity) -> str: - """Get the MQTT topic for publishing discovery config.""" - return f"{self.config.prefix}/{entity.component}/{self.config.node_id}/{entity.object_id}/config" - - def get_discovery_payload(self, entity: DiscoveryEntity) -> str: - """Get the JSON payload for discovery config.""" - return json.dumps(entity.config) -``` - -### 1.3 Enhanced MQTT Notifier - -**File:** `src/homesec/plugins/notifiers/mqtt.py` - -Extend the existing `paho-mqtt` notifier (do not switch libraries). Key changes: - -- Keep the `Notifier.send()` interface and use `asyncio.to_thread` for publish operations. -- Add discovery publish helpers using `MQTTDiscoveryBuilder` (new file). -- Store camera names in-memory to publish discovery per camera. -- On HA birth message, republish discovery and current state topics. -- Keep backward-compatible alert topic publishing. - -Implementation notes: - -- Use `MQTTConfig.auth.username_env` and `MQTTConfig.auth.password_env` for credentials. -- Avoid blocking `loop_start`/`loop_stop` in the event loop (wrap in `asyncio.to_thread`). - -### 1.4 Application Integration - -**File:** `src/homesec/app.py` - -Update the application to register cameras with the MQTT notifier. Prefer a small -`DiscoveryNotifier` Protocol (or `isinstance` check) over `hasattr` on private methods. - -```python -# In Application._create_components(), after sources are created: - -# Register cameras with discovery-capable notifier(s) -for entry in self._notifier_entries: - notifier = entry.notifier - if isinstance(notifier, DiscoveryNotifier): - for source in self._sources: - await notifier.register_camera(source.camera_name) +## Key Interfaces - # Publish initial discovery - await notifier.publish_discovery() -``` - -### 1.5 Configuration Example - -**File:** `config/example.yaml` (add section) - -```yaml -notifiers: - - backend: mqtt - config: - host: localhost - port: 1883 - auth: - username_env: MQTT_USERNAME - password_env: MQTT_PASSWORD - topic_template: "homecam/alerts/{camera_name}" - qos: 1 - retain: false - discovery: - enabled: true - prefix: homeassistant - node_id: homesec - device_name: HomeSec - subscribe_to_birth: true - birth_topic: homeassistant/status -``` - -### 1.6 Testing - -**New File:** `tests/unit/plugins/notifiers/test_mqtt_discovery.py` - -```python -"""Tests for MQTT Discovery functionality.""" - -import pytest -import json - -from homesec.models.config import MQTTDiscoveryConfig -from homesec.plugins.notifiers.mqtt_discovery import MQTTDiscoveryBuilder - - -class TestMQTTDiscoveryBuilder: - """Tests for MQTTDiscoveryBuilder.""" - - @pytest.fixture - def builder(self): - config = MQTTDiscoveryConfig(enabled=True) - return MQTTDiscoveryBuilder(config, "1.2.3") - - def test_build_camera_entities_creates_all_types(self, builder): - """Should create all expected entity types for a camera.""" - entities = builder.build_camera_entities("front_door") - - components = {e.component for e in entities} - assert "binary_sensor" in components - assert "sensor" in components - assert "image" in components - assert "device_automation" in components - - def test_discovery_topic_format(self, builder): - """Should generate correct discovery topic.""" - entities = builder.build_camera_entities("front_door") - motion_entity = next(e for e in entities if "motion" in e.object_id) +These interfaces are defined across phases. See individual phase docs for details. - topic = builder.get_discovery_topic(motion_entity) - assert topic == "homeassistant/binary_sensor/homesec/front_door_motion/config" +### ConfigManager (Phase 1) +- `get_config() -> Config` +- `update_camera(...) -> ConfigUpdateResult` +- `add_camera(...) -> ConfigUpdateResult` +- `remove_camera(...) -> ConfigUpdateResult` +- `config_version: int` (optimistic concurrency) - def test_device_info_grouping(self, builder): - """Should group entities under same device.""" - entities = builder.build_camera_entities("front_door") +### ClipRepository Extensions (Phase 1) +- `get_clip(clip_id) -> Clip | None` +- `list_clips(...) -> tuple[list[Clip], int]` +- `list_events(...) -> tuple[list[Event], int]` +- `delete_clip(clip_id) -> None` +- `count_clips_since(since: datetime) -> int` +- `count_alerts_since(since: datetime) -> int` - device_ids = { - e.config["device"]["identifiers"][0] - for e in entities - if "device" in e.config - } - assert len(device_ids) == 1 - assert "homesec_front_door" in device_ids.pop() +### Notifier Extensions (Phase 0) +- `publish_camera_health(camera_name: str, healthy: bool) -> None` - def test_hub_entities(self, builder): - """Should create hub-level entities.""" - entities = builder.build_hub_entities() - - assert len(entities) >= 2 - assert any("status" in e.object_id for e in entities) - assert any("clips_today" in e.object_id for e in entities) -``` - -### 1.7 Acceptance Criteria - -- [ ] `mqtt.discovery.enabled: true` causes discovery messages to be published -- [ ] All cameras appear as devices in Home Assistant -- [ ] Binary sensors (motion, person) work correctly -- [ ] Sensors (activity, risk, health) update on alerts -- [ ] Device triggers fire for automations -- [ ] Discovery republishes when HA restarts (birth message) -- [ ] Backwards compatible with existing MQTT alert topic +### ClipSource Extensions (Phase 0) +- `enabled: bool` property (respects CameraConfig.enabled) --- -## Phase 2: REST API for Configuration - -**Goal:** Enable remote configuration and monitoring of HomeSec via HTTP API. - -**Estimated Effort:** 5-7 days - -### 2.0 Control Plane Requirements - -- FastAPI only; all endpoints are `async def`. -- Use async SQLAlchemy only for DB access (no sync engines or blocking DB calls). -- No blocking operations inside endpoints; use `asyncio.to_thread` for file I/O and restarts. -- API must not write directly to `StateStore`/`EventStore`. Add read methods on `ClipRepository`. -- Config is loaded from **multiple YAML files** (left → right). Rightmost wins. -- Merge semantics: dicts deep-merge; lists replace. -- Config updates are validated with Pydantic, **written to the override YAML only**, and return `restart_required: true`. -- API provides a restart endpoint to request a graceful shutdown. -- Server config: introduce `FastAPIServerConfig` (host/port, enabled, api_key_env, CORS, health path) to replace `HealthConfig`. -- Secrets are never stored in config; only env var names are persisted. - -### 2.1 API Framework Setup - -**New File:** `src/homesec/api/__init__.py` - -```python -"""HomeSec REST API package.""" - -from .server import create_app, APIServer - -__all__ = ["create_app", "APIServer"] -``` - -**New File:** `src/homesec/api/server.py` - -```python -"""REST API server for HomeSec.""" - -from __future__ import annotations - -import logging -from contextlib import asynccontextmanager -from typing import TYPE_CHECKING - -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -from .routes import cameras, clips, config, events, health - -if TYPE_CHECKING: - from homesec.app import Application - -logger = logging.getLogger(__name__) - - -def create_app(app_instance: Application) -> FastAPI: - """Create the FastAPI application.""" - - @asynccontextmanager - async def lifespan(app: FastAPI): - # Store reference to Application - app.state.homesec = app_instance - yield - - app = FastAPI( - title="HomeSec API", - description="REST API for HomeSec video security pipeline", - version="1.0.0", - lifespan=lifespan, - ) - - # CORS for Home Assistant frontend - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Configure appropriately - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - - # Register routes - app.include_router(health.router, prefix="/api/v1", tags=["health"]) - app.include_router(config.router, prefix="/api/v1", tags=["config"]) - app.include_router(cameras.router, prefix="/api/v1", tags=["cameras"]) - app.include_router(clips.router, prefix="/api/v1", tags=["clips"]) - app.include_router(events.router, prefix="/api/v1", tags=["events"]) - # MQTT is used for event push (no WebSocket in v1) - - return app - - -class APIServer: - """Manages the API server lifecycle.""" - - def __init__(self, app: FastAPI, host: str = "0.0.0.0", port: int = 8080): - self.app = app - self.host = host - self.port = port - self._server = None - - async def start(self) -> None: - """Start the API server.""" - import uvicorn - - config = uvicorn.Config( - self.app, - host=self.host, - port=self.port, - log_level="info", - ) - self._server = uvicorn.Server(config) - await self._server.serve() - - async def stop(self) -> None: - """Stop the API server.""" - if self._server: - self._server.should_exit = True -``` - -**Port coordination:** Replace the aiohttp `HealthServer` with FastAPI and make -the port configurable via `FastAPIServerConfig` (default 8080). Provide both -`/health` and `/api/v1/health` for compatibility. - -### 2.2 Config Persistence + Restart - -**New File:** `src/homesec/config/manager.py` - -Responsibilities: - -- Load and validate config from **multiple YAML files** (left → right). Rightmost wins. -- Base YAML is bootstrap-only; **override YAML** contains all HA-managed config. -- Merge semantics: dicts deep-merge; lists replace. -- Persist updated override YAML atomically (write temp file, fsync, rename). -- Store `config_version` in the override file and enforce optimistic concurrency. -- Return `restart_required: true` for any config-changing endpoints. -- Expose methods: - - `get_config() -> Config` - - `update_config(new_config: dict) -> ConfigUpdateResult` - - `update_camera(...) -> ConfigUpdateResult` - - `remove_camera(...) -> ConfigUpdateResult` -- Use `asyncio.to_thread` for file I/O to keep endpoints non-blocking. -- Provide `dump_override(path: Path)` to export backup YAML. - - `ConfigUpdateResult` should include the new `config_version`. -- Application should expose `config_store` and `config_version` for API routes. -- Override file is machine-owned; comment preservation is not required. -- Application should load configs via ConfigManager with multiple `--config` paths. - -CLI requirements: - -- Support multiple `--config` flags (order matters). -- Default override path: `config/ha-overrides.yaml` (override can be passed as the last `--config`). - -**Repository extensions:** - -- Add read APIs to `ClipRepository`: - - `get_clip(clip_id)` - - `list_clips(...)` - - `list_events(...)` - - `delete_clip(clip_id)` (mark deleted + emit event) -- Implement with async SQLAlchemy in `PostgresStateStore` / `PostgresEventStore`. - -**New Endpoint:** `POST /api/v1/system/restart` - -- Triggers graceful shutdown (`Application.request_shutdown()`). -- HA can call this after config update, or restart the add-on. - -### 2.3 API Routes - -**New File:** `src/homesec/api/routes/cameras.py` - -```python -"""Camera management API routes.""" - -from __future__ import annotations - -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException, status -from pydantic import BaseModel - -from ..dependencies import get_homesec_app - -router = APIRouter(prefix="/cameras") - -# Note: All config-mutating endpoints return restart_required=True and do not -# attempt hot-reload. HA may call /api/v1/system/restart or restart the add-on. -# All config-mutating endpoints require config_version for optimistic concurrency. - - -class CameraCreate(BaseModel): - """Request model for creating a camera.""" - name: str - source_backend: str # rtsp, ftp, local_folder - source_config: dict # Backend-specific configuration - config_version: int - - -class CameraUpdate(BaseModel): - """Request model for updating a camera.""" - source_config: dict | None = None - config_version: int - - -class CameraResponse(BaseModel): - """Response model for camera.""" - name: str - source_backend: str - healthy: bool - last_heartbeat: float | None - source_config: dict - - -class CameraListResponse(BaseModel): - """Response model for camera list.""" - cameras: list[CameraResponse] - total: int - - -class ConfigChangeResponse(BaseModel): - """Response model for config changes.""" - restart_required: bool = True - config_version: int - camera: CameraResponse | None = None - - -@router.get("", response_model=CameraListResponse) -async def list_cameras(app=Depends(get_homesec_app)): - """List all configured cameras.""" - cameras = [] - config = app.config_store.get_config() - for camera in config.cameras: - source = app.get_source(camera.name) - cameras.append(CameraResponse( - name=camera.name, - source_backend=camera.source.backend, - healthy=source.is_healthy() if source else False, - last_heartbeat=source.last_heartbeat() if source else None, - source_config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), - )) - return CameraListResponse(cameras=cameras, total=len(cameras)) - - -@router.get("/{camera_name}", response_model=CameraResponse) -async def get_camera(camera_name: str, app=Depends(get_homesec_app)): - """Get a specific camera's configuration.""" - config = app.config_store.get_config() - camera = next((c for c in config.cameras if c.name == camera_name), None) - if not camera: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Camera '{camera_name}' not found", - ) - source = app.get_source(camera_name) - return CameraResponse( - name=camera.name, - source_backend=camera.source.backend, - healthy=source.is_healthy() if source else False, - last_heartbeat=source.last_heartbeat() if source else None, - source_config=camera.source.config if isinstance(camera.source.config, dict) else camera.source.config.model_dump(), - ) - - -@router.post("", response_model=ConfigChangeResponse, status_code=status.HTTP_201_CREATED) -async def create_camera(camera: CameraCreate, app=Depends(get_homesec_app)): - """Add a new camera.""" - if app.get_source(camera.name): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Camera '{camera.name}' already exists", - ) - - try: - result = await app.config_store.add_camera( - name=camera.name, - source_backend=camera.source_backend, - source_config=camera.source_config, - config_version=camera.config_version, - ) - return ConfigChangeResponse( - restart_required=True, - config_version=result.config_version, - camera=CameraResponse( - name=camera.name, - source_backend=camera.source_backend, - healthy=False, - last_heartbeat=None, - source_config=camera.source_config, - ), - ) - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=str(e), - ) - - -@router.put("/{camera_name}", response_model=ConfigChangeResponse) -async def update_camera( - camera_name: str, - update: CameraUpdate, - app=Depends(get_homesec_app), -): - """Update a camera's source configuration.""" - source = app.get_source(camera_name) - if not source: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Camera '{camera_name}' not found", - ) - - result = await app.config_store.update_camera( - camera_name=camera_name, - source_config=update.source_config, - config_version=update.config_version, - ) - - return ConfigChangeResponse( - restart_required=True, - config_version=result.config_version, - camera=result.camera, - ) - - -@router.delete("/{camera_name}", response_model=ConfigChangeResponse) -async def delete_camera( - camera_name: str, - config_version: int, - app=Depends(get_homesec_app), -): - """Remove a camera.""" - source = app.get_source(camera_name) - if not source: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Camera '{camera_name}' not found", - ) - - result = await app.config_store.remove_camera( - camera_name=camera_name, - config_version=config_version, - ) - return ConfigChangeResponse( - restart_required=True, - config_version=result.config_version, - camera=None, - ) - - -@router.get("/{camera_name}/status") -async def get_camera_status(camera_name: str, app=Depends(get_homesec_app)): - """Get detailed camera status.""" - source = app.get_source(camera_name) - if not source: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Camera '{camera_name}' not found", - ) - - return { - "name": camera_name, - "healthy": source.is_healthy(), - "last_heartbeat": source.last_heartbeat(), - "last_heartbeat_age_s": source.last_heartbeat_age(), - "stats": await source.get_stats(), - } - - -@router.post("/{camera_name}/test") -async def test_camera(camera_name: str, app=Depends(get_homesec_app)): - """Test camera connection.""" - source = app.get_source(camera_name) - if not source: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Camera '{camera_name}' not found", - ) - - try: - result = await source.ping() - return {"success": result, "message": "Connection successful" if result else "Connection failed"} - except Exception as e: - return {"success": False, "message": str(e)} -``` - -**New File:** `src/homesec/api/routes/clips.py` - -```python -"""Clip management API routes.""" - -from __future__ import annotations - -from datetime import datetime -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException, Query, status -from pydantic import BaseModel - -from ..dependencies import get_homesec_app - -router = APIRouter(prefix="/clips") - - -class ClipResponse(BaseModel): - """Response model for a clip.""" - clip_id: str - camera_name: str - status: str - created_at: datetime - storage_uri: str | None - view_url: str | None - filter_result: dict | None - analysis_result: dict | None - alert_decision: dict | None - - -class ClipListResponse(BaseModel): - """Response model for clip list.""" - clips: list[ClipResponse] - total: int - page: int - page_size: int - - -@router.get("", response_model=ClipListResponse) -async def list_clips( - app=Depends(get_homesec_app), - camera: str | None = None, - status: str | None = None, - since: datetime | None = None, - until: datetime | None = None, - page: int = Query(1, ge=1), - page_size: int = Query(50, ge=1, le=100), -): - """List clips with optional filtering.""" - clips, total = await app.repository.list_clips( - camera=camera, - status=status, - since=since, - until=until, - offset=(page - 1) * page_size, - limit=page_size, - ) - - return ClipListResponse( - clips=[ClipResponse(**c.model_dump()) for c in clips], - total=total, - page=page, - page_size=page_size, - ) - - -@router.get("/{clip_id}", response_model=ClipResponse) -async def get_clip(clip_id: str, app=Depends(get_homesec_app)): - """Get a specific clip.""" - clip = await app.repository.get_clip(clip_id) - if not clip: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Clip '{clip_id}' not found", - ) - return ClipResponse(**clip.model_dump()) - - -@router.delete("/{clip_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_clip(clip_id: str, app=Depends(get_homesec_app)): - """Delete a clip.""" - clip = await app.repository.get_clip(clip_id) - if not clip: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Clip '{clip_id}' not found", - ) - await app.repository.delete_clip(clip_id) - - -@router.post("/{clip_id}/reprocess", status_code=status.HTTP_202_ACCEPTED) -async def reprocess_clip(clip_id: str, app=Depends(get_homesec_app)): - """Reprocess a clip through the pipeline.""" - clip = await app.repository.get_clip(clip_id) - if not clip: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Clip '{clip_id}' not found", - ) - - await app.pipeline.reprocess(clip_id) - return {"message": "Clip queued for reprocessing", "clip_id": clip_id} -``` - -**New File:** `src/homesec/api/routes/events.py` - -```python -"""Event history API routes.""" - -from __future__ import annotations - -from datetime import datetime - -from fastapi import APIRouter, Depends, Query -from pydantic import BaseModel - -from ..dependencies import get_homesec_app - -router = APIRouter(prefix="/events") - - -class EventResponse(BaseModel): - """Response model for an event.""" - id: int - clip_id: str - event_type: str - timestamp: datetime - event_data: dict - - -class EventListResponse(BaseModel): - """Response model for event list.""" - events: list[EventResponse] - total: int - - -@router.get("", response_model=EventListResponse) -async def list_events( - app=Depends(get_homesec_app), - clip_id: str | None = None, - event_type: str | None = None, - camera: str | None = None, - since: datetime | None = None, - until: datetime | None = None, - limit: int = Query(100, ge=1, le=1000), -): - """List events with optional filtering.""" - events, total = await app.repository.list_events( - clip_id=clip_id, - event_type=event_type, - camera=camera, - since=since, - until=until, - limit=limit, - ) - - return EventListResponse( - events=[EventResponse(**e.model_dump()) for e in events], - total=total, - ) -``` - -**New File:** `src/homesec/api/routes/stats.py` - -```python -"""Statistics API routes.""" - -from __future__ import annotations - -from datetime import date, datetime, timedelta - -from fastapi import APIRouter, Depends -from pydantic import BaseModel - -from ..dependencies import get_homesec_app - -router = APIRouter(prefix="/stats") - - -class StatsResponse(BaseModel): - """Response model for statistics.""" - clips_today: int - clips_total: int - alerts_today: int - alerts_total: int - cameras_online: int - cameras_total: int - last_alert_at: datetime | None - last_clip_at: datetime | None - - -@router.get("", response_model=StatsResponse) -async def get_stats(app=Depends(get_homesec_app)): - """Get system-wide statistics.""" - today = date.today() - today_start = datetime.combine(today, datetime.min.time()) - - # Get clip counts - _, clips_today = await app.repository.list_clips(since=today_start, limit=0) - _, clips_total = await app.repository.list_clips(limit=0) - - # Get alert counts (notifications sent) - _, alerts_today = await app.repository.list_events( - event_type="notification_sent", - since=today_start, - limit=0, - ) - _, alerts_total = await app.repository.list_events( - event_type="notification_sent", - limit=0, - ) - - # Get camera health - cameras = app.get_all_sources() - cameras_online = sum(1 for c in cameras if c.is_healthy()) - - # Get last timestamps - recent_clips, _ = await app.repository.list_clips(limit=1) - recent_alerts, _ = await app.repository.list_events( - event_type="notification_sent", - limit=1, - ) - - return StatsResponse( - clips_today=clips_today, - clips_total=clips_total, - alerts_today=alerts_today, - alerts_total=alerts_total, - cameras_online=cameras_online, - cameras_total=len(cameras), - last_alert_at=recent_alerts[0].timestamp if recent_alerts else None, - last_clip_at=recent_clips[0].created_at if recent_clips else None, - ) -``` - -**New File:** `src/homesec/api/routes/health.py` - -```python -"""Health check API routes.""" - -from __future__ import annotations - -from fastapi import APIRouter, Depends, Response, status -from pydantic import BaseModel - -from ..dependencies import get_homesec_app - -router = APIRouter() - - -class SourceHealth(BaseModel): - """Health status for a single source.""" - name: str - healthy: bool - last_heartbeat: float | None - last_heartbeat_age_s: float | None - - -class HealthResponse(BaseModel): - """Response model for health check.""" - status: str # "healthy", "degraded", "unhealthy" - version: str - uptime_s: float - sources: list[SourceHealth] - postgres_connected: bool - - -@router.get("/health", response_model=HealthResponse) -@router.get("/api/v1/health", response_model=HealthResponse) -async def health_check( - response: Response, - app=Depends(get_homesec_app), -): - """Health check endpoint.""" - sources = [] - all_healthy = True - any_healthy = False - - for source in app.get_all_sources(): - healthy = source.is_healthy() - if healthy: - any_healthy = True - else: - all_healthy = False - - sources.append(SourceHealth( - name=source.camera_name, - healthy=healthy, - last_heartbeat=source.last_heartbeat(), - last_heartbeat_age_s=source.last_heartbeat_age(), - )) - - # Check Postgres connectivity - postgres_connected = await app.repository.ping() - - # Determine overall status - if all_healthy and postgres_connected: - health_status = "healthy" - elif any_healthy: - health_status = "degraded" - else: - health_status = "unhealthy" - response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE - - return HealthResponse( - status=health_status, - version=app.version, - uptime_s=app.uptime_seconds(), - sources=sources, - postgres_connected=postgres_connected, - ) -``` - -Real-time updates use HA Events API; no WebSocket endpoint in v1. - -### 2.4 API Configuration - -**File:** `src/homesec/models/config.py` (add) - -```python -class FastAPIServerConfig(BaseModel): - """Configuration for the FastAPI server.""" - - enabled: bool = True - host: str = "0.0.0.0" - port: int = 8080 - cors_origins: list[str] = ["*"] - # Authentication (optional) - auth_enabled: bool = False - api_key_env: str | None = None - # Health endpoints - health_path: str = "/health" - api_health_path: str = "/api/v1/health" - -# In Config: -# - Replace `health: HealthConfig` with `server: FastAPIServerConfig` -``` - -### 2.5 OpenAPI Documentation +## Migration Guide -The FastAPI app automatically generates OpenAPI docs at `/api/v1/docs` (Swagger UI) and `/api/v1/redoc` (ReDoc). +### From Standalone to Add-on -### 2.6 Acceptance Criteria +1. Export current `config.yaml` +2. Install HomeSec add-on +3. Copy config to `/config/homesec/config.yaml` +4. Create `/data/overrides.yaml` for HA-managed config +5. Update database URL if using external Postgres +6. Start add-on -- [ ] All CRUD operations for cameras work -- [ ] Config changes are validated, persisted, and return `restart_required: true` -- [ ] Stale `config_version` updates return 409 Conflict -- [ ] Restart endpoint triggers graceful shutdown -- [ ] Clip listing with filtering works -- [ ] Event history API works -- [ ] OpenAPI documentation is accurate -- [ ] CORS works for Home Assistant frontend -- [ ] API authentication (optional) works -- [ ] Returns 503 when Postgres is unavailable +### From MQTT Notifier to Native Integration ---- - -## Phase 2.5: Home Assistant Notifier Plugin - -**Goal:** Enable real-time event push from HomeSec to Home Assistant without requiring MQTT. - -**Estimated Effort:** 2-3 days - -### 2.5.1 Configuration Model - -**File:** `src/homesec/models/config.py` - -```python -class HomeAssistantNotifierConfig(BaseModel): - """Configuration for Home Assistant notifier.""" - - # For standalone mode (not running as add-on) - # When running as HA add-on, SUPERVISOR_TOKEN is used automatically - url_env: str | None = None # e.g., "HA_URL" -> http://homeassistant.local:8123 - token_env: str | None = None # e.g., "HA_TOKEN" -> long-lived access token - - # Event configuration - event_prefix: str = "homesec" # Events will be homesec_alert, homesec_health, etc. -``` - -### 2.5.2 Home Assistant Notifier Implementation - -**New File:** `src/homesec/plugins/notifiers/home_assistant.py` - -```python -"""Home Assistant notifier - pushes events directly to HA Events API.""" - -from __future__ import annotations - -import logging -import os -from typing import TYPE_CHECKING - -import aiohttp - -from homesec.interfaces import Notifier -from homesec.models.config import HomeAssistantNotifierConfig -from homesec.plugins.registry import plugin, PluginType - -if TYPE_CHECKING: - from homesec.models.alert import Alert - -logger = logging.getLogger(__name__) - - -@plugin(plugin_type=PluginType.NOTIFIER, name="home_assistant") -class HomeAssistantNotifier(Notifier): - """Push events directly to Home Assistant via Events API.""" - - config_cls = HomeAssistantNotifierConfig - - def __init__(self, config: HomeAssistantNotifierConfig): - self.config = config - self._session: aiohttp.ClientSession | None = None - self._supervisor_mode = False - - async def start(self) -> None: - """Initialize the HTTP session and detect supervisor mode.""" - self._session = aiohttp.ClientSession() - - # Detect if running as HA add-on (SUPERVISOR_TOKEN is injected automatically) - if os.environ.get("SUPERVISOR_TOKEN"): - self._supervisor_mode = True - logger.info("HomeAssistantNotifier: Running in supervisor mode (zero-config)") - else: - # Standalone mode - validate config - if not self.config.url_env or not self.config.token_env: - raise ValueError( - "HomeAssistantNotifier requires url_env and token_env in standalone mode" - ) - logger.info("HomeAssistantNotifier: Running in standalone mode") - - async def stop(self) -> None: - """Close the HTTP session.""" - if self._session: - await self._session.close() - self._session = None - - def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: - """Get the URL and headers for the HA Events API.""" - if self._supervisor_mode: - url = f"http://supervisor/core/api/events/{self.config.event_prefix}_{event_type}" - headers = { - "Authorization": f"Bearer {os.environ['SUPERVISOR_TOKEN']}", - "Content-Type": "application/json", - } - else: - from homesec.config import resolve_env_var - base_url = resolve_env_var(self.config.url_env) - token = resolve_env_var(self.config.token_env) - url = f"{base_url}/api/events/{self.config.event_prefix}_{event_type}" - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } - return url, headers - - async def notify(self, alert: Alert) -> None: - """Send alert to Home Assistant as an event.""" - if not self._session: - raise RuntimeError("HomeAssistantNotifier not started") - - url, headers = self._get_url_and_headers("alert") - - event_data = { - "camera": alert.camera_name, - "clip_id": alert.clip_id, - "activity_type": alert.activity_type, - "risk_level": alert.risk_level.value if alert.risk_level else None, - "summary": alert.summary, - "view_url": alert.view_url, - "storage_uri": alert.storage_uri, - "timestamp": alert.ts.isoformat(), - } - - # Add analysis details if present - if alert.analysis: - event_data["detected_objects"] = alert.analysis.detected_objects - event_data["analysis"] = alert.analysis.model_dump(mode="json") - - try: - async with self._session.post(url, json=event_data, headers=headers) as resp: - if resp.status >= 400: - body = await resp.text() - logger.error( - "Failed to send event to HA: %s %s - %s", - resp.status, - resp.reason, - body, - ) - else: - logger.debug("Sent homesec_alert event to HA for clip %s", alert.clip_id) - except aiohttp.ClientError as exc: - logger.error("Failed to connect to Home Assistant: %s", exc) - # Don't raise - notifications are best-effort - - async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: - """Publish camera health status to HA.""" - if not self._session: - return - - url, headers = self._get_url_and_headers("camera_health") - event_data = { - "camera": camera_name, - "healthy": healthy, - "status": "healthy" if healthy else "unhealthy", - } - - try: - async with self._session.post(url, json=event_data, headers=headers) as resp: - if resp.status >= 400: - logger.warning("Failed to send camera health to HA: %s", resp.status) - except aiohttp.ClientError: - pass # Best effort - - async def publish_clip_recorded(self, clip_id: str, camera_name: str) -> None: - """Publish clip recorded event to HA.""" - if not self._session: - return - - url, headers = self._get_url_and_headers("clip_recorded") - event_data = { - "clip_id": clip_id, - "camera": camera_name, - } - - try: - async with self._session.post(url, json=event_data, headers=headers) as resp: - if resp.status >= 400: - logger.warning("Failed to send clip_recorded to HA: %s", resp.status) - except aiohttp.ClientError: - pass # Best effort -``` - -### 2.5.3 Configuration Example - -**File:** `config/example.yaml` (add section) - -```yaml -notifiers: - # Home Assistant notifier (recommended for HA users) - - backend: home_assistant - config: - # When running as HA add-on, no configuration needed (uses SUPERVISOR_TOKEN) - # For standalone mode, provide HA URL and token: - # url_env: HA_URL # http://homeassistant.local:8123 - # token_env: HA_TOKEN # Long-lived access token from HA -``` - -### 2.5.4 Testing - -**New File:** `tests/unit/plugins/notifiers/test_home_assistant.py` - -```python -"""Tests for Home Assistant notifier.""" - -import os -from unittest.mock import AsyncMock, patch - -import pytest -from aiohttp import ClientResponseError - -from homesec.models.config import HomeAssistantNotifierConfig -from homesec.plugins.notifiers.home_assistant import HomeAssistantNotifier - - -class TestHomeAssistantNotifier: - """Tests for HomeAssistantNotifier.""" - - @pytest.fixture - def config(self): - return HomeAssistantNotifierConfig( - url_env="HA_URL", - token_env="HA_TOKEN", - ) - - @pytest.fixture - def notifier(self, config): - return HomeAssistantNotifier(config) - - async def test_supervisor_mode_detection(self, notifier): - # Given: SUPERVISOR_TOKEN is set - with patch.dict(os.environ, {"SUPERVISOR_TOKEN": "test_token"}): - # When: notifier starts - await notifier.start() - - # Then: supervisor mode is enabled - assert notifier._supervisor_mode is True - - await notifier.stop() - - async def test_standalone_mode_requires_config(self, config): - # Given: config without url_env - config.url_env = None - - # When: notifier starts without SUPERVISOR_TOKEN - notifier = HomeAssistantNotifier(config) - with patch.dict(os.environ, {}, clear=True): - # Then: ValueError is raised - with pytest.raises(ValueError, match="url_env and token_env"): - await notifier.start() - - async def test_notify_sends_event(self, notifier, alert_fixture): - # Given: notifier is started in standalone mode - with patch.dict(os.environ, {"HA_URL": "http://ha:8123", "HA_TOKEN": "token"}): - await notifier.start() - - # When: notify is called - with patch.object(notifier._session, "post", new_callable=AsyncMock) as mock_post: - mock_post.return_value.__aenter__.return_value.status = 200 - await notifier.notify(alert_fixture) - - # Then: event is posted to HA - mock_post.assert_called_once() - call_args = mock_post.call_args - assert "homesec_alert" in call_args[0][0] - - await notifier.stop() -``` - -### 2.5.5 Acceptance Criteria - -- [ ] Notifier auto-detects supervisor mode via `SUPERVISOR_TOKEN` -- [ ] Zero-config when running as HA add-on -- [ ] Standalone mode requires `url_env` and `token_env` -- [ ] Events are fired: `homesec_alert`, `homesec_camera_health`, `homesec_clip_recorded` -- [ ] Notification failures don't crash the pipeline (best-effort) -- [ ] Events contain all required metadata for HA integration - ---- - -## Phase 3: Home Assistant Add-on - -**Goal:** Provide one-click installation for Home Assistant OS/Supervised users. - -**Estimated Effort:** 3-4 days - -### 3.1 Add-on Repository Structure - -**Location:** `homeassistant/addon/` in the main homesec monorepo. - -Users add the add-on via: `https://github.com/lan17/homesec` - -Note: `repository.json` must be at the repo root (not in `homeassistant/addon/`) for Home Assistant to discover it. The file points to `homeassistant/addon/homesec/` as the add-on location. - -``` -homeassistant/addon/ -├── README.md -└── homesec/ - ├── config.yaml # Add-on manifest - ├── Dockerfile # Container build - ├── build.yaml # Build configuration - ├── DOCS.md # Documentation - ├── CHANGELOG.md # Version history - ├── icon.png # Add-on icon (512x512) - ├── logo.png # Add-on logo (256x256) - ├── rootfs/ # s6-overlay services - │ └── etc/ - │ ├── s6-overlay/ - │ └── nginx/ - └── translations/ - └── en.yaml # UI strings -``` - -### 3.2 Add-on Manifest - -**File:** `homeassistant/addon/homesec/config.yaml` - -Note: Update to current Home Assistant add-on schema: - -- Use `addon_config` mapping instead of `config:rw` where possible. -- Read runtime options from `/data/options.json` via Bashio. -- Keep secrets out of the generated config (env vars only). - -```yaml -name: HomeSec -version: "1.2.2" -slug: homesec -description: Self-hosted AI video security pipeline -url: https://github.com/lan17/homesec -arch: - - amd64 - - aarch64 -init: false -homeassistant_api: true -hassio_api: true -host_network: false -ingress: true -ingress_port: 8080 -ingress_stream: true -panel_icon: mdi:cctv -panel_title: HomeSec - -# Port mappings -ports: - 8080/tcp: null # API (exposed via ingress) - -# Volume mappings -map: - - addon_config:rw # /config - HomeSec managed config - - media:rw # /media - Media storage - - share:rw # /share - Shared data - -# Services -services: - - mqtt:want # Optional - only needed if user enables MQTT Discovery - -# Options schema -schema: - config_path: str? - override_path: str? - log_level: list(debug|info|warning|error)? - # Database - database_url: str? - # Storage - storage_type: list(local|dropbox)? - storage_path: str? - dropbox_token: password? - # VLM - vlm_enabled: bool? - openai_api_key: password? - # VLM model - openai_model: str? - # MQTT Discovery (optional - primary path uses HA Events API) - mqtt_discovery: bool? - -# Default options -options: - config_path: /config/homesec/config.yaml - override_path: /data/overrides.yaml - log_level: info - database_url: "" - storage_type: local - storage_path: /media/homesec/clips - vlm_enabled: false - mqtt_discovery: true - -# Startup dependencies -startup: services -stage: stable - -# Advanced options -advanced: true -privileged: [] -apparmor: true - -# Watchdog for auto-restart -watchdog: http://[HOST]:[PORT:8080]/api/v1/health -``` - -### 3.3 Add-on Dockerfile - -**File:** `homeassistant/addon/homesec/Dockerfile` - -```dockerfile -# syntax=docker/dockerfile:1 -ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.8 -FROM ${BUILD_FROM} - -# Install runtime dependencies including PostgreSQL -RUN apk add --no-cache \ - python3 \ - py3-pip \ - ffmpeg \ - postgresql16 \ - postgresql16-contrib \ - opencv \ - curl \ - && rm -rf /var/cache/apk/* - -# Create PostgreSQL directories -RUN mkdir -p /run/postgresql /data/postgres \ - && chown -R postgres:postgres /run/postgresql /data/postgres - -# Install HomeSec -ARG HOMESEC_VERSION=1.2.2 -RUN pip3 install --no-cache-dir homesec==${HOMESEC_VERSION} - -# Copy root filesystem (s6-overlay services) -COPY rootfs / - -# Set working directory -WORKDIR /app - -# Labels -LABEL \ - io.hass.name="HomeSec" \ - io.hass.description="Self-hosted AI video security pipeline" \ - io.hass.version="${HOMESEC_VERSION}" \ - io.hass.type="addon" \ - io.hass.arch="amd64|aarch64" - -# Healthcheck -HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ - CMD curl -f http://localhost:8080/api/v1/health || exit 1 -``` - -### 3.4 s6-overlay Service Structure - -The add-on uses s6-overlay to run PostgreSQL and HomeSec as two services with proper dependency ordering. - -**Directory structure:** - -``` -rootfs/etc/s6-overlay/s6-rc.d/ -├── postgres/ -│ ├── type # Contains: longrun -│ ├── run # PostgreSQL startup script -│ └── dependencies.d/ -│ └── base # Depends on base setup -├── postgres-init/ -│ ├── type # Contains: oneshot -│ ├── up # Initialize DB if needed -│ └── dependencies.d/ -│ └── base -├── homesec/ -│ ├── type # Contains: longrun -│ ├── run # HomeSec startup script -│ └── dependencies.d/ -│ └── postgres # Waits for PostgreSQL -└── user/ - └── contents.d/ - ├── postgres-init - ├── postgres - └── homesec -``` - -### 3.5 PostgreSQL Initialization Service - -**File:** `homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up` - -```bash -#!/command/with-contenv bashio - -# Skip if already initialized -if [[ -f /data/postgres/data/PG_VERSION ]]; then - bashio::log.info "PostgreSQL already initialized" - exit 0 -fi - -bashio::log.info "Initializing PostgreSQL database..." - -# Create data directory -mkdir -p /data/postgres/data -chown -R postgres:postgres /data/postgres - -# Initialize database cluster -su postgres -c "initdb -D /data/postgres/data --encoding=UTF8 --locale=C" - -# Configure PostgreSQL for local connections only -cat >> /data/postgres/data/postgresql.conf << EOF -listen_addresses = 'localhost' -max_connections = 20 -shared_buffers = 128MB -EOF - -# Start PostgreSQL temporarily to create database -su postgres -c "pg_ctl -D /data/postgres/data start -w -o '-c listen_addresses=localhost'" - -# Create homesec database and user -su postgres -c "createdb homesec" -su postgres -c "psql -c \"ALTER USER postgres PASSWORD 'homesec';\"" - -# Stop PostgreSQL (will be started by longrun service) -su postgres -c "pg_ctl -D /data/postgres/data stop -w" - -bashio::log.info "PostgreSQL initialization complete" -``` - -### 3.6 PostgreSQL Service - -**File:** `homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres/run` - -```bash -#!/command/with-contenv bashio - -bashio::log.info "Starting PostgreSQL..." - -# Run PostgreSQL in foreground -exec su postgres -c "postgres -D /data/postgres/data" -``` - -### 3.7 HomeSec Service - -**File:** `homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/run` - -```bash -#!/command/with-contenv bashio -# ============================================================================== -# HomeSec Service - runs after PostgreSQL is ready -# ============================================================================== - -# Read add-on options -CONFIG_PATH=$(bashio::config 'config_path') -OVERRIDE_PATH=$(bashio::config 'override_path') -LOG_LEVEL=$(bashio::config 'log_level') -EXTERNAL_DB_URL=$(bashio::config 'database_url') -STORAGE_TYPE=$(bashio::config 'storage_type') -STORAGE_PATH=$(bashio::config 'storage_path') -MQTT_DISCOVERY=$(bashio::config 'mqtt_discovery') - -# Wait for PostgreSQL to be ready (if using bundled) -if [[ -z "${EXTERNAL_DB_URL}" ]]; then - bashio::log.info "Waiting for bundled PostgreSQL..." - until pg_isready -h localhost -U postgres -q; do - sleep 1 - done - export DATABASE_URL="postgresql://postgres:homesec@localhost/homesec" - bashio::log.info "Bundled PostgreSQL is ready" -else - export DATABASE_URL="${EXTERNAL_DB_URL}" - bashio::log.info "Using external database: ${EXTERNAL_DB_URL%%@*}@..." -fi - -# Get MQTT credentials from HA if MQTT Discovery enabled -if [[ "${MQTT_DISCOVERY}" == "true" ]] && bashio::services.available "mqtt"; then - MQTT_HOST=$(bashio::services mqtt "host") - MQTT_PORT=$(bashio::services mqtt "port") - MQTT_USER=$(bashio::services mqtt "username") - MQTT_PASS=$(bashio::services mqtt "password") - export MQTT_HOST MQTT_PORT MQTT_USER MQTT_PASS - bashio::log.info "MQTT Discovery enabled via ${MQTT_HOST}:${MQTT_PORT}" -fi - -# Create directories -mkdir -p "$(dirname "${CONFIG_PATH}")" -mkdir -p "$(dirname "${OVERRIDE_PATH}")" -mkdir -p "${STORAGE_PATH}" - -# Generate base config if it doesn't exist -if [[ ! -f "${CONFIG_PATH}" ]]; then - bashio::log.info "Generating initial configuration at ${CONFIG_PATH}" - cat > "${CONFIG_PATH}" << EOF -version: 1 - -cameras: [] - -storage: - backend: ${STORAGE_TYPE} - config: - path: ${STORAGE_PATH} - -state_store: - dsn_env: DATABASE_URL - -notifiers: - # Primary: Push events to HA via Events API (uses SUPERVISOR_TOKEN automatically) - - backend: home_assistant - config: {} - -server: - enabled: true - host: 0.0.0.0 - port: 8080 -EOF - - # Optionally add MQTT notifier if user enabled MQTT Discovery - if [[ "${MQTT_DISCOVERY}" == "true" ]] && bashio::services.available "mqtt"; then - bashio::log.info "Adding MQTT Discovery notifier to config" - cat >> "${CONFIG_PATH}" << EOF - - # Optional: MQTT Discovery for users who prefer MQTT entities - - backend: mqtt - config: - host_env: MQTT_HOST - port_env: MQTT_PORT - auth: - username_env: MQTT_USER - password_env: MQTT_PASS - discovery: - enabled: true -EOF - fi -fi - -# Create override file if missing -if [[ ! -f "${OVERRIDE_PATH}" ]]; then - bashio::log.info "Creating override file at ${OVERRIDE_PATH}" - cat > "${OVERRIDE_PATH}" << EOF -version: 1 -config_version: 1 -EOF -fi - -bashio::log.info "Starting HomeSec..." - -# Run HomeSec with both config files (base + overrides) -exec python3 -m homesec.cli run \ - --config "${CONFIG_PATH}" \ - --config "${OVERRIDE_PATH}" \ - --log-level "${LOG_LEVEL}" -``` - -### 3.8 Ingress Configuration - -**File:** `homeassistant/addon/homesec/rootfs/etc/nginx/includes/ingress.conf` - -```nginx -# Proxy to HomeSec API -location / { - proxy_pass http://127.0.0.1:8080; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Timeouts for long-running connections - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; -} -``` - -### 3.9 Acceptance Criteria - -- [ ] Add-on installs successfully from monorepo URL -- [ ] Bundled PostgreSQL starts and initializes correctly -- [ ] HomeSec waits for PostgreSQL before starting -- [ ] SUPERVISOR_TOKEN enables zero-config HA Events API -- [ ] Ingress provides access to API/UI -- [ ] Configuration options work correctly -- [ ] Watchdog restarts on failure -- [ ] Logs are accessible in HA -- [ ] Works on both amd64 and aarch64 -- [ ] Optional: MQTT Discovery works if user enables it - ---- - -## Phase 4: Native Home Assistant Integration - -**Goal:** Full UI-based configuration and deep entity integration in Home Assistant. - -**Estimated Effort:** 7-10 days - -### 4.1 Integration Structure - -**Directory:** `homeassistant/integration/custom_components/homesec/` - -``` -homeassistant/integration/custom_components/homesec/ -├── __init__.py # Setup and entry points -├── manifest.json # Integration metadata -├── const.py # Constants -├── config_flow.py # UI configuration flow -├── coordinator.py # Data update coordinator -├── entity.py # Base entity class -├── sensor.py # Sensor platform -├── binary_sensor.py # Binary sensor platform -├── switch.py # Switch platform -├── select.py # Select platform -├── image.py # Image platform -├── diagnostics.py # Diagnostic data -├── services.yaml # Service definitions -├── strings.json # UI strings -└── translations/ - └── en.json # English translations -``` - -### 4.2 Manifest - -**File:** `homeassistant/integration/custom_components/homesec/manifest.json` - -```json -{ - "domain": "homesec", - "name": "HomeSec", - "codeowners": ["@lan17"], - "config_flow": true, - "dependencies": [], - "single_config_entry": true, - "documentation": "https://github.com/lan17/homesec", - "integration_type": "hub", - "iot_class": "local_push", - "issue_tracker": "https://github.com/lan17/homesec/issues", - "requirements": ["aiohttp>=3.8.0"], - "version": "1.0.0" -} -``` - -### 4.3 Constants - -**File:** `homeassistant/integration/custom_components/homesec/const.py` - -```python -"""Constants for HomeSec integration.""" - -from typing import Final - -DOMAIN: Final = "homesec" - -# Configuration keys -CONF_HOST: Final = "host" -CONF_PORT: Final = "port" -CONF_API_KEY: Final = "api_key" -CONF_VERIFY_SSL: Final = "verify_ssl" - -# Default values -DEFAULT_PORT: Final = 8080 -DEFAULT_VERIFY_SSL: Final = True -ADDON_HOSTNAME: Final = "localhost" # Add-on runs on same host as HA - -# Motion sensor -DEFAULT_MOTION_RESET_SECONDS: Final = 30 - -# Platforms (v1 - core functionality only) -# TODO v2: Add "camera", "image", "select" platforms -PLATFORMS: Final = [ - "binary_sensor", - "sensor", - "switch", -] - -# Entity categories -DIAGNOSTIC_SENSORS: Final = ["health", "last_heartbeat"] - -# Update intervals -SCAN_INTERVAL_SECONDS: Final = 30 - -# Attributes -ATTR_CLIP_ID: Final = "clip_id" -ATTR_CLIP_URL: Final = "clip_url" -ATTR_SNAPSHOT_URL: Final = "snapshot_url" -ATTR_ACTIVITY_TYPE: Final = "activity_type" -ATTR_RISK_LEVEL: Final = "risk_level" -ATTR_SUMMARY: Final = "summary" -ATTR_DETECTED_OBJECTS: Final = "detected_objects" -``` - -### 4.4 Config Flow - -**File:** `homeassistant/integration/custom_components/homesec/config_flow.py` - -The config flow automatically detects if the HomeSec add-on is running and offers one-click setup. - -```python -"""Config flow for HomeSec integration.""" - -from __future__ import annotations - -import logging -from typing import Any - -import aiohttp -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_API_KEY -from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import ( - DOMAIN, - DEFAULT_PORT, - CONF_VERIFY_SSL, - DEFAULT_VERIFY_SSL, - ADDON_HOSTNAME, -) - -_LOGGER = logging.getLogger(__name__) - -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - vol.Optional(CONF_API_KEY): str, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, - } -) - - -async def validate_connection( - hass: HomeAssistant, - host: str, - port: int, - api_key: str | None = None, - verify_ssl: bool = True, -) -> dict[str, Any]: - """Validate the user input allows us to connect.""" - session = async_get_clientsession(hass, verify_ssl=verify_ssl) - - headers = {} - if api_key: - headers["Authorization"] = f"Bearer {api_key}" - - url = f"http://{host}:{port}/api/v1/health" - - async with session.get(url, headers=headers) as response: - if response.status == 401: - raise InvalidAuth - if response.status != 200: - raise CannotConnect - - data = await response.json() - return { - "title": f"HomeSec ({host})", - "version": data.get("version", "unknown"), - "cameras": data.get("sources", []), - } - - -async def detect_addon(hass: HomeAssistant) -> bool: - """Check if HomeSec add-on is running.""" - try: - # Try to connect to add-on at localhost:8080 - await validate_connection(hass, ADDON_HOSTNAME, DEFAULT_PORT, verify_ssl=False) - return True - except (CannotConnect, InvalidAuth, Exception): - return False - - -class HomesecConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for HomeSec.""" - - VERSION = 1 - - def __init__(self) -> None: - """Initialize the config flow.""" - self._host: str | None = None - self._port: int = DEFAULT_PORT - self._api_key: str | None = None - self._verify_ssl: bool = DEFAULT_VERIFY_SSL - self._cameras: list[dict] = [] - self._addon_detected: bool = False - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the initial step - check for add-on first.""" - # Check if add-on is running - if await detect_addon(self.hass): - self._addon_detected = True - return await self.async_step_addon() - - # No add-on found, show manual setup - return await self.async_step_manual(user_input) - - async def async_step_addon( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle add-on auto-discovery confirmation.""" - errors = {} - - if user_input is not None: - # User confirmed, connect to add-on - self._host = ADDON_HOSTNAME - self._port = DEFAULT_PORT - self._verify_ssl = False - - try: - info = await validate_connection( - self.hass, self._host, self._port, verify_ssl=False - ) - self._cameras = info.get("cameras", []) - except CannotConnect: - errors["base"] = "cannot_connect" - # Fall back to manual setup - return await self.async_step_manual() - except Exception: - _LOGGER.exception("Unexpected exception connecting to add-on") - errors["base"] = "unknown" - return await self.async_step_manual() - else: - await self.async_set_unique_id(f"homesec_{self._host}_{self._port}") - self._abort_if_unique_id_configured() - return await self.async_step_cameras() - - # Show confirmation form - return self.async_show_form( - step_id="addon", - description_placeholders={ - "addon_url": f"http://{ADDON_HOSTNAME}:{DEFAULT_PORT}", - }, - errors=errors, - ) - - async def async_step_manual( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle manual setup for standalone HomeSec.""" - errors = {} - - if user_input is not None: - self._host = user_input[CONF_HOST] - self._port = user_input.get(CONF_PORT, DEFAULT_PORT) - self._api_key = user_input.get(CONF_API_KEY) - self._verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) - - try: - info = await validate_connection( - self.hass, - self._host, - self._port, - self._api_key, - self._verify_ssl, - ) - self._cameras = info.get("cameras", []) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - await self.async_set_unique_id(f"homesec_{self._host}_{self._port}") - self._abort_if_unique_id_configured() - return await self.async_step_cameras() - - return self.async_show_form( - step_id="manual", - data_schema=STEP_USER_DATA_SCHEMA, - errors=errors, - ) - - async def async_step_cameras( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle camera configuration step.""" - if user_input is not None: - return self.async_create_entry( - title=f"HomeSec ({'Add-on' if self._addon_detected else self._host})", - data={ - CONF_HOST: self._host, - CONF_PORT: self._port, - CONF_API_KEY: self._api_key, - CONF_VERIFY_SSL: self._verify_ssl, - "addon": self._addon_detected, - }, - options={ - "cameras": user_input.get("cameras", []), - "motion_reset_seconds": 30, # Configurable motion sensor reset - }, - ) - - camera_names = [c["name"] for c in self._cameras] - schema = vol.Schema( - { - vol.Optional("cameras", default=camera_names): vol.All( - vol.Coerce(list), - [vol.In(camera_names)], - ), - } - ) - - return self.async_show_form( - step_id="cameras", - data_schema=schema, - description_placeholders={"camera_count": str(len(self._cameras))}, - ) - - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle options flow for HomeSec.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] - camera_names = [c["name"] for c in coordinator.data.get("cameras", [])] - current_cameras = self.config_entry.options.get("cameras", camera_names) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional("cameras", default=current_cameras): vol.All( - vol.Coerce(list), - [vol.In(camera_names)], - ), - vol.Optional( - "scan_interval", - default=self.config_entry.options.get("scan_interval", 30), - ): vol.All(vol.Coerce(int), vol.Range(min=10, max=300)), - vol.Optional( - "motion_reset_seconds", - default=self.config_entry.options.get("motion_reset_seconds", 30), - ): vol.All(vol.Coerce(int), vol.Range(min=5, max=300)), - } - ), - ) - - -class CannotConnect(Exception): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(Exception): - """Error to indicate there is invalid auth.""" -``` - -Integration behavior for config changes: - -- When users add/update/remove cameras in HA, call the HomeSec API. -- If `restart_required: true`, show a confirmation and invoke `/api/v1/system/restart` - (or instruct the user to restart the add-on). - -### 4.5 Data Coordinator - -**File:** `homeassistant/integration/custom_components/homesec/coordinator.py` - -```python -"""Data coordinator for HomeSec integration.""" - -from __future__ import annotations - -import asyncio -import logging -from datetime import datetime, timedelta -from typing import Any - -import aiohttp -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import DOMAIN, SCAN_INTERVAL_SECONDS - -_LOGGER = logging.getLogger(__name__) - - -class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): - """Coordinator to manage HomeSec data updates.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - host: str, - port: int, - api_key: str | None = None, - verify_ssl: bool = True, - ) -> None: - """Initialize the coordinator.""" - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=SCAN_INTERVAL_SECONDS), - ) - self.host = host - self.port = port - self.api_key = api_key - self.verify_ssl = verify_ssl - self.config_entry = config_entry - self._session = async_get_clientsession(hass, verify_ssl=verify_ssl) - self._event_unsubs: list[callable] = [] - self._motion_timers: dict[str, callable] = {} # Camera -> cancel callback - self._config_version: int = 0 # Track for optimistic concurrency - - @property - def base_url(self) -> str: - """Return the base URL for the API.""" - return f"http://{self.host}:{self.port}/api/v1" - - @property - def _headers(self) -> dict[str, str]: - """Return headers for API requests.""" - headers = {"Content-Type": "application/json"} - if self.api_key: - headers["Authorization"] = f"Bearer {self.api_key}" - return headers - - async def _async_update_data(self) -> dict[str, Any]: - """Fetch data from HomeSec API.""" - try: - async with asyncio.timeout(10): - # Fetch health status - async with self._session.get( - f"{self.base_url}/health", - headers=self._headers, - ) as response: - response.raise_for_status() - health = await response.json() - - # Fetch cameras - async with self._session.get( - f"{self.base_url}/cameras", - headers=self._headers, - ) as response: - response.raise_for_status() - cameras_data = await response.json() - - # Fetch config for version tracking - async with self._session.get( - f"{self.base_url}/config", - headers=self._headers, - ) as response: - response.raise_for_status() - config_data = await response.json() - self._config_version = config_data.get("config_version", 0) - - return { - "health": health, - "cameras": cameras_data.get("cameras", []), - "config_version": self._config_version, - "connected": True, - } - - except asyncio.TimeoutError as err: - raise UpdateFailed("Timeout connecting to HomeSec") from err - except aiohttp.ClientError as err: - raise UpdateFailed(f"Error communicating with HomeSec: {err}") from err - - async def async_subscribe_events(self) -> None: - """Subscribe to HomeSec events fired via HA Events API.""" - if self._event_unsubs: - return - - from homeassistant.core import Event - from homeassistant.helpers.event import async_call_later - - async def _on_alert(event: Event) -> None: - """Handle homesec_alert event with motion timer.""" - _LOGGER.debug("Received homesec_alert event: %s", event.data) - camera = event.data.get("camera") - if not camera: - return - - # Store latest alert data - self.data.setdefault("latest_alerts", {})[camera] = event.data - - # Set motion active for this camera - self.data.setdefault("motion_active", {})[camera] = True - - # Cancel any existing reset timer for this camera - cancel_key = f"motion_reset_{camera}" - if cancel_key in self._motion_timers: - self._motion_timers[cancel_key]() # Cancel existing timer - - # Schedule motion reset after configured duration - reset_seconds = self.config_entry.options.get("motion_reset_seconds", 30) - - async def reset_motion(_now: datetime) -> None: - """Reset motion sensor after timeout.""" - self.data.setdefault("motion_active", {})[camera] = False - self._motion_timers.pop(cancel_key, None) - self.async_set_updated_data(self.data) - - self._motion_timers[cancel_key] = async_call_later( - self.hass, reset_seconds, reset_motion - ) - - # Trigger immediate data refresh - self.async_set_updated_data(self.data) - - async def _on_camera_health(event: Event) -> None: - """Handle homesec_camera_health event.""" - _LOGGER.debug("Received homesec_camera_health event: %s", event.data) - await self.async_request_refresh() - - async def _on_clip_recorded(event: Event) -> None: - """Handle homesec_clip_recorded event.""" - _LOGGER.debug("Received homesec_clip_recorded event: %s", event.data) - - # Subscribe to HomeSec events - self._event_unsubs.append( - self.hass.bus.async_listen("homesec_alert", _on_alert) - ) - self._event_unsubs.append( - self.hass.bus.async_listen("homesec_camera_health", _on_camera_health) - ) - self._event_unsubs.append( - self.hass.bus.async_listen("homesec_clip_recorded", _on_clip_recorded) - ) - _LOGGER.info("Subscribed to HomeSec events") - - async def async_unsubscribe_events(self) -> None: - """Unsubscribe from HomeSec events.""" - for unsub in self._event_unsubs: - unsub() - self._event_unsubs.clear() - - # API Methods - - async def async_get_cameras(self) -> list[dict]: - """Get list of cameras.""" - async with self._session.get( - f"{self.base_url}/cameras", - headers=self._headers, - ) as response: - response.raise_for_status() - data = await response.json() - return data.get("cameras", []) - - async def async_add_camera( - self, - name: str, - source_backend: str, - source_config: dict, - ) -> dict: - """Add a new camera. Uses optimistic concurrency with config_version.""" - payload = { - "name": name, - "source_backend": source_backend, - "source_config": source_config, - "config_version": self._config_version, - } - async with self._session.post( - f"{self.base_url}/cameras", - headers=self._headers, - json=payload, - ) as response: - if response.status == 409: - raise ConfigVersionConflict("Config was modified, please refresh") - response.raise_for_status() - result = await response.json() - self._config_version = result.get("config_version", self._config_version) - return result - - async def async_update_camera( - self, - camera_name: str, - source_config: dict | None = None, - ) -> dict: - """Update camera source configuration. Uses optimistic concurrency.""" - payload = {"config_version": self._config_version} - if source_config is not None: - payload["source_config"] = source_config - - async with self._session.put( - f"{self.base_url}/cameras/{camera_name}", - headers=self._headers, - json=payload, - ) as response: - if response.status == 409: - raise ConfigVersionConflict("Config was modified, please refresh") - response.raise_for_status() - result = await response.json() - self._config_version = result.get("config_version", self._config_version) - return result - - async def async_delete_camera(self, camera_name: str) -> None: - """Delete a camera. Uses optimistic concurrency.""" - async with self._session.delete( - f"{self.base_url}/cameras/{camera_name}", - headers=self._headers, - params={"config_version": self._config_version}, - ) as response: - if response.status == 409: - raise ConfigVersionConflict("Config was modified, please refresh") - response.raise_for_status() - result = await response.json() - self._config_version = result.get("config_version", self._config_version) - - async def async_set_camera_enabled(self, camera_name: str, enabled: bool) -> dict: - """Enable or disable a camera (stops RTSP connection when disabled).""" - return await self.async_update_camera( - camera_name, - source_config={"enabled": enabled}, - ) - - -class ConfigVersionConflict(Exception): - """Raised when config_version is stale (409 Conflict).""" - - async def async_test_camera(self, camera_name: str) -> dict: - """Test camera connection.""" - async with self._session.post( - f"{self.base_url}/cameras/{camera_name}/test", - headers=self._headers, - ) as response: - response.raise_for_status() - return await response.json() -``` - -### 4.6 Integration Setup - -**File:** `homeassistant/integration/custom_components/homesec/__init__.py` - -```python -"""HomeSec integration for Home Assistant.""" - -from __future__ import annotations - -import logging - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_API_KEY, Platform -from homeassistant.core import HomeAssistant - -from .const import DOMAIN, CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL -from .coordinator import HomesecCoordinator - -_LOGGER = logging.getLogger(__name__) - -PLATFORMS: list[Platform] = [ - Platform.BINARY_SENSOR, - Platform.SENSOR, - Platform.SWITCH, -] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up HomeSec from a config entry.""" - coordinator = HomesecCoordinator( - hass, - entry, - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - api_key=entry.data.get(CONF_API_KEY), - verify_ssl=entry.data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), - ) - - # Fetch initial data - await coordinator.async_config_entry_first_refresh() - - # Subscribe to HomeSec events (via HA Events API) - await coordinator.async_subscribe_events() - - # Store coordinator - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator - - # Set up platforms - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - # Register update listener for options changes - entry.async_on_unload(entry.add_update_listener(async_update_options)) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - # Unsubscribe from events - coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] - await coordinator.async_unsubscribe_events() - - # Cancel any pending motion timers - for cancel in coordinator._motion_timers.values(): - cancel() - coordinator._motion_timers.clear() - - # Unload platforms - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok - - -async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) -``` - -### 4.7 Base Entity - -**File:** `homeassistant/integration/custom_components/homesec/entity.py` - -```python -"""Base entity for HomeSec integration.""" - -from __future__ import annotations - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import HomesecCoordinator - - -class HomesecEntity(CoordinatorEntity[HomesecCoordinator]): - """Base class for HomeSec entities.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: HomesecCoordinator, - camera_name: str, - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - self._camera_name = camera_name - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this entity.""" - return DeviceInfo( - identifiers={(DOMAIN, self._camera_name)}, - name=f"HomeSec {self._camera_name}", - manufacturer="HomeSec", - model="AI Security Camera", - sw_version=self.coordinator.data.get("health", {}).get("version", "unknown"), - via_device=(DOMAIN, "homesec_hub"), - ) - - def _get_camera_data(self) -> dict | None: - """Get camera data from coordinator.""" - cameras = self.coordinator.data.get("cameras", []) - for camera in cameras: - if camera.get("name") == self._camera_name: - return camera - return None - - def _is_motion_active(self) -> bool: - """Check if motion is currently active for this camera.""" - return self.coordinator.data.get("motion_active", {}).get(self._camera_name, False) - - def _get_latest_alert(self) -> dict | None: - """Get the latest alert for this camera.""" - return self.coordinator.data.get("latest_alerts", {}).get(self._camera_name) -``` - -### 4.8 Entity Platforms - -**File:** `homeassistant/integration/custom_components/homesec/sensor.py` - -```python -"""Sensor platform for HomeSec integration.""" - -from __future__ import annotations - -from typing import Any - -from homeassistant.components.sensor import ( - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN, ATTR_ACTIVITY_TYPE, ATTR_RISK_LEVEL, ATTR_SUMMARY -from .coordinator import HomesecCoordinator -from .entity import HomesecEntity - -CAMERA_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="last_activity", - name="Last Activity", - icon="mdi:motion-sensor", - ), - SensorEntityDescription( - key="risk_level", - name="Risk Level", - icon="mdi:shield-alert", - ), - SensorEntityDescription( - key="health", - name="Health", - icon="mdi:heart-pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - SensorEntityDescription( - key="clips_today", - name="Clips Today", - icon="mdi:filmstrip-box", - state_class=SensorStateClass.TOTAL_INCREASING, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up HomeSec sensors.""" - coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] - - entities: list[SensorEntity] = [] - - for camera in coordinator.data.get("cameras", []): - camera_name = camera["name"] - for description in CAMERA_SENSORS: - entities.append( - HomesecCameraSensor(coordinator, camera_name, description) - ) - - async_add_entities(entities) - - -class HomesecCameraSensor(HomesecEntity, SensorEntity): - """Representation of a HomeSec camera sensor.""" - - def __init__( - self, - coordinator: HomesecCoordinator, - camera_name: str, - description: SensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator, camera_name) - self.entity_description = description - self._attr_unique_id = f"{camera_name}_{description.key}" - - @property - def native_value(self) -> str | int | None: - """Return the state of the sensor.""" - camera = self._get_camera_data() - if not camera: - return None - - key = self.entity_description.key - - if key == "last_activity": - return camera.get("last_activity", {}).get("activity_type") - elif key == "risk_level": - return camera.get("last_activity", {}).get("risk_level") - elif key == "health": - return "healthy" if camera.get("healthy") else "unhealthy" - elif key == "clips_today": - return camera.get("stats", {}).get("clips_today", 0) - - return None - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return additional state attributes.""" - camera = self._get_camera_data() - if not camera: - return {} - - key = self.entity_description.key - attrs = {} - - if key == "last_activity": - activity = camera.get("last_activity", {}) - attrs[ATTR_ACTIVITY_TYPE] = activity.get("activity_type") - attrs[ATTR_RISK_LEVEL] = activity.get("risk_level") - attrs[ATTR_SUMMARY] = activity.get("summary") - attrs["clip_id"] = activity.get("clip_id") - attrs["view_url"] = activity.get("view_url") - attrs["timestamp"] = activity.get("timestamp") - - return attrs -``` - -**File:** `homeassistant/integration/custom_components/homesec/binary_sensor.py` - -```python -"""Binary sensor platform for HomeSec integration.""" - -from __future__ import annotations - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, - BinarySensorEntityDescription, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN -from .coordinator import HomesecCoordinator -from .entity import HomesecEntity - -CAMERA_BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( - BinarySensorEntityDescription( - key="motion", - name="Motion", - device_class=BinarySensorDeviceClass.MOTION, - ), - BinarySensorEntityDescription( - key="person", - name="Person Detected", - device_class=BinarySensorDeviceClass.OCCUPANCY, - ), - BinarySensorEntityDescription( - key="online", - name="Online", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up HomeSec binary sensors.""" - coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] - - entities: list[BinarySensorEntity] = [] - - for camera in coordinator.data.get("cameras", []): - camera_name = camera["name"] - for description in CAMERA_BINARY_SENSORS: - entities.append( - HomesecCameraBinarySensor(coordinator, camera_name, description) - ) - - async_add_entities(entities) - - -class HomesecCameraBinarySensor(HomesecEntity, BinarySensorEntity): - """Representation of a HomeSec camera binary sensor.""" - - def __init__( - self, - coordinator: HomesecCoordinator, - camera_name: str, - description: BinarySensorEntityDescription, - ) -> None: - """Initialize the binary sensor.""" - super().__init__(coordinator, camera_name) - self.entity_description = description - self._attr_unique_id = f"{camera_name}_{description.key}" - - @property - def is_on(self) -> bool | None: - """Return true if the binary sensor is on.""" - camera = self._get_camera_data() - if not camera: - return None - - key = self.entity_description.key - - if key == "motion": - # Use timer-based motion state from coordinator - return self._is_motion_active() - elif key == "person": - # Person detected based on latest alert - alert = self._get_latest_alert() - if alert and self._is_motion_active(): - return alert.get("activity_type") == "person" - return False - elif key == "online": - return camera.get("healthy", False) - - return None -``` - -**File:** `homeassistant/integration/custom_components/homesec/switch.py` - -```python -"""Switch platform for HomeSec integration. - -Prerequisite: Add `enabled: bool = True` field to CameraConfig in -src/homesec/models/config.py. The switch toggles this field via the API. -""" - -from __future__ import annotations - -import logging -from typing import Any - -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) -from .coordinator import HomesecCoordinator -from .entity import HomesecEntity - -CAMERA_SWITCHES: tuple[SwitchEntityDescription, ...] = ( - SwitchEntityDescription( - key="enabled", - name="Enabled", - icon="mdi:video", - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up HomeSec switches.""" - coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] - - entities: list[SwitchEntity] = [] - - for camera in coordinator.data.get("cameras", []): - camera_name = camera["name"] - for description in CAMERA_SWITCHES: - entities.append( - HomesecCameraSwitch(coordinator, camera_name, description) - ) - - async_add_entities(entities) - - -class HomesecCameraSwitch(HomesecEntity, SwitchEntity): - """Representation of a HomeSec camera switch.""" - - def __init__( - self, - coordinator: HomesecCoordinator, - camera_name: str, - description: SwitchEntityDescription, - ) -> None: - """Initialize the switch.""" - super().__init__(coordinator, camera_name) - self.entity_description = description - self._attr_unique_id = f"{camera_name}_{description.key}" - - @property - def is_on(self) -> bool | None: - """Return true if the switch is on.""" - camera = self._get_camera_data() - if not camera: - return None - - key = self.entity_description.key - - if key == "enabled": - # NOTE: Requires `enabled: bool` field on CameraConfig (see prerequisite below) - return camera.get("enabled", True) - - return None - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the switch on (enable camera, starts RTSP connection).""" - try: - await self.coordinator.async_set_camera_enabled(self._camera_name, True) - await self.coordinator.async_request_refresh() - except Exception as err: - _LOGGER.error("Failed to enable camera %s: %s", self._camera_name, err) - raise HomeAssistantError(f"Failed to enable camera: {err}") from err - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the switch off (disable camera, stops RTSP connection).""" - try: - await self.coordinator.async_set_camera_enabled(self._camera_name, False) - await self.coordinator.async_request_refresh() - except Exception as err: - _LOGGER.error("Failed to disable camera %s: %s", self._camera_name, err) - raise HomeAssistantError(f"Failed to disable camera: {err}") from err -``` - -### 4.9 Services - -**File:** `homeassistant/integration/custom_components/homesec/services.yaml` - -```yaml -add_camera: - name: Add Camera - description: Add a new camera to HomeSec - fields: - name: - name: Name - description: Unique identifier for the camera - required: true - example: "front_door" - selector: - text: - source_backend: - name: Source Backend - description: Camera source backend type - required: true - example: "rtsp" - selector: - select: - options: - - "rtsp" - - "ftp" - - "local_folder" - rtsp_url: - name: RTSP URL - description: RTSP stream URL (for rtsp backend) - example: "rtsp://192.168.1.100:554/stream" - selector: - text: - -remove_camera: - name: Remove Camera - description: Remove a camera from HomeSec - target: - device: - integration: homesec - -# TODO v2: Add these services (require additional API endpoints) -# set_alert_policy: -# name: Set Alert Policy -# description: Configure alert policy override for a camera -# (requires PUT /api/v1/config/alert_policy endpoint) -# -# test_camera: -# name: Test Camera -# description: Test camera connection -# (requires POST /api/v1/cameras/{name}/test endpoint) -``` - -### 4.10 Translations - -**File:** `homeassistant/integration/custom_components/homesec/translations/en.json` - -```json -{ - "config": { - "step": { - "user": { - "title": "Connect to HomeSec", - "description": "Enter the connection details for your HomeSec instance.", - "data": { - "host": "Host", - "port": "Port", - "api_key": "API Key (optional)", - "verify_ssl": "Verify SSL certificate" - } - }, - "cameras": { - "title": "Select Cameras", - "description": "Found {camera_count} cameras. Select which ones to add to Home Assistant.", - "data": { - "cameras": "Cameras" - } - } - }, - "error": { - "cannot_connect": "Failed to connect to HomeSec", - "invalid_auth": "Invalid API key", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "This HomeSec instance is already configured" - } - }, - "options": { - "step": { - "init": { - "title": "HomeSec Options", - "data": { - "cameras": "Enabled cameras", - "scan_interval": "Update interval (seconds)" - } - } - } - }, - "entity": { - "sensor": { - "last_activity": { - "name": "Last Activity" - }, - "risk_level": { - "name": "Risk Level" - }, - "health": { - "name": "Health" - }, - "clips_today": { - "name": "Clips Today" - } - }, - "binary_sensor": { - "motion": { - "name": "Motion" - }, - "person": { - "name": "Person Detected" - }, - "online": { - "name": "Online" - } - } - }, - "services": { - "add_camera": { - "name": "Add Camera", - "description": "Add a new camera to HomeSec" - }, - "remove_camera": { - "name": "Remove Camera", - "description": "Remove a camera from HomeSec" - } - } -} -``` - -### 4.11 Acceptance Criteria - -- [ ] Config flow auto-detects add-on and connects to HomeSec -- [ ] Config flow falls back to manual setup for standalone users -- [ ] All entity platforms create entities correctly -- [ ] DataUpdateCoordinator fetches data at correct intervals -- [ ] HA Events subscription triggers refresh on alerts (homesec_alert, homesec_camera_health) -- [ ] Motion sensor auto-resets after configurable timeout (default 30s) -- [ ] Camera switch enables/disables camera (requires `enabled` field prerequisite) -- [ ] Config version tracking prevents conflicts (409 on stale version) -- [ ] Services work correctly (add_camera, remove_camera) -- [ ] Options flow allows reconfiguration -- [ ] Diagnostics provide useful debug information -- [ ] All strings are translatable -- [ ] HACS installation works -- [ ] Error handling shows user-friendly messages on API failures - ---- - -## Phase 5: Advanced Features - -**Goal:** Premium features for power users. - -**Estimated Effort:** 5-7 days - -### 5.1 Custom Lovelace Card (Optional) - -**File:** `homeassistant/integration/custom_components/homesec/www/homesec-camera-card.js` - -```javascript -class HomesecCameraCard extends HTMLElement { - set hass(hass) { - if (!this.content) { - this.innerHTML = ` - -
-
- -
-
- Motion - Person -
-
-
-
-
-
-
- - `; - this.content = true; - } - - const config = this._config; - const cameraEntity = config.entity; - const state = hass.states[cameraEntity]; - - if (state) { - // Update camera image - const img = this.querySelector("#camera-image"); - img.src = `/api/camera_proxy/${cameraEntity}?token=${state.attributes.access_token}`; - - // Update detection badges - const motionEntity = cameraEntity.replace("camera.", "binary_sensor.") + "_motion"; - const personEntity = cameraEntity.replace("camera.", "binary_sensor.") + "_person"; - - const motionBadge = this.querySelector("#motion-badge"); - const personBadge = this.querySelector("#person-badge"); - - motionBadge.className = `badge ${hass.states[motionEntity]?.state === "on" ? "active" : "inactive"}`; - personBadge.className = `badge ${hass.states[personEntity]?.state === "on" ? "active" : "inactive"}`; - - // Update activity info - const activityEntity = cameraEntity.replace("camera.", "sensor.") + "_last_activity"; - const activityState = hass.states[activityEntity]; - - if (activityState) { - this.querySelector("#activity").textContent = `Activity: ${activityState.state}`; - this.querySelector("#risk").textContent = `Risk: ${activityState.attributes.risk_level || "N/A"}`; - } - } - } - - setConfig(config) { - if (!config.entity) { - throw new Error("You need to define an entity"); - } - this._config = config; - } - - getCardSize() { - return 4; - } -} - -customElements.define("homesec-camera-card", HomesecCameraCard); -``` - -### 5.2 Event Timeline Panel (Optional) - -Create a custom panel for viewing event history with timeline visualization. - -### 5.3 Acceptance Criteria - -- [ ] Snapshot images work -- [ ] Custom Lovelace card displays detection overlays -- [ ] Event timeline shows historical data - ---- - -## Testing Strategy - -All new tests must include Given/When/Then comments (per `TESTING.md`). Prefer behavioral -style tests that assert observable outcomes, not internal state. - -### Unit Tests - -``` -tests/ -├── unit/ -│ ├── api/ -│ │ ├── test_routes_cameras.py -│ │ ├── test_routes_clips.py -│ │ └── test_routes_events.py -│ ├── plugins/ -│ │ └── notifiers/ -│ │ └── test_mqtt_discovery.py -│ └── models/ -│ └── test_config_mqtt.py -├── integration/ -│ ├── test_mqtt_ha_integration.py -│ ├── test_api_full_flow.py -│ └── test_ha_addon.py -└── e2e/ - └── test_ha_config_flow.py -``` - -### Integration Tests - -1. **API Tests:** - - Full CRUD cycle for cameras - - Authentication flows - - Returns 503 when Postgres is unavailable - -3. **HA Notifier Tests:** - - Supervisor mode detection (SUPERVISOR_TOKEN) - - Standalone mode requires url_env + token_env - - Events fire correctly: homesec_alert, homesec_camera_health, homesec_clip_recorded - - HA unreachable does not stall clip processing (best-effort) - -4. **Add-on Tests:** - - Installation on HA OS - - SUPERVISOR_TOKEN injected automatically - - HA Events API works (homesec_alert reaches HA) - - Ingress access to API - -### Manual Testing Checklist - -- [ ] Install add-on from repository -- [ ] Configure via HA UI -- [ ] Add/remove cameras -- [ ] Verify entities update on alerts (via HA Events) -- [ ] Test automations with HomeSec triggers -- [ ] Verify snapshots and links (if enabled) in dashboard - -### Optional: MQTT Discovery Tests (Phase 1) - -If user enables MQTT Discovery: -- [ ] Publish discovery → verify entities appear in HA -- [ ] HA restart → verify discovery republishes -- [ ] Camera add → verify new entities created - ---- - -## Migration Guide - -### From Standalone to Add-on - -1. Export current `config.yaml` -2. Install HomeSec add-on -3. Copy config to `/config/homesec/config.yaml` -4. Create `/data/overrides.yaml` for HA-managed config -5. Update database URL if using external Postgres -6. Start add-on - -### From MQTT-only to Full Integration - -1. Keep existing MQTT configuration +1. Keep existing MQTT notifier configuration (for Node-RED, etc.) 2. Install custom integration via HACS 3. Configure integration with HomeSec URL -4. Entities will be created alongside MQTT entities -5. Optionally disable MQTT discovery to avoid duplicates - ---- - -## Appendix: File Change Summary - -### Phase 2: REST API -- `src/homesec/api/` - New package (server.py, routes/*, dependencies.py) -- `src/homesec/api/routes/stats.py` - Stats endpoint for hub entities -- `src/homesec/api/routes/health.py` - Health endpoint with response schema -- `src/homesec/config/manager.py` - Config persistence + restart signaling -- `src/homesec/models/config.py` - Add FastAPIServerConfig -- `src/homesec/config/loader.py` - Support multiple YAML files + merge semantics -- `src/homesec/cli.py` - Accept repeated `--config` flags (order matters) -- `src/homesec/app.py` - Integrate API server -- `pyproject.toml` - Add fastapi, uvicorn dependencies - -### Phase 2.5: Home Assistant Notifier -- `src/homesec/models/config.py` - Add HomeAssistantNotifierConfig -- `src/homesec/plugins/notifiers/home_assistant.py` - New file (HA Events API notifier) -- `config/example.yaml` - Add home_assistant notifier example -- `tests/unit/plugins/notifiers/test_home_assistant.py` - New tests - -### Phase 4: Integration (in `homeassistant/integration/`) -- `homeassistant/integration/hacs.json` - HACS configuration -- `homeassistant/integration/custom_components/homesec/__init__.py` - Setup entry -- `homeassistant/integration/custom_components/homesec/manifest.json` - Metadata -- `homeassistant/integration/custom_components/homesec/const.py` - Constants -- `homeassistant/integration/custom_components/homesec/config_flow.py` - Add-on auto-discovery -- `homeassistant/integration/custom_components/homesec/coordinator.py` - Data + event subscriptions + motion timers -- `homeassistant/integration/custom_components/homesec/entity.py` - Base entity class -- `homeassistant/integration/custom_components/homesec/sensor.py` - Sensors -- `homeassistant/integration/custom_components/homesec/binary_sensor.py` - Motion (30s auto-reset) -- `homeassistant/integration/custom_components/homesec/switch.py` - Camera enable/disable (stops RTSP) -- `homeassistant/integration/custom_components/homesec/services.yaml` - Service definitions -- `homeassistant/integration/custom_components/homesec/translations/en.json` - Strings - -### Phase 3: Add-on (in `homeassistant/addon/`) -- `repository.json` - Add-on repository manifest (at repo root, points to `homeassistant/addon/homesec/`) -- `homeassistant/addon/homesec/config.yaml` - Add-on manifest (homeassistant_api: true) -- `homeassistant/addon/homesec/Dockerfile` - Container build with PostgreSQL 16 -- `homeassistant/addon/homesec/rootfs/` - s6-overlay services (postgres-init, postgres, homesec) -- `homeassistant/addon/homesec/rootfs/etc/nginx/` - Ingress config - -### Phase 1: MQTT Discovery (Optional) -- `src/homesec/models/config.py` - Add MQTTDiscoveryConfig -- `src/homesec/plugins/notifiers/mqtt.py` - Enhance with discovery -- `src/homesec/plugins/notifiers/mqtt_discovery.py` - New file -- `src/homesec/app.py` - Register cameras with notifier -- `config/example.yaml` - Add discovery example -- `tests/unit/plugins/notifiers/test_mqtt_discovery.py` - New tests - -### Phase 5: Advanced -- `homeassistant/integration/custom_components/homesec/www/` - Lovelace cards - ---- - -## Timeline Summary - -| Phase | Duration | Dependencies | -|-------|----------|--------------| -| Phase 2: REST API | 5-7 days | None | -| Phase 2.5: HA Notifier | 2-3 days | None | -| Phase 4: Integration | 7-10 days | Phase 2, 2.5 | -| Phase 3: Add-on | 3-4 days | Phase 2, 2.5 | -| Phase 1: MQTT Discovery (optional) | 2-3 days | None | -| Phase 5: Advanced | 5-7 days | Phase 4 | - -**Total: 20-27 days** (excluding optional MQTT Discovery) - -Execution order: Phase 2 → Phase 2.5 → Phase 4 → Phase 3 → Phase 5. Phase 1 (MQTT Discovery) is optional. - -Key benefit: No MQTT broker required. Add-on users get zero-config real-time events via `SUPERVISOR_TOKEN`. +4. Native entities will be created +5. Optionally disable MQTT notifier if no longer needed diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 6acb0a23..863a26ab 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -26,7 +26,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug - Tests: Given/When/Then comments required for all new tests. - P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). - **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically; standalone users provide HA URL + token. -- **No MQTT required**: MQTT Discovery is optional for users who prefer it; primary path uses HA Events API. +- **No MQTT broker required**: Primary path uses HA Events API. Existing MQTT notifier remains available for Node-RED and other MQTT consumers. - **409 Conflict UX**: Show error to user when config version is stale. - **API during Postgres outage**: Return 503 Service Unavailable. @@ -363,7 +363,9 @@ The HomeSec add-on is a Docker container managed by HA Supervisor. It bundles Po --- -### Option B: MQTT Discovery (Quick Win) +### Option B: MQTT Discovery (Quick Win) - NOT CHOSEN + +> **Note:** This option was not chosen. We went with Option A (Add-on + Native Integration) using the HA Events API for real-time communication. Enhance the existing MQTT notifier to publish **discovery messages** that auto-create HA entities: @@ -405,31 +407,12 @@ Move core homesec logic into a Home Assistant integration (runs in HA's Python e Execution order for Option A: 1. REST API + config persistence (control plane) -2. Native HA integration (config flow + entities) +2. Home Assistant notifier plugin (HA Events API) 3. Home Assistant add-on packaging -4. MQTT discovery (optional parallel track) +4. Native HA integration (config flow + entities) 5. Advanced UX (optional) -### Phase 1: MQTT Discovery Enhancement (Quick Win) - -Enhance the existing MQTT notifier to publish discovery configs: - -```python -# New entities auto-created in HA: -- binary_sensor.homesec_{camera}_motion # Motion detected -- sensor.homesec_{camera}_last_activity # "person", "delivery", etc. -- sensor.homesec_{camera}_risk_level # LOW/MEDIUM/HIGH/CRITICAL -- sensor.homesec_{camera}_health # healthy/degraded/unhealthy -- device_tracker.homesec_{camera} # Online/offline status -``` - -**Changes to homesec:** -1. Add `mqtt_discovery: true` config option -2. On startup, publish discovery configs for each camera -3. Listen for HA birth message to republish discovery -4. Publish state updates on detection events - -### Phase 2: REST API for Configuration +### Phase 1: REST API for Configuration Add a new REST API to HomeSec for remote configuration. All endpoints are `async def` and use async SQLAlchemy only. @@ -493,7 +476,6 @@ Note: `repository.json` lives at the repo root (not in `homeassistant/addon/`) f - Bundled PostgreSQL via s6-overlay (zero-config database) - Ingress support for web UI (if we build one) - Watchdog for auto-restart -- Optional MQTT Discovery for users who prefer it Users add the add-on repo via: `https://github.com/lan17/homesec` @@ -740,10 +722,9 @@ The HA integration subscribes to these events and updates entities in real-time. |-------|--------|-------|-------------| | **1. REST API** | Medium | High | Enable remote configuration and control plane | | **2. HA Notifier** | Low | High | New notifier plugin for HA Events API | -| **3. Integration** | High | Very High | Full HA UI configuration + event subscriptions | -| **4. Add-on** | Medium | High | Zero-config install for HA OS users | -| **5. MQTT Discovery (optional)** | Low | Low | For users who prefer MQTT over native integration | -| **6. Dashboard Cards** | Medium | Medium | Rich visualization | +| **3. Add-on** | Medium | High | Zero-config install for HA OS users | +| **4. Integration** | High | Very High | Full HA UI configuration + event subscriptions | +| **5. Dashboard Cards** | Medium | Medium | Rich visualization | --- @@ -753,5 +734,4 @@ The HA integration subscribes to these events and updates entities in real-time. - [Home Assistant Config Flow](https://developers.home-assistant.io/docs/config_entries_config_flow_handler/) - [Home Assistant Camera Entity](https://developers.home-assistant.io/docs/core/entity/camera/) - [Home Assistant DataUpdateCoordinator](https://developers.home-assistant.io/docs/integration_fetching_data/) -- [MQTT Discovery](https://www.home-assistant.io/integrations/mqtt/) - [HACS Documentation](https://www.hacs.xyz/) From 31b8a2de4b76e3a2165f05005511e92761552bb2 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 19:46:56 -0800 Subject: [PATCH 11/31] docs: fix Phase 0 and Phase 1 for implementation readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 fixes: - Fix method name: notify() → send() (matches actual interface) - Remove phantom start() method from interface - Move stats methods to Phase 1 (ClipRepository already exists) - Add HealthMonitorConfig for configurable health check interval - Use default no-op pattern so existing notifiers don't break - Add accurate file paths Phase 1 additions: - Add ClipRepository extensions section (list, count, delete methods) - Add ClipRepository test cases Brainstorm doc: - Fix all "lists replace" → "lists merge (union)" for consistency Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-0-prerequisites.md | 215 ++++++++++++------ docs/ha-phase-1-rest-api.md | 93 ++++++++ docs/home-assistant-integration-brainstorm.md | 6 +- 3 files changed, 241 insertions(+), 73 deletions(-) diff --git a/docs/ha-phase-0-prerequisites.md b/docs/ha-phase-0-prerequisites.md index 6870986e..8a874907 100644 --- a/docs/ha-phase-0-prerequisites.md +++ b/docs/ha-phase-0-prerequisites.md @@ -13,9 +13,11 @@ Before implementing the HA integration, these changes are needed in the core HomeSec codebase: 1. Add `enabled` field to `CameraConfig` -2. Add `publish_camera_health()` to Notifier interface -3. Implement camera health monitoring loop in Application -4. Add stats methods to ClipRepository +2. Add `publish_camera_health()` to `Notifier` interface +3. Implement `publish_camera_health()` in `MultiplexNotifier` +4. Add no-op implementations to existing notifiers +5. Add camera health monitoring loop in `Application` +6. Add health monitoring configuration --- @@ -46,32 +48,44 @@ class CameraConfig(BaseModel): **File**: `src/homesec/interfaces.py` -### Interface +### Current Interface ```python -class Notifier(Protocol): - """Notification service interface.""" +class Notifier(Shutdownable, ABC): + """Sends notifications (e.g., MQTT, email, SMS).""" + + @abstractmethod + async def send(self, alert: Alert) -> None: + """Send notification. Raises on failure.""" + raise NotImplementedError + + @abstractmethod + async def ping(self) -> bool: + """Health check. Returns True if notifier is reachable.""" + raise NotImplementedError +``` - async def start(self) -> None: ... - async def shutdown(self) -> None: ... - async def notify(self, alert: Alert) -> None: ... +### Add This Method - # NEW: Camera health status change +```python + # NEW: Camera health status change (add to Notifier class) async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: """Publish camera health status change. Called by Application when a camera transitions between healthy/unhealthy. + Default implementation is no-op. Override in subclasses that support it. + Implementations should be best-effort (don't raise on failure). """ - ... + pass # Default no-op ``` ### Constraints - Must be async +- Default implementation is no-op (not abstract - existing notifiers don't break) - Must be best-effort (failures don't crash pipeline) -- Called on health state transitions, not on every health check -- Existing notifiers (MQTT, SendGrid) can implement as no-op initially +- Called on health state transitions only, not on every health check --- @@ -79,26 +93,69 @@ class Notifier(Protocol): **File**: `src/homesec/notifiers/multiplex.py` +### Add This Method + +```python +async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: + """Broadcast camera health to all notifiers. + + Fan out to all notifiers, log errors but don't raise. + """ + if self._shutdown_called: + return + + results = await self._call_all( + lambda notifier: notifier.publish_camera_health(camera_name, healthy) + ) + + for entry, result in results: + match result: + case BaseException() as err: + logger.warning( + "Notifier publish_camera_health failed: notifier=%s error=%s", + entry.name, + err, + ) +``` + +### Constraints + +- Must fan out to all configured notifiers +- Must handle individual notifier failures gracefully (log, don't raise) +- Should continue to other notifiers even if one fails + +--- + +## 0.4 Add Health Monitoring Configuration + +**File**: `src/homesec/models/config.py` + ### Interface ```python -class MultiplexNotifier: - """Routes notifications to multiple backends.""" +class HealthMonitorConfig(BaseModel): + """Configuration for camera health monitoring.""" - async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: - """Broadcast camera health to all notifiers.""" - # Fan out to all notifiers, log errors but don't raise + enabled: bool = True + check_interval_s: int = Field(default=30, ge=5, le=300) +``` + +Add to `Config` class: +```python +class Config(BaseModel): + # ... existing fields ... + health_monitor: HealthMonitorConfig = Field(default_factory=HealthMonitorConfig) ``` ### Constraints -- Must fan out to all configured notifiers -- Must handle individual notifier failures gracefully -- Should log errors but continue to other notifiers +- Default interval is 30 seconds +- Minimum 5 seconds, maximum 300 seconds +- Can be disabled entirely with `enabled: false` --- -## 0.4 Camera Health Monitoring in Application +## 0.5 Camera Health Monitoring in Application **File**: `src/homesec/app.py` @@ -108,59 +165,74 @@ class MultiplexNotifier: class Application: """Main application orchestrator.""" - async def _monitor_camera_health(self) -> None: - """Background task that monitors camera health and publishes changes. - - Runs every N seconds (configurable), checks is_healthy() on each source, - and calls notifier.publish_camera_health() when state changes. - """ - ... + def __init__(self, ...): + # ... existing init ... + self._camera_health_state: dict[str, bool] = {} # Track previous health per camera + self._health_monitor_task: asyncio.Task | None = None async def _start_sources(self) -> None: """Start all enabled camera sources. Must check camera.enabled before starting each source. + Skip sources where enabled=False. """ - ... -``` + for camera_config in self._config.cameras: + if not camera_config.enabled: + logger.info("Skipping disabled camera: %s", camera_config.name) + continue + # ... start source ... -### Constraints + async def _monitor_camera_health(self) -> None: + """Background task that monitors camera health and publishes changes. -- Health check interval should be configurable (default: 30s) -- Only publish on state transitions (healthy→unhealthy or unhealthy→healthy) -- Track previous health state per camera -- Must respect `CameraConfig.enabled` - don't start disabled cameras -- Health monitoring task should be cancelled on shutdown + Runs every check_interval_s seconds, checks is_healthy() on each source, + and calls notifier.publish_camera_health() only when state changes. + """ + interval = self._config.health_monitor.check_interval_s ---- + while True: + await asyncio.sleep(interval) -## 0.5 Add Stats Methods to ClipRepository + for camera_name, source in self._sources.items(): + current_healthy = source.is_healthy() + previous_healthy = self._camera_health_state.get(camera_name) -**File**: `src/homesec/repository.py` (or appropriate location) + # Only publish on state transitions + if previous_healthy is not None and current_healthy != previous_healthy: + await self._notifier.publish_camera_health(camera_name, current_healthy) -### Interface + self._camera_health_state[camera_name] = current_healthy -```python -class ClipRepository(Protocol): - """Repository for clip and event data.""" + async def run(self) -> None: + """Run the application.""" + # ... existing startup ... + + # Start health monitoring if enabled + if self._config.health_monitor.enabled: + self._health_monitor_task = asyncio.create_task(self._monitor_camera_health()) - # Existing methods... + # ... rest of run ... - # NEW: Stats methods for API - async def count_clips_since(self, since: datetime) -> int: - """Count clips created since the given timestamp.""" - ... + async def shutdown(self) -> None: + """Shutdown the application.""" + # Cancel health monitor + if self._health_monitor_task: + self._health_monitor_task.cancel() + try: + await self._health_monitor_task + except asyncio.CancelledError: + pass - async def count_alerts_since(self, since: datetime) -> int: - """Count alert events (notification_sent) since the given timestamp.""" - ... + # ... rest of shutdown ... ``` ### Constraints -- Must use async SQLAlchemy -- Should be efficient (use COUNT, not fetch all) -- `count_alerts_since` counts events where `event_type='notification_sent'` +- Only publish on state transitions (healthy→unhealthy or unhealthy→healthy) +- Track previous health state per camera +- Must respect `CameraConfig.enabled` - don't start disabled cameras +- Health monitoring task should be cancelled on shutdown +- First health check after startup doesn't trigger publish (no previous state) --- @@ -168,13 +240,12 @@ class ClipRepository(Protocol): | File | Change | |------|--------| -| `src/homesec/models/config.py` | Add `enabled: bool = True` to `CameraConfig` | -| `src/homesec/interfaces.py` | Add `publish_camera_health()` to `Notifier` protocol | +| `src/homesec/models/config.py` | Add `enabled: bool = True` to `CameraConfig`, add `HealthMonitorConfig` | +| `src/homesec/interfaces.py` | Add `publish_camera_health()` with default no-op to `Notifier` | | `src/homesec/notifiers/multiplex.py` | Implement `publish_camera_health()` fan-out | -| `src/homesec/plugins/notifiers/mqtt.py` | Add no-op `publish_camera_health()` | -| `src/homesec/plugins/notifiers/sendgrid_email.py` | Add no-op `publish_camera_health()` | | `src/homesec/app.py` | Add health monitoring loop, respect `enabled` field | -| `src/homesec/repository.py` | Add `count_clips_since()`, `count_alerts_since()` | + +**Note**: Existing notifiers (MQTT, SendGrid) inherit the default no-op implementation and don't need changes. --- @@ -195,13 +266,14 @@ class ClipRepository(Protocol): - Given a config with `enabled=True` (or missing), when Application starts, then source is started **publish_camera_health** -- Given a camera transitions from healthy to unhealthy, when health monitor runs, then `publish_camera_health(camera, False)` is called -- Given a camera stays healthy, when health monitor runs, then `publish_camera_health` is NOT called +- Given a camera transitions from healthy to unhealthy, when health monitor runs, then `publish_camera_health(camera, False)` is called once +- Given a camera stays healthy across checks, when health monitor runs, then `publish_camera_health` is NOT called +- Given a camera is checked for the first time, when health monitor runs, then `publish_camera_health` is NOT called (no previous state) - Given MultiplexNotifier with 3 notifiers and one fails, when `publish_camera_health` is called, then other notifiers still receive the call -**count_clips_since / count_alerts_since** -- Given 5 clips created today and 10 yesterday, when `count_clips_since(today_start)` is called, then returns 5 -- Given 0 alerts, when `count_alerts_since(any_date)` is called, then returns 0 +**HealthMonitorConfig** +- Given `health_monitor.enabled=False`, when Application starts, then no health monitor task is created +- Given `health_monitor.check_interval_s=10`, when Application runs, then health checks happen every 10 seconds --- @@ -214,7 +286,10 @@ pytest tests/unit/models/test_config.py -v -k "enabled" pytest tests/unit/notifiers/test_multiplex.py -v -k "camera_health" # Verify CameraConfig accepts enabled field -python -c "from homesec.models.config import CameraConfig; print(CameraConfig(name='test', enabled=False, source={'backend': 'rtsp', 'config': {}}))" +python -c "from homesec.models.config import CameraConfig, CameraSourceConfig; print(CameraConfig(name='test', enabled=False, source=CameraSourceConfig(backend='rtsp', config={})))" + +# Verify HealthMonitorConfig +python -c "from homesec.models.config import HealthMonitorConfig; print(HealthMonitorConfig(check_interval_s=60))" ``` --- @@ -222,11 +297,11 @@ python -c "from homesec.models.config import CameraConfig; print(CameraConfig(na ## Definition of Done - [ ] `CameraConfig` has `enabled: bool = True` field -- [ ] `Notifier` protocol includes `publish_camera_health()` +- [ ] `HealthMonitorConfig` exists with `enabled` and `check_interval_s` fields +- [ ] `Notifier` interface includes `publish_camera_health()` with default no-op - [ ] `MultiplexNotifier` fans out `publish_camera_health()` to all notifiers -- [ ] Existing notifiers (MQTT, SendGrid) have no-op implementations - [ ] Application skips starting sources where `enabled=False` -- [ ] Application monitors camera health and calls `publish_camera_health()` on transitions -- [ ] `ClipRepository` has `count_clips_since()` and `count_alerts_since()` +- [ ] Application monitors camera health and calls `publish_camera_health()` on transitions only +- [ ] Health monitoring respects configuration (enabled, interval) - [ ] All tests pass - [ ] Existing functionality unchanged (backwards compatible) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 7271eebe..3ee817c7 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -189,6 +189,91 @@ def deep_merge(base: dict, override: dict) -> dict: --- +## 1.2.1 ClipRepository Extensions + +**File**: `src/homesec/repository/clip_repository.py` + +The existing `ClipRepository` needs these additional methods for the API: + +### Interface + +```python +class ClipRepository: + """Coordinates state + event writes with best-effort retries.""" + + # ... existing methods ... + + # NEW: Read methods for API + async def get_clip(self, clip_id: str) -> ClipStateData | None: + """Get clip state by ID.""" + ... + + async def list_clips( + self, + *, + camera: str | None = None, + status: ClipStatus | None = None, + since: datetime | None = None, + until: datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: + """List clips with filtering and pagination. + + Returns (clips, total_count). + """ + ... + + async def list_events( + self, + *, + clip_id: str | None = None, + event_type: str | None = None, + camera: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + limit: int = 100, + ) -> tuple[list[ClipLifecycleEvent], int]: + """List events with filtering. + + Returns (events, total_count). + """ + ... + + async def delete_clip(self, clip_id: str) -> None: + """Mark clip as deleted and delete from storage. + + Uses existing record_clip_deleted() internally. + Also deletes from storage backend. + """ + ... + + async def count_clips_since(self, since: datetime) -> int: + """Count clips created since the given timestamp.""" + ... + + async def count_alerts_since(self, since: datetime) -> int: + """Count alert events (notification_sent) since the given timestamp.""" + ... + + async def ping(self) -> bool: + """Health check - verify database is reachable. + + Delegates to StateStore.ping(). + """ + return await self._state.ping() +``` + +### Constraints + +- Must use async SQLAlchemy +- Counts should be efficient (use SQL COUNT, not fetch all) +- `count_alerts_since` counts events where `event_type='notification_sent'` +- `list_clips` and `list_events` return tuple of (items, total_count) for pagination +- `delete_clip` should delete from both local and cloud storage + +--- + ## 1.3 API Routes ### Files @@ -329,6 +414,7 @@ class FastAPIServerConfig(BaseModel): | `src/homesec/config/manager.py` | Config persistence | | `src/homesec/config/loader.py` | Multi-file config loading | | `src/homesec/models/config.py` | Add `FastAPIServerConfig` | +| `src/homesec/repository/clip_repository.py` | Add read/list/count methods | | `src/homesec/cli.py` | Support multiple `--config` flags | | `src/homesec/app.py` | Integrate API server | | `pyproject.toml` | Add fastapi, uvicorn dependencies | @@ -364,6 +450,13 @@ class FastAPIServerConfig(BaseModel): **Clips** - Given 100 clips, when GET /clips?page=2&page_size=10, then returns clips 11-20 +**ClipRepository** +- Given 5 clips created today and 10 yesterday, when `count_clips_since(today_start)`, then returns 5 +- Given 0 alerts, when `count_alerts_since(any_date)`, then returns 0 +- Given clips with mixed cameras, when `list_clips(camera="front")`, then returns only "front" clips +- Given clip exists, when `delete_clip(clip_id)`, then clip marked deleted and storage files removed +- Given StateStore is up, when `ping()`, then returns True + --- ## Verification diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 863a26ab..8b6ed01b 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -19,7 +19,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug - API stack: FastAPI, async endpoints only, async SQLAlchemy only. - Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA may trigger restart. - Config storage: **Override YAML file** is source of truth for dynamic config. Base YAML is bootstrap-only. -- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists replace. +- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists merge (union). - Single instance: HA integration assumes one HomeSec instance (`single_config_entry`). - Secrets: never stored in HomeSec; config stores env var names; HA/add-on passes env vars at boot. - Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). @@ -421,7 +421,7 @@ Config model for Option B: - Base YAML is bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). - API writes a machine-owned **override YAML** file for all dynamic config. - HomeSec loads multiple YAML files left → right; rightmost wins. -- Dicts deep-merge; lists replace (override lists fully replace base lists). +- Dicts deep-merge; lists merge (union). - Override file default: `config/ha-overrides.yaml` (configurable via CLI). - CLI accepts multiple `--config` flags; order matters. - All config writes require `config_version` for optimistic concurrency. @@ -644,7 +644,7 @@ async def async_get_config_entry_diagnostics(hass, entry): - **Base YAML**: bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). - **Override YAML**: machine-owned, fully managed by HA via API. - **Load order**: multiple YAML files loaded left → right; rightmost wins. -- **Merge semantics**: dicts deep-merge; lists replace (override lists fully replace base lists). +- **Merge semantics**: dicts deep-merge; lists merge (union). - **Restart**: required for all config changes. --- From 1de4a2d023a442b6f46d84fedfdca700eb1252f5 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 19:48:02 -0800 Subject: [PATCH 12/31] docs: fix Phase 0 overview - remove redundant item Existing notifiers automatically inherit the default no-op implementation of publish_camera_health(), so no changes are needed to MQTT/SendGrid. Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-0-prerequisites.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/ha-phase-0-prerequisites.md b/docs/ha-phase-0-prerequisites.md index 8a874907..6fda85b4 100644 --- a/docs/ha-phase-0-prerequisites.md +++ b/docs/ha-phase-0-prerequisites.md @@ -13,11 +13,10 @@ Before implementing the HA integration, these changes are needed in the core HomeSec codebase: 1. Add `enabled` field to `CameraConfig` -2. Add `publish_camera_health()` to `Notifier` interface +2. Add `publish_camera_health()` to `Notifier` interface (with default no-op) 3. Implement `publish_camera_health()` in `MultiplexNotifier` -4. Add no-op implementations to existing notifiers +4. Add health monitoring configuration 5. Add camera health monitoring loop in `Application` -6. Add health monitoring configuration --- From aa2d6a32a1385d97ca56bc111a0acbedac0d7fd3 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 20:07:10 -0800 Subject: [PATCH 13/31] update phase 0 no push based health notification --- docs/ha-phase-0-prerequisites.md | 271 ++++-------------- docs/ha-phase-2-ha-notifier.md | 33 +-- docs/ha-phase-4-ha-integration.md | 4 +- docs/home-assistant-implementation-plan.md | 7 +- docs/home-assistant-integration-brainstorm.md | 6 +- 5 files changed, 64 insertions(+), 257 deletions(-) diff --git a/docs/ha-phase-0-prerequisites.md b/docs/ha-phase-0-prerequisites.md index 6fda85b4..96345ef4 100644 --- a/docs/ha-phase-0-prerequisites.md +++ b/docs/ha-phase-0-prerequisites.md @@ -1,8 +1,8 @@ # Phase 0: Core Prerequisites -**Goal**: Prepare the HomeSec core codebase for HA integration by adding required fields and interfaces. +**Goal**: Prepare the HomeSec core codebase for HA integration by adding the `enabled` field to CameraConfig. -**Estimated Effort**: 2-3 days +**Estimated Effort**: 0.5 days **Dependencies**: None @@ -10,13 +10,11 @@ ## Overview -Before implementing the HA integration, these changes are needed in the core HomeSec codebase: +Before implementing the HA integration, one small change is needed in the core HomeSec codebase: 1. Add `enabled` field to `CameraConfig` -2. Add `publish_camera_health()` to `Notifier` interface (with default no-op) -3. Implement `publish_camera_health()` in `MultiplexNotifier` -4. Add health monitoring configuration -5. Add camera health monitoring loop in `Application` + +**Note**: Camera health monitoring uses a **pull-based** architecture. The HA Integration (Phase 4) will poll the REST API (Phase 1) to get camera health status. No push-based health monitoring is needed in the core. --- @@ -31,7 +29,7 @@ class CameraConfig(BaseModel): """Camera configuration and clip source selection.""" name: str - enabled: bool = True # NEW: Allow disabling camera via API + enabled: bool = True # Allow disabling camera via API source: CameraSourceConfig ``` @@ -43,236 +41,74 @@ class CameraConfig(BaseModel): --- -## 0.2 Add `publish_camera_health()` to Notifier Interface - -**File**: `src/homesec/interfaces.py` - -### Current Interface - -```python -class Notifier(Shutdownable, ABC): - """Sends notifications (e.g., MQTT, email, SMS).""" - - @abstractmethod - async def send(self, alert: Alert) -> None: - """Send notification. Raises on failure.""" - raise NotImplementedError - - @abstractmethod - async def ping(self) -> bool: - """Health check. Returns True if notifier is reachable.""" - raise NotImplementedError -``` - -### Add This Method - -```python - # NEW: Camera health status change (add to Notifier class) - async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: - """Publish camera health status change. - - Called by Application when a camera transitions between healthy/unhealthy. - Default implementation is no-op. Override in subclasses that support it. - - Implementations should be best-effort (don't raise on failure). - """ - pass # Default no-op -``` - -### Constraints - -- Must be async -- Default implementation is no-op (not abstract - existing notifiers don't break) -- Must be best-effort (failures don't crash pipeline) -- Called on health state transitions only, not on every health check - ---- - -## 0.3 Update MultiplexNotifier +## 0.2 Respect `enabled` in Application -**File**: `src/homesec/notifiers/multiplex.py` +**File**: `src/homesec/app.py` -### Add This Method +### Interface ```python -async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: - """Broadcast camera health to all notifiers. +def _create_sources(self, config: Config) -> list[ClipSource]: + """Create clip sources based on config using plugin registry. - Fan out to all notifiers, log errors but don't raise. + Respects CameraConfig.enabled - skips disabled cameras. """ - if self._shutdown_called: - return - - results = await self._call_all( - lambda notifier: notifier.publish_camera_health(camera_name, healthy) - ) - - for entry, result in results: - match result: - case BaseException() as err: - logger.warning( - "Notifier publish_camera_health failed: notifier=%s error=%s", - entry.name, - err, - ) + sources: list[ClipSource] = [] + + for camera in config.cameras: + if not camera.enabled: + logger.info("Skipping disabled camera: %s", camera.name) + continue + + source_cfg = camera.source + source = load_source_plugin( + source_backend=source_cfg.backend, + config=source_cfg.config, + camera_name=camera.name, + ) + sources.append(source) + self._sources_by_name[camera.name] = source + + return sources ``` ### Constraints -- Must fan out to all configured notifiers -- Must handle individual notifier failures gracefully (log, don't raise) -- Should continue to other notifiers even if one fails +- Skip sources where `enabled=False` +- Log which cameras are skipped +- Maintain `_sources_by_name` mapping for API queries (Phase 1) --- -## 0.4 Add Health Monitoring Configuration - -**File**: `src/homesec/models/config.py` - -### Interface - -```python -class HealthMonitorConfig(BaseModel): - """Configuration for camera health monitoring.""" - - enabled: bool = True - check_interval_s: int = Field(default=30, ge=5, le=300) -``` - -Add to `Config` class: -```python -class Config(BaseModel): - # ... existing fields ... - health_monitor: HealthMonitorConfig = Field(default_factory=HealthMonitorConfig) -``` - -### Constraints +## File Changes Summary -- Default interval is 30 seconds -- Minimum 5 seconds, maximum 300 seconds -- Can be disabled entirely with `enabled: false` +| File | Change | +|------|--------| +| `src/homesec/models/config.py` | Add `enabled: bool = True` to `CameraConfig` | +| `src/homesec/app.py` | Skip disabled cameras, add `_sources_by_name` mapping | --- -## 0.5 Camera Health Monitoring in Application - -**File**: `src/homesec/app.py` - -### Interface - -```python -class Application: - """Main application orchestrator.""" - - def __init__(self, ...): - # ... existing init ... - self._camera_health_state: dict[str, bool] = {} # Track previous health per camera - self._health_monitor_task: asyncio.Task | None = None - - async def _start_sources(self) -> None: - """Start all enabled camera sources. - - Must check camera.enabled before starting each source. - Skip sources where enabled=False. - """ - for camera_config in self._config.cameras: - if not camera_config.enabled: - logger.info("Skipping disabled camera: %s", camera_config.name) - continue - # ... start source ... - - async def _monitor_camera_health(self) -> None: - """Background task that monitors camera health and publishes changes. - - Runs every check_interval_s seconds, checks is_healthy() on each source, - and calls notifier.publish_camera_health() only when state changes. - """ - interval = self._config.health_monitor.check_interval_s - - while True: - await asyncio.sleep(interval) - - for camera_name, source in self._sources.items(): - current_healthy = source.is_healthy() - previous_healthy = self._camera_health_state.get(camera_name) - - # Only publish on state transitions - if previous_healthy is not None and current_healthy != previous_healthy: - await self._notifier.publish_camera_health(camera_name, current_healthy) - - self._camera_health_state[camera_name] = current_healthy - - async def run(self) -> None: - """Run the application.""" - # ... existing startup ... - - # Start health monitoring if enabled - if self._config.health_monitor.enabled: - self._health_monitor_task = asyncio.create_task(self._monitor_camera_health()) - - # ... rest of run ... - - async def shutdown(self) -> None: - """Shutdown the application.""" - # Cancel health monitor - if self._health_monitor_task: - self._health_monitor_task.cancel() - try: - await self._health_monitor_task - except asyncio.CancelledError: - pass - - # ... rest of shutdown ... -``` - -### Constraints - -- Only publish on state transitions (healthy→unhealthy or unhealthy→healthy) -- Track previous health state per camera -- Must respect `CameraConfig.enabled` - don't start disabled cameras -- Health monitoring task should be cancelled on shutdown -- First health check after startup doesn't trigger publish (no previous state) +## Health Monitoring Architecture Note ---- - -## File Changes Summary +Camera health uses a **pull-based** architecture: -| File | Change | -|------|--------| -| `src/homesec/models/config.py` | Add `enabled: bool = True` to `CameraConfig`, add `HealthMonitorConfig` | -| `src/homesec/interfaces.py` | Add `publish_camera_health()` with default no-op to `Notifier` | -| `src/homesec/notifiers/multiplex.py` | Implement `publish_camera_health()` fan-out | -| `src/homesec/app.py` | Add health monitoring loop, respect `enabled` field | +1. **ClipSource** already has `is_healthy()` method +2. **Phase 1 REST API** exposes `GET /api/v1/cameras/{name}/status` (includes health) +3. **Phase 4 HA Integration** polls that endpoint every 30-60s via DataUpdateCoordinator -**Note**: Existing notifiers (MQTT, SendGrid) inherit the default no-op implementation and don't need changes. +This keeps the HomeSec core simple and stateless. The HA Integration handles converting pull to entity updates. --- ## Test Expectations -### Fixtures Needed - -- `camera_config_enabled` - CameraConfig with enabled=True -- `camera_config_disabled` - CameraConfig with enabled=False -- `mock_notifier` - Notifier that records calls to `publish_camera_health()` -- `mock_source_healthy` - ClipSource where `is_healthy()` returns True -- `mock_source_unhealthy` - ClipSource where `is_healthy()` returns False - ### Test Cases **CameraConfig.enabled** - Given a config with `enabled=False`, when Application starts, then source is not started - Given a config with `enabled=True` (or missing), when Application starts, then source is started - -**publish_camera_health** -- Given a camera transitions from healthy to unhealthy, when health monitor runs, then `publish_camera_health(camera, False)` is called once -- Given a camera stays healthy across checks, when health monitor runs, then `publish_camera_health` is NOT called -- Given a camera is checked for the first time, when health monitor runs, then `publish_camera_health` is NOT called (no previous state) -- Given MultiplexNotifier with 3 notifiers and one fails, when `publish_camera_health` is called, then other notifiers still receive the call - -**HealthMonitorConfig** -- Given `health_monitor.enabled=False`, when Application starts, then no health monitor task is created -- Given `health_monitor.check_interval_s=10`, when Application runs, then health checks happen every 10 seconds +- Given two cameras (one enabled, one disabled), when Application starts, then only enabled camera source is created --- @@ -280,27 +116,18 @@ class Application: ```bash # Run unit tests -pytest tests/unit/test_app.py -v -k "health" -pytest tests/unit/models/test_config.py -v -k "enabled" -pytest tests/unit/notifiers/test_multiplex.py -v -k "camera_health" +make check # Verify CameraConfig accepts enabled field python -c "from homesec.models.config import CameraConfig, CameraSourceConfig; print(CameraConfig(name='test', enabled=False, source=CameraSourceConfig(backend='rtsp', config={})))" - -# Verify HealthMonitorConfig -python -c "from homesec.models.config import HealthMonitorConfig; print(HealthMonitorConfig(check_interval_s=60))" ``` --- ## Definition of Done -- [ ] `CameraConfig` has `enabled: bool = True` field -- [ ] `HealthMonitorConfig` exists with `enabled` and `check_interval_s` fields -- [ ] `Notifier` interface includes `publish_camera_health()` with default no-op -- [ ] `MultiplexNotifier` fans out `publish_camera_health()` to all notifiers -- [ ] Application skips starting sources where `enabled=False` -- [ ] Application monitors camera health and calls `publish_camera_health()` on transitions only -- [ ] Health monitoring respects configuration (enabled, interval) +- [x] `CameraConfig` has `enabled: bool = True` field +- [x] Application skips starting sources where `enabled=False` +- [x] `_sources_by_name` mapping maintained for future API use - [ ] All tests pass -- [ ] Existing functionality unchanged (backwards compatible) +- [x] Existing functionality unchanged (backwards compatible) diff --git a/docs/ha-phase-2-ha-notifier.md b/docs/ha-phase-2-ha-notifier.md index 10a8ae2a..5af3278f 100644 --- a/docs/ha-phase-2-ha-notifier.md +++ b/docs/ha-phase-2-ha-notifier.md @@ -1,6 +1,6 @@ # Phase 2: Home Assistant Notifier Plugin -**Goal**: Enable real-time event push from HomeSec to Home Assistant without requiring MQTT. +**Goal**: Enable real-time alert and clip event push from HomeSec to Home Assistant without requiring MQTT. **Estimated Effort**: 2-3 days @@ -12,6 +12,8 @@ This phase adds a new notifier plugin that pushes events directly to Home Assistant via the HA Events API. When running as an add-on, it uses `SUPERVISOR_TOKEN` for zero-config authentication. +**Note**: Camera health is **not** pushed via events. Health uses a pull-based architecture where the HA Integration (Phase 4) polls the REST API (Phase 1). This keeps the core simple. + --- ## 2.1 Configuration Model @@ -33,7 +35,7 @@ class HomeAssistantNotifierConfig(BaseModel): token_env: str | None = None # e.g., "HA_TOKEN" -> long-lived access token # Event configuration - event_prefix: str = "homesec" # Events: homesec_alert, homesec_camera_health, etc. + event_prefix: str = "homesec" # Events: homesec_alert, homesec_clip_recorded ``` ### Constraints @@ -90,14 +92,6 @@ class HomeAssistantNotifier(Notifier): """ ... - async def publish_camera_health(self, camera_name: str, healthy: bool) -> None: - """Send camera health event to Home Assistant. - - Event: homesec_camera_health - Data: {camera: str, healthy: bool, status: "healthy"|"unhealthy"} - """ - ... - async def publish_clip_recorded(self, clip_id: str, camera_name: str) -> None: """Send clip recorded event to Home Assistant. @@ -130,7 +124,7 @@ def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: - Use aiohttp for HTTP requests - Events API endpoint: `POST /api/events/{event_type}` - Supervisor URL: `http://supervisor/core/api/events/...` -- Event names: `{event_prefix}_alert`, `{event_prefix}_camera_health`, `{event_prefix}_clip_recorded` +- Event names: `{event_prefix}_alert`, `{event_prefix}_clip_recorded` --- @@ -154,18 +148,6 @@ Fired when an alert is generated after VLM analysis. } ``` -### homesec_camera_health - -Fired when camera health status changes. - -```json -{ - "camera": "front_door", - "healthy": false, - "status": "unhealthy" -} -``` - ### homesec_clip_recorded Fired when a new clip is recorded (before analysis). @@ -229,9 +211,6 @@ notifiers: - Given HA returns 401, when notify() called, then error logged but no exception raised - Given HA unreachable, when notify() called, then error logged but no exception raised -**Camera Health** -- Given notifier started, when publish_camera_health("front", False), then POST homesec_camera_health event with healthy=false - --- ## Verification @@ -256,7 +235,7 @@ pytest tests/unit/plugins/notifiers/test_home_assistant.py -v - [ ] Notifier auto-detects supervisor mode via `SUPERVISOR_TOKEN` - [ ] Zero-config works when running as HA add-on - [ ] Standalone mode requires `url_env` and `token_env` -- [ ] Events fired: `homesec_alert`, `homesec_camera_health`, `homesec_clip_recorded` +- [ ] Events fired: `homesec_alert`, `homesec_clip_recorded` - [ ] Notification failures don't crash the pipeline (best-effort) - [ ] Events contain all required metadata - [ ] Config example added diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index 3708f69f..fb592df9 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -206,7 +206,9 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def async_subscribe_events(self) -> None: """Subscribe to HomeSec events fired via HA Events API. - Events: homesec_alert, homesec_camera_health, homesec_clip_recorded + Events: homesec_alert, homesec_clip_recorded + + Note: Camera health is polled via REST API, not event-driven. """ ... diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index deae1114..ad7db343 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -101,11 +101,8 @@ These interfaces are defined across phases. See individual phase docs for detail - `count_clips_since(since: datetime) -> int` - `count_alerts_since(since: datetime) -> int` -### Notifier Extensions (Phase 0) -- `publish_camera_health(camera_name: str, healthy: bool) -> None` - -### ClipSource Extensions (Phase 0) -- `enabled: bool` property (respects CameraConfig.enabled) +### CameraConfig Extensions (Phase 0) +- `enabled: bool = True` field (allows disabling cameras via API) --- diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 8b6ed01b..456fbf00 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -113,9 +113,10 @@ homesec/ | Event | Trigger | Data | |-------|---------|------| | `homesec_alert` | Detection alert | camera, clip_id, activity_type, risk_level, summary, view_url | -| `homesec_camera_health` | Camera status change | camera, healthy, status | | `homesec_clip_recorded` | New clip recorded | clip_id, camera | +**Note**: Camera health is **not** pushed via events. The HA Integration polls the REST API (`GET /api/v1/cameras/{name}/status`) every 30-60s to get health status. This keeps the HomeSec core simple and stateless. + ### Future Features (v2+) - Custom Lovelace card with detection timeline @@ -709,11 +710,12 @@ class HomeAssistantNotifier(Notifier): **Event types fired:** - `homesec_alert` - Detection alert with full metadata -- `homesec_camera_health` - Camera health status changes - `homesec_clip_recorded` - New clip recorded The HA integration subscribes to these events and updates entities in real-time. +**Note**: Camera health is polled via REST API, not pushed via events. + --- ## Summary: Recommended Roadmap From 121a3b94383bafb51d5567d9941614dec851264f Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 20:16:33 -0800 Subject: [PATCH 14/31] docs: remove config_version optimistic concurrency for v1 Simplify API design by using last-write-wins semantics instead of optimistic concurrency. This is acceptable for v1 given the single HA instance assumption and restart-required model. Changes: - Remove config_version from ConfigManager interface - Remove config_version from API request/response models - Remove ConfigVersionConflict exception - Remove 409 Conflict handling - Update constraints and test cases Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 35 ++----------------- docs/ha-phase-4-ha-integration.md | 15 +------- docs/home-assistant-implementation-plan.md | 3 +- docs/home-assistant-integration-brainstorm.md | 4 +-- 4 files changed, 7 insertions(+), 50 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 3ee817c7..b5678b3a 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -89,19 +89,13 @@ async def verify_api_key(request: Request, app=Depends(get_homesec_app)) -> None ```python class ConfigUpdateResult(BaseModel): """Result of a config update operation.""" - config_version: int restart_required: bool = True class ConfigManager: - """Manages configuration persistence with optimistic concurrency.""" + """Manages configuration persistence (last-write-wins semantics).""" def __init__(self, base_paths: list[Path], override_path: Path): ... - @property - def config_version(self) -> int: - """Current config version for optimistic concurrency.""" - ... - def get_config(self) -> Config: """Get the current merged configuration.""" ... @@ -112,13 +106,11 @@ class ConfigManager: enabled: bool, source_backend: str, source_config: dict, - config_version: int, ) -> ConfigUpdateResult: """Add a new camera to the override config. Raises: ValueError: If camera name already exists - ConfigVersionConflict: If config_version is stale """ ... @@ -127,32 +119,24 @@ class ConfigManager: camera_name: str, enabled: bool | None, source_config: dict | None, - config_version: int, ) -> ConfigUpdateResult: """Update an existing camera in the override config. Raises: ValueError: If camera doesn't exist - ConfigVersionConflict: If config_version is stale """ ... async def remove_camera( self, camera_name: str, - config_version: int, ) -> ConfigUpdateResult: """Remove a camera from the override config. Raises: ValueError: If camera doesn't exist - ConfigVersionConflict: If config_version is stale """ ... - -class ConfigVersionConflict(Exception): - """Raised when config_version doesn't match current version.""" - pass ``` **ConfigLoader** (`loader.py`) @@ -164,8 +148,6 @@ def load_configs(paths: list[Path]) -> Config: - Files loaded left to right, rightmost wins - Dicts: deep merge (recursive) - Lists: merge (union, preserving order, no duplicates) - - The override file may contain a top-level `config_version` key. """ ... @@ -182,7 +164,6 @@ def deep_merge(base: dict, override: dict) -> dict: ### Constraints - Override file written atomically (write temp, fsync, rename) -- `config_version` stored in override file, incremented on each write - All file I/O via `asyncio.to_thread` - Base config is read-only; all mutations go to override file - Override file is machine-owned (no comment preservation needed) @@ -318,12 +299,10 @@ class CameraCreate(BaseModel): enabled: bool = True source_backend: str # rtsp, ftp, local_folder source_config: dict - config_version: int class CameraUpdate(BaseModel): enabled: bool | None = None source_config: dict | None = None - config_version: int class CameraResponse(BaseModel): name: str @@ -335,15 +314,13 @@ class CameraResponse(BaseModel): class ConfigChangeResponse(BaseModel): restart_required: bool = True - config_version: int camera: CameraResponse | None = None ``` ### Constraints - All config-mutating endpoints return `restart_required: True` -- All config-mutating endpoints require `config_version` for optimistic concurrency -- Return 409 Conflict when `config_version` is stale +- Last-write-wins semantics (no optimistic concurrency in v1) - Return 503 Service Unavailable when Postgres is down - Pagination: `page` (1-indexed), `page_size` (default 50, max 100) @@ -436,12 +413,7 @@ class FastAPIServerConfig(BaseModel): **Camera CRUD** - Given no cameras, when POST /cameras with valid data, then 201 and camera created - Given camera "front", when GET /cameras/front, then returns camera data -- Given camera "front", when DELETE /cameras/front with correct version, then 200 -- Given camera "front", when DELETE /cameras/front with stale version, then 409 Conflict - -**Config Version** -- Given config_version=5, when update with version=4, then 409 Conflict -- Given config_version=5, when update with version=5, then 200 and new version=6 +- Given camera "front", when DELETE /cameras/front, then 200 and camera removed **Health** - Given Postgres is up, when GET /health, then status="healthy" @@ -485,7 +457,6 @@ open http://localhost:8080/docs - [ ] All CRUD operations for cameras work - [ ] Config changes are validated and persisted to override file - [ ] Config changes return `restart_required: true` -- [ ] Stale `config_version` returns 409 Conflict - [ ] `/api/v1/system/restart` triggers graceful shutdown - [ ] Clip listing with pagination and filtering works - [ ] Event history API works diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index fb592df9..0c7e5876 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -181,11 +181,6 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): @property def base_url(self) -> str: ... - @property - def config_version(self) -> int: - """Current config version for optimistic concurrency.""" - ... - async def _async_update_data(self) -> dict[str, Any]: """Fetch data from HomeSec API. @@ -193,7 +188,6 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): { "health": {...}, "cameras": [...], - "config_version": int, "stats": {...}, "connected": bool, "motion_active": {camera: bool}, # Event-driven @@ -216,17 +210,12 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Unsubscribe from HomeSec events.""" ... - # API Methods (use config_version for optimistic concurrency) + # API Methods async def async_add_camera(self, name, source_backend, source_config) -> dict: ... async def async_update_camera(self, camera_name, source_config=None) -> dict: ... async def async_delete_camera(self, camera_name) -> None: ... async def async_set_camera_enabled(self, camera_name, enabled: bool) -> dict: ... async def async_test_camera(self, camera_name) -> dict: ... - - -class ConfigVersionConflict(Exception): - """Raised when config_version is stale (409 Conflict).""" - pass ``` ### Motion Timer Logic @@ -242,7 +231,6 @@ When `homesec_alert` event received: - Preserve `motion_active`, `latest_alerts`, `recent_alerts` across polling updates - Store last 20 alerts in `recent_alerts` (newest first) - Handle 503 gracefully (Postgres unavailable) -- Handle 409 by raising `ConfigVersionConflict` --- @@ -508,7 +496,6 @@ cp -r homeassistant/integration/custom_components/homesec ~/.homeassistant/custo - [ ] HA Events subscription triggers refresh on alerts - [ ] Motion sensor auto-resets after configurable timeout - [ ] Camera switch enables/disables via API -- [ ] Config version tracking prevents conflicts (409 handling) - [ ] Services work (add_camera, remove_camera) - [ ] Options flow allows reconfiguration - [ ] All strings translatable diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index ad7db343..6a491121 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -17,7 +17,7 @@ This document provides an overview of the Home Assistant integration for HomeSec - **P0 priority**: Recording + uploading must keep working even if Postgres is down - **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically - **No MQTT required**: Integration uses HA Events API directly. Existing MQTT notifier remains for Node-RED/other systems -- **409 Conflict UX**: Show error to user when config version is stale +- **Last write wins**: No optimistic concurrency in v1 (single HA instance assumption) - **API during Postgres outage**: Return 503 Service Unavailable - **Restart acceptable**: API writes validated config to disk and returns `restart_required`; HA can trigger restart @@ -91,7 +91,6 @@ These interfaces are defined across phases. See individual phase docs for detail - `update_camera(...) -> ConfigUpdateResult` - `add_camera(...) -> ConfigUpdateResult` - `remove_camera(...) -> ConfigUpdateResult` -- `config_version: int` (optimistic concurrency) ### ClipRepository Extensions (Phase 1) - `get_clip(clip_id) -> Clip | None` diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 456fbf00..b4c14170 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -27,7 +27,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug - P0 priority: recording + uploading must keep working even if Postgres is down (API and HA features are best-effort). - **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically; standalone users provide HA URL + token. - **No MQTT broker required**: Primary path uses HA Events API. Existing MQTT notifier remains available for Node-RED and other MQTT consumers. -- **409 Conflict UX**: Show error to user when config version is stale. +- **Last write wins**: No optimistic concurrency in v1 (single HA instance assumption). - **API during Postgres outage**: Return 503 Service Unavailable. ## Constraints and Non-Goals @@ -425,7 +425,7 @@ Config model for Option B: - Dicts deep-merge; lists merge (union). - Override file default: `config/ha-overrides.yaml` (configurable via CLI). - CLI accepts multiple `--config` flags; order matters. -- All config writes require `config_version` for optimistic concurrency. +- Config writes use last-write-wins semantics (no optimistic concurrency in v1). ```yaml # New endpoints From 56f9646d0db4379d3d1ac825ae9a1035a1ba0140 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 20:27:27 -0800 Subject: [PATCH 15/31] docs: remove recent_alerts sensor, add clip filters for alerts Remove in-memory recent_alerts storage from HA Integration: - HA's built-in logbook already records homesec_alert events - In-memory storage lost on restart - Complex attributes hard to use in dashboards Instead, enhance GET /api/v1/clips with filters: - alerted: bool - only clips that triggered notifications - risk_level: filter by risk level - activity_type: filter by activity type This allows v2 Lovelace cards to query the API directly. Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 10 ++++++ docs/ha-phase-4-ha-integration.md | 57 +++---------------------------- 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index b5678b3a..e2d5f858 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -194,6 +194,9 @@ class ClipRepository: *, camera: str | None = None, status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, since: datetime | None = None, until: datetime | None = None, offset: int = 0, @@ -201,6 +204,11 @@ class ClipRepository: ) -> tuple[list[ClipStateData], int]: """List clips with filtering and pagination. + Filters: + - alerted: If True, only clips that triggered notifications + - risk_level: Filter by analysis risk level (low/medium/high/critical) + - activity_type: Filter by detected activity type + Returns (clips, total_count). """ ... @@ -426,6 +434,8 @@ class FastAPIServerConfig(BaseModel): - Given 5 clips created today and 10 yesterday, when `count_clips_since(today_start)`, then returns 5 - Given 0 alerts, when `count_alerts_since(any_date)`, then returns 0 - Given clips with mixed cameras, when `list_clips(camera="front")`, then returns only "front" clips +- Given 10 clips (5 alerted, 5 not), when `list_clips(alerted=True)`, then returns only 5 alerted clips +- Given clips with mixed risk levels, when `list_clips(risk_level="high")`, then returns only high-risk clips - Given clip exists, when `delete_clip(clip_id)`, then clip marked deleted and storage files removed - Given StateStore is up, when `ping()`, then returns True diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index 0c7e5876..6ffe65c7 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -191,8 +191,7 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): "stats": {...}, "connected": bool, "motion_active": {camera: bool}, # Event-driven - "latest_alerts": {camera: {...}}, # Event-driven - "recent_alerts": [{...}, ...], # Last 20 alerts + "latest_alerts": {camera: {...}}, # Event-driven (for last_activity sensor) } """ ... @@ -228,8 +227,7 @@ When `homesec_alert` event received: ### Constraints -- Preserve `motion_active`, `latest_alerts`, `recent_alerts` across polling updates -- Store last 20 alerts in `recent_alerts` (newest first) +- Preserve `motion_active`, `latest_alerts` across polling updates - Handle 503 gracefully (Postgres unavailable) --- @@ -299,7 +297,6 @@ HomeSec Hub (identifiers: homesec_hub) - `alerts_today` - Alerts count today - `clips_today` - Clips count today - `system_health` - "healthy" / "degraded" / "unhealthy" -- `recent_alerts` - Special sensor with last 20 alerts in attributes **Camera Sensors** (HomesecCameraEntity): - `last_activity` - Activity type from latest alert @@ -320,52 +317,7 @@ HomeSec Hub (identifiers: homesec_hub) --- -## 4.8 Recent Alerts Sensor - -**Special sensor for dashboard filtering.** - -```python -class HomesecRecentAlertsSensor(HomesecHubEntity, SensorEntity): - """Sensor showing recent alerts with full metadata for filtering.""" - - @property - def native_value(self) -> int: - """Return count of alerts today.""" - ... - - @property - def extra_state_attributes(self) -> dict: - """Return recent alerts as attributes for dashboard filtering. - - Returns: - { - "alerts": [...], # List of alert dicts - "alerts_count": int, - "last_alert_at": str | None, - "by_risk_level": {"low": 2, "medium": 1, ...}, - "by_camera": {"front_door": 2, ...}, - "by_activity_type": {"person_at_door": 1, ...}, - } - """ - ... -``` - -### Dashboard Usage - -```yaml -# Example Lovelace template -{% for alert in state_attr('sensor.homesec_recent_alerts', 'alerts') %} - {% if alert.risk_level in ['high', 'critical'] %} - - Camera: {{ alert.camera }} - Activity: {{ alert.activity_type }} - Time: {{ alert.timestamp }} - {% endif %} -{% endfor %} -``` - ---- - -## 4.9 Services +## 4.8 Services **File**: `services.yaml` @@ -399,7 +351,7 @@ remove_camera: --- -## 4.10 Translations +## 4.9 Translations **File**: `translations/en.json` @@ -491,7 +443,6 @@ cp -r homeassistant/integration/custom_components/homesec ~/.homeassistant/custo - [ ] All entity platforms create entities correctly - [ ] Hub device created with system-wide sensors - [ ] Camera devices created with via_device to hub -- [ ] Recent Alerts sensor stores last 20 alerts with filter metadata - [ ] DataUpdateCoordinator fetches at correct intervals - [ ] HA Events subscription triggers refresh on alerts - [ ] Motion sensor auto-resets after configurable timeout From 225bcc7f744e1c176bf07e0cb3c693778a030a37 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 20:37:47 -0800 Subject: [PATCH 16/31] docs: simplify HA integration plan for v1 Consolidations: - Merge Phase 0 into Phase 1 (just CameraConfig.enabled field) - Remove /api/v1/events endpoint (defer to v2) - Remove /api/v1/clips/{id}/reprocess (defer to v2) - Remove homesec_clip_recorded event (defer to v2) - Remove HA Integration services (defer to v2) - Simplify options flow (just scan_interval, motion_reset_seconds) v1 focus: minimal viable HA integration with: - REST API for camera CRUD and clip queries - Single homesec_alert event for real-time notifications - Pull-based health monitoring - Last-write-wins config semantics Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-0-prerequisites.md | 134 +----------------- docs/ha-phase-1-rest-api.md | 59 ++++---- docs/ha-phase-2-ha-notifier.md | 27 +--- docs/ha-phase-4-ha-integration.md | 52 +------ docs/ha-phase-5-advanced.md | 2 +- docs/home-assistant-implementation-plan.md | 18 +-- docs/home-assistant-integration-brainstorm.md | 30 +--- 7 files changed, 54 insertions(+), 268 deletions(-) diff --git a/docs/ha-phase-0-prerequisites.md b/docs/ha-phase-0-prerequisites.md index 96345ef4..28742006 100644 --- a/docs/ha-phase-0-prerequisites.md +++ b/docs/ha-phase-0-prerequisites.md @@ -1,133 +1,7 @@ # Phase 0: Core Prerequisites -**Goal**: Prepare the HomeSec core codebase for HA integration by adding the `enabled` field to CameraConfig. +> **Note**: This phase has been merged into [Phase 1 (REST API)](./ha-phase-1-rest-api.md). +> +> The only prerequisite (`CameraConfig.enabled` field) is now part of Phase 1, section 1.1.1. -**Estimated Effort**: 0.5 days - -**Dependencies**: None - ---- - -## Overview - -Before implementing the HA integration, one small change is needed in the core HomeSec codebase: - -1. Add `enabled` field to `CameraConfig` - -**Note**: Camera health monitoring uses a **pull-based** architecture. The HA Integration (Phase 4) will poll the REST API (Phase 1) to get camera health status. No push-based health monitoring is needed in the core. - ---- - -## 0.1 Add `enabled` Field to CameraConfig - -**File**: `src/homesec/models/config.py` - -### Interface - -```python -class CameraConfig(BaseModel): - """Camera configuration and clip source selection.""" - - name: str - enabled: bool = True # Allow disabling camera via API - source: CameraSourceConfig -``` - -### Constraints - -- Default is `True` (backwards compatible) -- When `enabled=False`, Application must not start the source -- API can toggle this field; requires restart to take effect - ---- - -## 0.2 Respect `enabled` in Application - -**File**: `src/homesec/app.py` - -### Interface - -```python -def _create_sources(self, config: Config) -> list[ClipSource]: - """Create clip sources based on config using plugin registry. - - Respects CameraConfig.enabled - skips disabled cameras. - """ - sources: list[ClipSource] = [] - - for camera in config.cameras: - if not camera.enabled: - logger.info("Skipping disabled camera: %s", camera.name) - continue - - source_cfg = camera.source - source = load_source_plugin( - source_backend=source_cfg.backend, - config=source_cfg.config, - camera_name=camera.name, - ) - sources.append(source) - self._sources_by_name[camera.name] = source - - return sources -``` - -### Constraints - -- Skip sources where `enabled=False` -- Log which cameras are skipped -- Maintain `_sources_by_name` mapping for API queries (Phase 1) - ---- - -## File Changes Summary - -| File | Change | -|------|--------| -| `src/homesec/models/config.py` | Add `enabled: bool = True` to `CameraConfig` | -| `src/homesec/app.py` | Skip disabled cameras, add `_sources_by_name` mapping | - ---- - -## Health Monitoring Architecture Note - -Camera health uses a **pull-based** architecture: - -1. **ClipSource** already has `is_healthy()` method -2. **Phase 1 REST API** exposes `GET /api/v1/cameras/{name}/status` (includes health) -3. **Phase 4 HA Integration** polls that endpoint every 30-60s via DataUpdateCoordinator - -This keeps the HomeSec core simple and stateless. The HA Integration handles converting pull to entity updates. - ---- - -## Test Expectations - -### Test Cases - -**CameraConfig.enabled** -- Given a config with `enabled=False`, when Application starts, then source is not started -- Given a config with `enabled=True` (or missing), when Application starts, then source is started -- Given two cameras (one enabled, one disabled), when Application starts, then only enabled camera source is created - ---- - -## Verification - -```bash -# Run unit tests -make check - -# Verify CameraConfig accepts enabled field -python -c "from homesec.models.config import CameraConfig, CameraSourceConfig; print(CameraConfig(name='test', enabled=False, source=CameraSourceConfig(backend='rtsp', config={})))" -``` - ---- - -## Definition of Done - -- [x] `CameraConfig` has `enabled: bool = True` field -- [x] Application skips starting sources where `enabled=False` -- [x] `_sources_by_name` mapping maintained for future API use -- [ ] All tests pass -- [x] Existing functionality unchanged (backwards compatible) +See [Phase 1](./ha-phase-1-rest-api.md) for implementation details. diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index e2d5f858..5d946833 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -4,18 +4,17 @@ **Estimated Effort**: 5-7 days -**Dependencies**: Phase 0 (Prerequisites) +**Dependencies**: None --- ## Overview This phase adds a FastAPI-based REST API to HomeSec for: -- Camera CRUD operations -- Clip listing and management -- Event history +- Camera CRUD operations (including `enabled` field for toggling cameras) +- Clip listing with filters (camera, status, alerted, risk_level, activity_type) - System stats and health -- Configuration management with optimistic concurrency +- Configuration management (last-write-wins) --- @@ -76,6 +75,28 @@ async def verify_api_key(request: Request, app=Depends(get_homesec_app)) -> None --- +## 1.1.1 CameraConfig.enabled Field + +**File**: `src/homesec/models/config.py` + +Add `enabled` field to allow toggling cameras via API: + +```python +class CameraConfig(BaseModel): + """Camera configuration and clip source selection.""" + + name: str + enabled: bool = True # Allow disabling camera via API + source: CameraSourceConfig +``` + +**Constraints:** +- Default is `True` (backwards compatible) +- When `enabled=False`, Application skips starting the source +- API can toggle this field; requires restart to take effect + +--- + ## 1.2 Config Management ### Files @@ -213,22 +234,6 @@ class ClipRepository: """ ... - async def list_events( - self, - *, - clip_id: str | None = None, - event_type: str | None = None, - camera: str | None = None, - since: datetime | None = None, - until: datetime | None = None, - limit: int = 100, - ) -> tuple[list[ClipLifecycleEvent], int]: - """List events with filtering. - - Returns (events, total_count). - """ - ... - async def delete_clip(self, clip_id: str) -> None: """Mark clip as deleted and delete from storage. @@ -258,7 +263,7 @@ class ClipRepository: - Must use async SQLAlchemy - Counts should be efficient (use SQL COUNT, not fetch all) - `count_alerts_since` counts events where `event_type='notification_sent'` -- `list_clips` and `list_events` return tuple of (items, total_count) for pagination +- `list_clips` returns tuple of (items, total_count) for pagination - `delete_clip` should delete from both local and cloud storage --- @@ -270,7 +275,6 @@ class ClipRepository: - `src/homesec/api/routes/__init__.py` - `src/homesec/api/routes/cameras.py` - `src/homesec/api/routes/clips.py` -- `src/homesec/api/routes/events.py` - `src/homesec/api/routes/stats.py` - `src/homesec/api/routes/health.py` - `src/homesec/api/routes/config.py` @@ -281,19 +285,17 @@ class ClipRepository: | Method | Path | Description | |--------|------|-------------| | GET | `/api/v1/health` | Health check | -| GET | `/api/v1/config` | Config summary and version | +| GET | `/api/v1/config` | Config summary | | GET | `/api/v1/cameras` | List cameras | | GET | `/api/v1/cameras/{name}` | Get camera | | POST | `/api/v1/cameras` | Create camera | | PUT | `/api/v1/cameras/{name}` | Update camera | | DELETE | `/api/v1/cameras/{name}` | Delete camera | -| GET | `/api/v1/cameras/{name}/status` | Camera status | +| GET | `/api/v1/cameras/{name}/status` | Camera status (includes health) | | POST | `/api/v1/cameras/{name}/test` | Test camera connection | -| GET | `/api/v1/clips` | List clips (paginated, filterable) | +| GET | `/api/v1/clips` | List clips (filterable: camera, status, alerted, risk_level, activity_type) | | GET | `/api/v1/clips/{id}` | Get clip | | DELETE | `/api/v1/clips/{id}` | Delete clip | -| POST | `/api/v1/clips/{id}/reprocess` | Reprocess clip | -| GET | `/api/v1/events` | List events (filterable) | | GET | `/api/v1/stats` | System statistics | | POST | `/api/v1/system/restart` | Request graceful restart | | GET | `/api/v1/system/health/detailed` | Detailed health with error codes | @@ -469,7 +471,6 @@ open http://localhost:8080/docs - [ ] Config changes return `restart_required: true` - [ ] `/api/v1/system/restart` triggers graceful shutdown - [ ] Clip listing with pagination and filtering works -- [ ] Event history API works - [ ] Stats endpoint returns correct counts - [ ] OpenAPI documentation is accurate at `/docs` - [ ] CORS works for configured origins diff --git a/docs/ha-phase-2-ha-notifier.md b/docs/ha-phase-2-ha-notifier.md index 5af3278f..9a1dcd3f 100644 --- a/docs/ha-phase-2-ha-notifier.md +++ b/docs/ha-phase-2-ha-notifier.md @@ -1,6 +1,6 @@ # Phase 2: Home Assistant Notifier Plugin -**Goal**: Enable real-time alert and clip event push from HomeSec to Home Assistant without requiring MQTT. +**Goal**: Enable real-time alert push from HomeSec to Home Assistant without requiring MQTT. **Estimated Effort**: 2-3 days @@ -35,7 +35,7 @@ class HomeAssistantNotifierConfig(BaseModel): token_env: str | None = None # e.g., "HA_TOKEN" -> long-lived access token # Event configuration - event_prefix: str = "homesec" # Events: homesec_alert, homesec_clip_recorded + event_prefix: str = "homesec" # Event: homesec_alert ``` ### Constraints @@ -91,14 +91,6 @@ class HomeAssistantNotifier(Notifier): - detected_objects: list[str] (if analysis present) """ ... - - async def publish_clip_recorded(self, clip_id: str, camera_name: str) -> None: - """Send clip recorded event to Home Assistant. - - Event: homesec_clip_recorded - Data: {clip_id: str, camera: str} - """ - ... ``` ### Internal Methods @@ -124,7 +116,7 @@ def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: - Use aiohttp for HTTP requests - Events API endpoint: `POST /api/events/{event_type}` - Supervisor URL: `http://supervisor/core/api/events/...` -- Event names: `{event_prefix}_alert`, `{event_prefix}_clip_recorded` +- Event name: `{event_prefix}_alert` --- @@ -148,17 +140,6 @@ Fired when an alert is generated after VLM analysis. } ``` -### homesec_clip_recorded - -Fired when a new clip is recorded (before analysis). - -```json -{ - "clip_id": "abc123", - "camera": "front_door" -} -``` - --- ## 2.4 Configuration Example @@ -235,7 +216,7 @@ pytest tests/unit/plugins/notifiers/test_home_assistant.py -v - [ ] Notifier auto-detects supervisor mode via `SUPERVISOR_TOKEN` - [ ] Zero-config works when running as HA add-on - [ ] Standalone mode requires `url_env` and `token_env` -- [ ] Events fired: `homesec_alert`, `homesec_clip_recorded` +- [ ] Event fired: `homesec_alert` - [ ] Notification failures don't crash the pipeline (best-effort) - [ ] Events contain all required metadata - [ ] Config example added diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index 6ffe65c7..7974c962 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -15,7 +15,6 @@ This phase creates a custom Home Assistant integration that: - Creates entities for cameras (sensors, binary sensors, switches) - Creates hub-level entities (system health, alerts today, clips today) - Subscribes to HomeSec events for real-time updates -- Provides services for camera management --- @@ -35,7 +34,6 @@ homeassistant/integration/custom_components/homesec/ ├── binary_sensor.py # Binary sensor platform ├── switch.py # Switch platform ├── diagnostics.py # Diagnostic data -├── services.yaml # Service definitions ├── strings.json # UI strings └── translations/ └── en.json # English translations @@ -133,7 +131,7 @@ class OptionsFlowHandler(OptionsFlow): """Handle options flow for HomeSec.""" async def async_step_init(self, user_input=None) -> FlowResult: - """Manage options: cameras, scan_interval, motion_reset_seconds.""" + """Manage options: scan_interval, motion_reset_seconds.""" ... @@ -161,7 +159,7 @@ async def detect_addon(hass) -> bool: - Store `addon: bool` in config entry data to track mode - Unique ID: `homesec_{host}_{port}` -- Options: cameras list, scan_interval, motion_reset_seconds +- Options: scan_interval, motion_reset_seconds --- @@ -199,7 +197,7 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def async_subscribe_events(self) -> None: """Subscribe to HomeSec events fired via HA Events API. - Events: homesec_alert, homesec_clip_recorded + Event: homesec_alert Note: Camera health is polled via REST API, not event-driven. """ @@ -317,41 +315,7 @@ HomeSec Hub (identifiers: homesec_hub) --- -## 4.8 Services - -**File**: `services.yaml` - -```yaml -add_camera: - name: Add Camera - description: Add a new camera to HomeSec - fields: - name: - required: true - selector: - text: - source_backend: - required: true - selector: - select: - options: ["rtsp", "ftp", "local_folder"] - rtsp_url: - selector: - text: - -remove_camera: - name: Remove Camera - description: Remove a camera from HomeSec - target: - device: - integration: homesec -``` - -**Note**: Service handlers to be implemented in `__init__.py`. - ---- - -## 4.9 Translations +## 4.8 Translations **File**: `translations/en.json` @@ -360,7 +324,6 @@ Provides translations for: - Error messages (cannot_connect, invalid_auth) - Options flow - Entity names -- Service names --- @@ -369,7 +332,7 @@ Provides translations for: | File | Change | |------|--------| | `homeassistant/integration/hacs.json` | HACS configuration | -| `custom_components/homesec/__init__.py` | Setup entry, services | +| `custom_components/homesec/__init__.py` | Setup entry | | `custom_components/homesec/manifest.json` | Integration metadata | | `custom_components/homesec/const.py` | Constants | | `custom_components/homesec/config_flow.py` | Config + options flow | @@ -379,7 +342,6 @@ Provides translations for: | `custom_components/homesec/binary_sensor.py` | Binary sensor platform | | `custom_components/homesec/switch.py` | Switch platform | | `custom_components/homesec/diagnostics.py` | Diagnostic data | -| `custom_components/homesec/services.yaml` | Service definitions | | `custom_components/homesec/strings.json` | UI strings | | `custom_components/homesec/translations/en.json` | English translations | @@ -413,9 +375,6 @@ Provides translations for: - Given alert for "front", when check sensor.front_last_activity, then shows activity_type - Given switch turned off, when toggle, then API called with enabled=False -**Services** -- Given add_camera service called, when valid data, then API POST /cameras called - --- ## Verification @@ -447,7 +406,6 @@ cp -r homeassistant/integration/custom_components/homesec ~/.homeassistant/custo - [ ] HA Events subscription triggers refresh on alerts - [ ] Motion sensor auto-resets after configurable timeout - [ ] Camera switch enables/disables via API -- [ ] Services work (add_camera, remove_camera) - [ ] Options flow allows reconfiguration - [ ] All strings translatable - [ ] HACS installation works diff --git a/docs/ha-phase-5-advanced.md b/docs/ha-phase-5-advanced.md index 9bdce7ca..c9186a48 100644 --- a/docs/ha-phase-5-advanced.md +++ b/docs/ha-phase-5-advanced.md @@ -60,7 +60,7 @@ Custom panel for viewing event history with timeline visualization. ### Constraints -- Requires additional API endpoint or uses existing `/api/v1/events` +- Can use `/api/v1/clips?alerted=true` for alert history - May require frontend build tooling (Lit, etc.) --- diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index 6a491121..116a5886 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -27,17 +27,18 @@ This document provides an overview of the Home Assistant integration for HomeSec | Phase | Document | Dependencies | Estimated Effort | |-------|----------|--------------|------------------| -| 0 | [Prerequisites](./ha-phase-0-prerequisites.md) | None | 2-3 days | -| 1 | [REST API](./ha-phase-1-rest-api.md) | Phase 0 | 5-7 days | +| 1 | [REST API](./ha-phase-1-rest-api.md) | None | 5-7 days | | 2 | [HA Notifier](./ha-phase-2-ha-notifier.md) | None | 2-3 days | | 3 | [Add-on](./ha-phase-3-addon.md) | Phase 1, 2 | 3-4 days | | 4 | [HA Integration](./ha-phase-4-ha-integration.md) | Phase 1, 2 | 7-10 days | -| 5 | [Advanced Features](./ha-phase-5-advanced.md) | Phase 4 | 5-7 days | +| 5 | [Advanced Features](./ha-phase-5-advanced.md) | Phase 4 | TBD | -**Total: 24-34 days** +**Total: 17-24 days** (v1 core) **Parallel work possible**: Phase 1 and Phase 2 can be done in parallel. +**Note**: Phase 0 (Prerequisites) was merged into Phase 1. Phase 5 scope TBD. + --- ## Repository Structure @@ -86,6 +87,9 @@ homesec/ These interfaces are defined across phases. See individual phase docs for details. +### CameraConfig (Phase 1) +- `enabled: bool = True` field (allows disabling cameras via API) + ### ConfigManager (Phase 1) - `get_config() -> Config` - `update_camera(...) -> ConfigUpdateResult` @@ -94,15 +98,11 @@ These interfaces are defined across phases. See individual phase docs for detail ### ClipRepository Extensions (Phase 1) - `get_clip(clip_id) -> Clip | None` -- `list_clips(...) -> tuple[list[Clip], int]` -- `list_events(...) -> tuple[list[Event], int]` +- `list_clips(...) -> tuple[list[Clip], int]` (supports `alerted`, `risk_level`, `activity_type` filters) - `delete_clip(clip_id) -> None` - `count_clips_since(since: datetime) -> int` - `count_alerts_since(since: datetime) -> int` -### CameraConfig Extensions (Phase 0) -- `enabled: bool = True` field (allows disabling cameras via API) - --- ## Migration Guide diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index b4c14170..d0c70b57 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -113,8 +113,6 @@ homesec/ | Event | Trigger | Data | |-------|---------|------| | `homesec_alert` | Detection alert | camera, clip_id, activity_type, risk_level, summary, view_url | -| `homesec_clip_recorded` | New clip recorded | clip_id, camera | - **Note**: Camera health is **not** pushed via events. The HA Integration polls the REST API (`GET /api/v1/cameras/{name}/status`) every 30-60s to get health status. This keeps the HomeSec core simple and stateless. ### Future Features (v2+) @@ -439,7 +437,6 @@ GET /api/v1/cameras/{name}/status # Camera status POST /api/v1/cameras/{name}/test # Test camera connection GET /api/v1/clips # List recent clips GET /api/v1/clips/{id} # Get clip details -GET /api/v1/events # Event history POST /api/v1/system/restart # Request graceful restart ``` @@ -498,7 +495,6 @@ homeassistant/integration/ ├── sensor.py # Sensor entities ├── binary_sensor.py # Motion sensors (30s auto-reset timer) ├── switch.py # Enable/disable cameras (stops RTSP) - ├── services.yaml # Service definitions ├── strings.json # UI strings └── translations/ └── en.json @@ -514,29 +510,6 @@ homeassistant/integration/ | `image` | Per-camera | Last snapshot | v2 | | `select` | `alert_sensitivity` | LOW/MEDIUM/HIGH | v2 | -**Services (v1):** - -```yaml -homesec.add_camera: - description: Add a new camera - fields: - name: Camera identifier - source_backend: rtsp, ftp, or local_folder - rtsp_url: RTSP stream URL (for rtsp backend) - -homesec.remove_camera: - description: Remove a camera - target: - device: Camera device -``` - -**Services (v2 - Future):** - -```yaml -# homesec.set_alert_policy - requires new API endpoint -# homesec.test_camera - requires new API endpoint -``` - **Config Flow:** ``` @@ -710,9 +683,8 @@ class HomeAssistantNotifier(Notifier): **Event types fired:** - `homesec_alert` - Detection alert with full metadata -- `homesec_clip_recorded` - New clip recorded -The HA integration subscribes to these events and updates entities in real-time. +The HA integration subscribes to this event and updates entities in real-time. **Note**: Camera health is polled via REST API, not pushed via events. From dabeca148c14b5374b4101645c0640c7a9fd4b5f Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 20:46:00 -0800 Subject: [PATCH 17/31] docs: defer camera test endpoint to v2 Remove POST /api/v1/cameras/{name}/test - health polling provides the same information reactively within one poll cycle. Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 1 - docs/ha-phase-4-ha-integration.md | 1 - docs/home-assistant-integration-brainstorm.md | 1 - 3 files changed, 3 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 5d946833..6d9f2ef6 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -292,7 +292,6 @@ class ClipRepository: | PUT | `/api/v1/cameras/{name}` | Update camera | | DELETE | `/api/v1/cameras/{name}` | Delete camera | | GET | `/api/v1/cameras/{name}/status` | Camera status (includes health) | -| POST | `/api/v1/cameras/{name}/test` | Test camera connection | | GET | `/api/v1/clips` | List clips (filterable: camera, status, alerted, risk_level, activity_type) | | GET | `/api/v1/clips/{id}` | Get clip | | DELETE | `/api/v1/clips/{id}` | Delete clip | diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index 7974c962..990abe2a 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -212,7 +212,6 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def async_update_camera(self, camera_name, source_config=None) -> dict: ... async def async_delete_camera(self, camera_name) -> None: ... async def async_set_camera_enabled(self, camera_name, enabled: bool) -> dict: ... - async def async_test_camera(self, camera_name) -> dict: ... ``` ### Motion Timer Logic diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index d0c70b57..1e082262 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -434,7 +434,6 @@ POST /api/v1/cameras # Add camera PUT /api/v1/cameras/{name} # Update camera DELETE /api/v1/cameras/{name} # Remove camera GET /api/v1/cameras/{name}/status # Camera status -POST /api/v1/cameras/{name}/test # Test camera connection GET /api/v1/clips # List recent clips GET /api/v1/clips/{id} # Get clip details POST /api/v1/system/restart # Request graceful restart From fb0c3f8d2db2d3ad52584225c0b192070b4ea64d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 04:50:15 +0000 Subject: [PATCH 18/31] docs: fix entity inconsistency and clarify list merge semantics - Replace `person` binary sensor with `online` in brainstorm to match Phase 4 - Clarify list merge semantics: lists with `name` field merge by key - Items with same name are merged (override fields win) - Items only in base are preserved - Items only in override are added - Add merge_named_lists() interface to Phase 1 https://claude.ai/code/session_019LEwJ9ARyfpJqZMqTPpZxn --- docs/ha-phase-1-rest-api.md | 22 +++++++++++++++++-- docs/home-assistant-implementation-plan.md | 2 +- docs/home-assistant-integration-brainstorm.md | 8 +++---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 6d9f2ef6..232dc418 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -168,7 +168,11 @@ def load_configs(paths: list[Path]) -> Config: Merge semantics: - Files loaded left to right, rightmost wins - Dicts: deep merge (recursive) - - Lists: merge (union, preserving order, no duplicates) + - Lists of objects with `name` field: key-based merge by `name` + - Items with same `name` are merged (override fields win) + - Items only in base are preserved + - Items only in override are added + - Other lists: override replaces base entirely """ ... @@ -176,10 +180,24 @@ def deep_merge(base: dict, override: dict) -> dict: """Deep merge two dicts. - Nested dicts are merged recursively - - Lists are merged (union) + - Lists of dicts with 'name' key: merge by name (see merge_named_lists) + - Other lists: override replaces base - Scalars: override wins """ ... + +def merge_named_lists(base: list[dict], override: list[dict]) -> list[dict]: + """Merge two lists of dicts by 'name' key. + + Example: + base: [{name: "a", x: 1}, {name: "b", x: 2}] + override: [{name: "b", x: 99}, {name: "c", x: 3}] + result: [{name: "a", x: 1}, {name: "b", x: 99}, {name: "c", x: 3}] + + - Preserves order: base items first (in order), then new override items + - Override items with matching name replace/merge with base items + """ + ... ``` ### Constraints diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index 116a5886..f48e112a 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -9,7 +9,7 @@ This document provides an overview of the Home Assistant integration for HomeSec - **Approach**: Add-on + native integration with HomeSec as the runtime - **API stack**: FastAPI, async endpoints only, async SQLAlchemy only - **Config storage**: Override YAML file is source of truth for dynamic config. Base YAML is bootstrap-only. -- **Config merge**: Multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge (recursive), lists merge (union). +- **Config merge**: Multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge (recursive), lists with `name` field merge by key. - **Single instance**: HA integration assumes one HomeSec instance (`single_config_entry`) - **Secrets**: Never stored in HomeSec config; only env var names are persisted - **Repository pattern**: API reads/writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access) diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 1e082262..b1f6610c 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -19,7 +19,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug - API stack: FastAPI, async endpoints only, async SQLAlchemy only. - Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA may trigger restart. - Config storage: **Override YAML file** is source of truth for dynamic config. Base YAML is bootstrap-only. -- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists merge (union). +- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists with `name` field merge by key. - Single instance: HA integration assumes one HomeSec instance (`single_config_entry`). - Secrets: never stored in HomeSec; config stores env var names; HA/add-on passes env vars at boot. - Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). @@ -94,7 +94,7 @@ homesec/ | Entity | Type | Description | |--------|------|-------------| | `binary_sensor.homesec_{camera}_motion` | Binary Sensor | Motion detected (on for 30s after alert) | -| `binary_sensor.homesec_{camera}_person` | Binary Sensor | Person detected | +| `binary_sensor.homesec_{camera}_online` | Binary Sensor | Camera connectivity | | `sensor.homesec_{camera}_last_activity` | Sensor | Last activity type (person, vehicle, package, etc.) | | `sensor.homesec_{camera}_risk_level` | Sensor | Risk level (LOW/MEDIUM/HIGH/CRITICAL) | | `sensor.homesec_{camera}_health` | Sensor | healthy/unhealthy | @@ -420,7 +420,7 @@ Config model for Option B: - Base YAML is bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). - API writes a machine-owned **override YAML** file for all dynamic config. - HomeSec loads multiple YAML files left → right; rightmost wins. -- Dicts deep-merge; lists merge (union). +- Dicts deep-merge; lists with `name` field merge by key. - Override file default: `config/ha-overrides.yaml` (configurable via CLI). - CLI accepts multiple `--config` flags; order matters. - Config writes use last-write-wins semantics (no optimistic concurrency in v1). @@ -617,7 +617,7 @@ async def async_get_config_entry_diagnostics(hass, entry): - **Base YAML**: bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). - **Override YAML**: machine-owned, fully managed by HA via API. - **Load order**: multiple YAML files loaded left → right; rightmost wins. -- **Merge semantics**: dicts deep-merge; lists merge (union). +- **Merge semantics**: dicts deep-merge; lists with `name` field merge by key (e.g., cameras merge by camera name). - **Restart**: required for all config changes. --- From 50d5f7e9697bead6e8d6d70b3b18bce6134e0c01 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 20:59:24 -0800 Subject: [PATCH 19/31] docs: clean up API endpoint clarity - Merge /cameras/{name}/status into /cameras/{name} (returns both config + runtime) - Rename /api/v1/system/health/detailed to /api/v1/diagnostics Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 5 ++--- docs/home-assistant-integration-brainstorm.md | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 232dc418..327b78fc 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -305,17 +305,16 @@ class ClipRepository: | GET | `/api/v1/health` | Health check | | GET | `/api/v1/config` | Config summary | | GET | `/api/v1/cameras` | List cameras | -| GET | `/api/v1/cameras/{name}` | Get camera | +| GET | `/api/v1/cameras/{name}` | Get camera (config + runtime status) | | POST | `/api/v1/cameras` | Create camera | | PUT | `/api/v1/cameras/{name}` | Update camera | | DELETE | `/api/v1/cameras/{name}` | Delete camera | -| GET | `/api/v1/cameras/{name}/status` | Camera status (includes health) | | GET | `/api/v1/clips` | List clips (filterable: camera, status, alerted, risk_level, activity_type) | | GET | `/api/v1/clips/{id}` | Get clip | | DELETE | `/api/v1/clips/{id}` | Delete clip | | GET | `/api/v1/stats` | System statistics | | POST | `/api/v1/system/restart` | Request graceful restart | -| GET | `/api/v1/system/health/detailed` | Detailed health with error codes | +| GET | `/api/v1/diagnostics` | Detailed health with error codes | ### Request/Response Models diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index b1f6610c..5e616cbd 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -113,7 +113,7 @@ homesec/ | Event | Trigger | Data | |-------|---------|------| | `homesec_alert` | Detection alert | camera, clip_id, activity_type, risk_level, summary, view_url | -**Note**: Camera health is **not** pushed via events. The HA Integration polls the REST API (`GET /api/v1/cameras/{name}/status`) every 30-60s to get health status. This keeps the HomeSec core simple and stateless. +**Note**: Camera health is **not** pushed via events. The HA Integration polls the REST API (`GET /api/v1/cameras/{name}`) every 30-60s to get health status. This keeps the HomeSec core simple and stateless. ### Future Features (v2+) @@ -433,7 +433,6 @@ GET /api/v1/cameras # List cameras POST /api/v1/cameras # Add camera PUT /api/v1/cameras/{name} # Update camera DELETE /api/v1/cameras/{name} # Remove camera -GET /api/v1/cameras/{name}/status # Camera status GET /api/v1/clips # List recent clips GET /api/v1/clips/{id} # Get clip details POST /api/v1/system/restart # Request graceful restart From 316524e061bc0a0e7d875f18647f6efc613df550 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 21:05:19 -0800 Subject: [PATCH 20/31] docs: clarify loader.py is extended, not replaced Add note that existing load_config(path) stays unchanged; new multi-file functions are added alongside it. Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 327b78fc..d21a8899 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -161,6 +161,10 @@ class ConfigManager: ``` **ConfigLoader** (`loader.py`) + +> **Note**: This extends the existing `loader.py`. The current `load_config(path)` function +> remains unchanged. We add `load_configs()`, `deep_merge()`, and `merge_named_lists()` alongside it. + ```python def load_configs(paths: list[Path]) -> Config: """Load and merge multiple YAML config files. From 13d69153252d5c3952e442ac16d4e6ac8b45c45a Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 21:34:10 -0800 Subject: [PATCH 21/31] docs: address LLM review feedback - simplify architecture Changes based on external review: 1. Health endpoint: Return 200 if pipeline running (even if Postgres unavailable). Include postgres status in payload. Prevents restart loops when only DB is down. 2. Remove config merge mechanics: Single config file with backup instead of base + override merge. Deletes just work. Removes deep_merge, merge_named_lists, multi-file loading complexity. 3. Clarify secret handling: Add-on maps options to env vars, config stores *_env names (existing pattern, now explicit in docs). 4. Motion sensors: Clarify they're alert-based (triggered by homesec_alert), not raw motion detection. Intentional behavior. 5. Fix delete_clip boundary: Repository only marks deleted in DB, returns clip data. API route coordinates storage deletion. Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 138 +++++++----------- docs/ha-phase-3-addon.md | 16 +- docs/ha-phase-4-ha-integration.md | 6 +- docs/home-assistant-implementation-plan.md | 8 +- docs/home-assistant-integration-brainstorm.md | 25 ++-- 5 files changed, 78 insertions(+), 115 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index d21a8899..0b1922b8 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -102,7 +102,6 @@ class CameraConfig(BaseModel): ### Files - `src/homesec/config/manager.py` -- `src/homesec/config/loader.py` ### Interfaces @@ -113,12 +112,15 @@ class ConfigUpdateResult(BaseModel): restart_required: bool = True class ConfigManager: - """Manages configuration persistence (last-write-wins semantics).""" + """Manages configuration persistence (single file, last-write-wins). - def __init__(self, base_paths: list[Path], override_path: Path): ... + On mutations, backs up current config to {path}.bak before overwriting. + """ + + def __init__(self, config_path: Path): ... def get_config(self) -> Config: - """Get the current merged configuration.""" + """Get the current configuration.""" ... async def add_camera( @@ -128,7 +130,7 @@ class ConfigManager: source_backend: str, source_config: dict, ) -> ConfigUpdateResult: - """Add a new camera to the override config. + """Add a new camera to the config. Raises: ValueError: If camera name already exists @@ -141,7 +143,7 @@ class ConfigManager: enabled: bool | None, source_config: dict | None, ) -> ConfigUpdateResult: - """Update an existing camera in the override config. + """Update an existing camera in the config. Raises: ValueError: If camera doesn't exist @@ -152,64 +154,31 @@ class ConfigManager: self, camera_name: str, ) -> ConfigUpdateResult: - """Remove a camera from the override config. + """Remove a camera from the config. Raises: ValueError: If camera doesn't exist """ ... -``` - -**ConfigLoader** (`loader.py`) - -> **Note**: This extends the existing `loader.py`. The current `load_config(path)` function -> remains unchanged. We add `load_configs()`, `deep_merge()`, and `merge_named_lists()` alongside it. - -```python -def load_configs(paths: list[Path]) -> Config: - """Load and merge multiple YAML config files. - - Merge semantics: - - Files loaded left to right, rightmost wins - - Dicts: deep merge (recursive) - - Lists of objects with `name` field: key-based merge by `name` - - Items with same `name` are merged (override fields win) - - Items only in base are preserved - - Items only in override are added - - Other lists: override replaces base entirely - """ - ... - -def deep_merge(base: dict, override: dict) -> dict: - """Deep merge two dicts. - - - Nested dicts are merged recursively - - Lists of dicts with 'name' key: merge by name (see merge_named_lists) - - Other lists: override replaces base - - Scalars: override wins - """ - ... - -def merge_named_lists(base: list[dict], override: list[dict]) -> list[dict]: - """Merge two lists of dicts by 'name' key. - Example: - base: [{name: "a", x: 1}, {name: "b", x: 2}] - override: [{name: "b", x: 99}, {name: "c", x: 3}] - result: [{name: "a", x: 1}, {name: "b", x: 99}, {name: "c", x: 3}] + async def _save_config(self, config: Config) -> None: + """Save config to disk with backup. - - Preserves order: base items first (in order), then new override items - - Override items with matching name replace/merge with base items - """ - ... + 1. Copy current config to {path}.bak + 2. Write new config atomically (temp file, fsync, rename) + """ + ... ``` +> **Note**: No merge logic needed. The existing `load_config(path)` in `loader.py` is used as-is. +> ConfigManager owns a single config file and overwrites it entirely on mutations. + ### Constraints -- Override file written atomically (write temp, fsync, rename) +- Single config file (no base/override split) +- Backup created before each mutation (`config.yaml` → `config.yaml.bak`) +- Config file written atomically (write temp, fsync, rename) - All file I/O via `asyncio.to_thread` -- Base config is read-only; all mutations go to override file -- Override file is machine-owned (no comment preservation needed) --- @@ -256,11 +225,14 @@ class ClipRepository: """ ... - async def delete_clip(self, clip_id: str) -> None: - """Mark clip as deleted and delete from storage. + async def delete_clip(self, clip_id: str) -> ClipStateData: + """Mark clip as deleted in database. + + Returns the clip data (including storage_uri) so caller can + coordinate storage deletion separately. - Uses existing record_clip_deleted() internally. - Also deletes from storage backend. + Note: Storage deletion is handled by the API route, not the repository. + This keeps the repository focused on database operations only. """ ... @@ -286,7 +258,7 @@ class ClipRepository: - Counts should be efficient (use SQL COUNT, not fetch all) - `count_alerts_since` counts events where `event_type='notification_sent'` - `list_clips` returns tuple of (items, total_count) for pagination -- `delete_clip` should delete from both local and cloud storage +- `delete_clip` only marks deleted in DB; API route coordinates storage deletion --- @@ -347,11 +319,24 @@ class ConfigChangeResponse(BaseModel): camera: CameraResponse | None = None ``` +**Health Response Model** +```python +class HealthResponse(BaseModel): + status: str # "healthy", "degraded", or "unhealthy" + pipeline: str # "running" or "stopped" + postgres: str # "connected" or "unavailable" + cameras_online: int # Count of healthy cameras +``` + +> **Note**: `/health` returns 200 if pipeline is running, even if Postgres is unavailable (degraded state). +> This allows the add-on watchdog to avoid restart loops when only the DB is down. +> Use `/diagnostics` for detailed component status. + ### Constraints - All config-mutating endpoints return `restart_required: True` - Last-write-wins semantics (no optimistic concurrency in v1) -- Return 503 Service Unavailable when Postgres is down +- `/health` returns 200 if pipeline running (status may be "degraded" if DB down) - Pagination: `page` (1-indexed), `page_size` (default 50, max 100) --- @@ -387,26 +372,7 @@ class FastAPIServerConfig(BaseModel): **File**: `src/homesec/cli.py` -### Interface - -```python -# Support multiple --config flags -# Example: homesec run --config base.yaml --config overrides.yaml - -@click.option( - "--config", - "config_paths", - multiple=True, - type=click.Path(exists=True), - help="Config file(s). Can be specified multiple times. Later files override earlier.", -) -``` - -### Constraints - -- Order matters: files loaded left to right -- Default override path: `config/ha-overrides.yaml` (if exists) -- Override path can be explicitly passed as last `--config` +No CLI changes needed. The existing `--config` flag already accepts a single config file path. --- @@ -418,11 +384,9 @@ class FastAPIServerConfig(BaseModel): | `src/homesec/api/server.py` | FastAPI app factory and server | | `src/homesec/api/dependencies.py` | Request dependencies | | `src/homesec/api/routes/*.py` | All route modules | -| `src/homesec/config/manager.py` | Config persistence | -| `src/homesec/config/loader.py` | Multi-file config loading | +| `src/homesec/config/manager.py` | Config persistence (single file with backup) | | `src/homesec/models/config.py` | Add `FastAPIServerConfig` | | `src/homesec/repository/clip_repository.py` | Add read/list/count methods | -| `src/homesec/cli.py` | Support multiple `--config` flags | | `src/homesec/app.py` | Integrate API server | | `pyproject.toml` | Add fastapi, uvicorn dependencies | @@ -446,8 +410,9 @@ class FastAPIServerConfig(BaseModel): - Given camera "front", when DELETE /cameras/front, then 200 and camera removed **Health** -- Given Postgres is up, when GET /health, then status="healthy" -- Given Postgres is down, when GET /health, then 503 and status="unhealthy" +- Given Postgres is up and pipeline running, when GET /health, then 200 and status="healthy" +- Given Postgres is down but pipeline running, when GET /health, then 200 and status="degraded", postgres="unavailable" +- Given pipeline stopped, when GET /health, then 503 and status="unhealthy" **Clips** - Given 100 clips, when GET /clips?page=2&page_size=10, then returns clips 11-20 @@ -458,7 +423,7 @@ class FastAPIServerConfig(BaseModel): - Given clips with mixed cameras, when `list_clips(camera="front")`, then returns only "front" clips - Given 10 clips (5 alerted, 5 not), when `list_clips(alerted=True)`, then returns only 5 alerted clips - Given clips with mixed risk levels, when `list_clips(risk_level="high")`, then returns only high-risk clips -- Given clip exists, when `delete_clip(clip_id)`, then clip marked deleted and storage files removed +- Given clip exists, when `delete_clip(clip_id)`, then clip marked deleted and returns clip data - Given StateStore is up, when `ping()`, then returns True --- @@ -487,7 +452,7 @@ open http://localhost:8080/docs - [ ] FastAPI server starts and serves requests - [ ] All CRUD operations for cameras work -- [ ] Config changes are validated and persisted to override file +- [ ] Config changes are validated and persisted (with backup) - [ ] Config changes return `restart_required: true` - [ ] `/api/v1/system/restart` triggers graceful shutdown - [ ] Clip listing with pagination and filtering works @@ -495,6 +460,5 @@ open http://localhost:8080/docs - [ ] OpenAPI documentation is accurate at `/docs` - [ ] CORS works for configured origins - [ ] API authentication (when enabled) works -- [ ] Returns 503 when Postgres is unavailable -- [ ] Multiple `--config` flags work in CLI +- [ ] `/health` returns 200 if pipeline running (even if DB degraded) - [ ] All tests pass diff --git a/docs/ha-phase-3-addon.md b/docs/ha-phase-3-addon.md index 45ccba8f..fa9301ec 100644 --- a/docs/ha-phase-3-addon.md +++ b/docs/ha-phase-3-addon.md @@ -89,32 +89,35 @@ map: schema: config_path: str? - override_path: str? log_level: list(debug|info|warning|error)? database_url: str? # External DB (optional) storage_type: list(local|dropbox)? storage_path: str? - dropbox_token: password? + dropbox_token: password? # Mapped to DROPBOX_TOKEN env var vlm_enabled: bool? - openai_api_key: password? + openai_api_key: password? # Mapped to OPENAI_API_KEY env var openai_model: str? options: config_path: /config/homesec/config.yaml - override_path: /data/overrides.yaml log_level: info database_url: "" storage_type: local storage_path: /media/homesec/clips vlm_enabled: false +# Secret handling: Add-on options with `password` type are mapped to +# environment variables by the run script. Config YAML stores env var +# names (e.g., `token_env: DROPBOX_TOKEN`), not actual secret values. +# This follows HomeSec's existing pattern for credential management. + startup: services stage: stable advanced: true privileged: [] apparmor: true -# Watchdog for auto-restart +# Watchdog for auto-restart (returns 200 if pipeline running, even if DB degraded) watchdog: http://[HOST]:[PORT:8080]/api/v1/health ``` @@ -209,9 +212,10 @@ exec su postgres -c "postgres -D /data/postgres/data" Waits for PostgreSQL, generates config if missing, runs HomeSec: - Reads options from `/data/options.json` via Bashio +- Maps secret options (dropbox_token, openai_api_key) to environment variables - Waits for `pg_isready` - Generates initial config if not exists -- Runs `python3 -m homesec.cli run --config ... --config ...` +- Runs `python3 -m homesec.cli run --config /config/homesec/config.yaml` ### Constraints diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index 990abe2a..82d45226 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -303,10 +303,14 @@ HomeSec Hub (identifiers: homesec_hub) ### Binary Sensors (binary_sensor.py) **Camera Binary Sensors** (HomesecCameraEntity): -- `motion` - Motion detected (auto-resets after timeout) +- `motion` - Alert-based motion (triggered by `homesec_alert`, auto-resets after timeout) - `person` - Person detected (based on latest alert activity_type) - `online` - Camera connectivity +> **Note**: `motion` and `person` sensors are driven by `homesec_alert` events, not raw clip recording. +> If the alert policy suppresses an alert (e.g., low-risk activity), these sensors won't trigger. +> This is intentional - they represent "alertable activity" rather than all motion. + ### Switches (switch.py) **Camera Switches** (HomesecCameraEntity): diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index f48e112a..f39176fd 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -8,8 +8,7 @@ This document provides an overview of the Home Assistant integration for HomeSec - **Approach**: Add-on + native integration with HomeSec as the runtime - **API stack**: FastAPI, async endpoints only, async SQLAlchemy only -- **Config storage**: Override YAML file is source of truth for dynamic config. Base YAML is bootstrap-only. -- **Config merge**: Multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge (recursive), lists with `name` field merge by key. +- **Config storage**: Single YAML file for all config. API mutations overwrite the file (backup created first). - **Single instance**: HA integration assumes one HomeSec instance (`single_config_entry`) - **Secrets**: Never stored in HomeSec config; only env var names are persisted - **Repository pattern**: API reads/writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access) @@ -112,9 +111,8 @@ These interfaces are defined across phases. See individual phase docs for detail 1. Export current `config.yaml` 2. Install HomeSec add-on 3. Copy config to `/config/homesec/config.yaml` -4. Create `/data/overrides.yaml` for HA-managed config -5. Update database URL if using external Postgres -6. Start add-on +4. Update database URL if using external Postgres +5. Start add-on ### From MQTT Notifier to Native Integration diff --git a/docs/home-assistant-integration-brainstorm.md b/docs/home-assistant-integration-brainstorm.md index 5e616cbd..ae068643 100644 --- a/docs/home-assistant-integration-brainstorm.md +++ b/docs/home-assistant-integration-brainstorm.md @@ -18,8 +18,7 @@ HomeSec is already well-architected for Home Assistant integration with its plug - Required: runtime add/remove cameras and other config changes from HA. - API stack: FastAPI, async endpoints only, async SQLAlchemy only. - Restart is acceptable: API writes validated config to disk and returns `restart_required`; HA may trigger restart. -- Config storage: **Override YAML file** is source of truth for dynamic config. Base YAML is bootstrap-only. -- Config merge: multiple YAML files loaded left → right; rightmost wins. Dicts deep-merge, lists with `name` field merge by key. +- Config storage: **Single YAML file** for all config. API mutations overwrite the file (backup created first). - Single instance: HA integration assumes one HomeSec instance (`single_config_entry`). - Secrets: never stored in HomeSec; config stores env var names; HA/add-on passes env vars at boot. - Repository pattern: API reads and writes go through `ClipRepository` (no direct `StateStore`/`EventStore` access). @@ -312,9 +311,9 @@ The HomeSec add-on is a Docker container managed by HA Supervisor. It bundles Po | Path | Contents | Persistence | |------|----------|-------------| | `/data/postgres/` | PostgreSQL database files | Add-on private, persistent | -| `/data/overrides.yaml` | HA-managed config overrides | Add-on private, persistent | | `/media/homesec/clips/` | Video clips (LocalStorage) | Shared with HA, persistent | -| `/config/homesec/` | Base config file | Shared with HA, persistent | +| `/config/homesec/config.yaml` | HomeSec config file | Shared with HA, persistent | +| `/config/homesec/config.yaml.bak` | Config backup (created on each mutation) | Shared with HA, persistent | --- @@ -415,14 +414,10 @@ Execution order for Option A: Add a new REST API to HomeSec for remote configuration. All endpoints are `async def` and use async SQLAlchemy only. -Config model for Option B: +Config model: -- Base YAML is bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). -- API writes a machine-owned **override YAML** file for all dynamic config. -- HomeSec loads multiple YAML files left → right; rightmost wins. -- Dicts deep-merge; lists with `name` field merge by key. -- Override file default: `config/ha-overrides.yaml` (configurable via CLI). -- CLI accepts multiple `--config` flags; order matters. +- Single YAML config file for all settings. +- API mutations overwrite the config file entirely (backup created first as `config.yaml.bak`). - Config writes use last-write-wins semantics (no optimistic concurrency in v1). ```yaml @@ -438,7 +433,7 @@ GET /api/v1/clips/{id} # Get clip details POST /api/v1/system/restart # Request graceful restart ``` -Config updates are validated with Pydantic, written to the override YAML, and return `restart_required: true`. HA can then call a restart endpoint or restart the add-on. +Config updates are validated with Pydantic, written to the config file (with backup), and return `restart_required: true`. HA can then call a restart endpoint or restart the add-on. Real-time updates use HA Events API (no WebSocket or MQTT required in v1). ### Phase 3: Home Assistant Add-on @@ -613,10 +608,8 @@ async def async_get_config_entry_diagnostics(hass, entry): ### Adopted Strategy (Override YAML) -- **Base YAML**: bootstrap-only (DB DSN, server config, storage root, MQTT broker, etc.). -- **Override YAML**: machine-owned, fully managed by HA via API. -- **Load order**: multiple YAML files loaded left → right; rightmost wins. -- **Merge semantics**: dicts deep-merge; lists with `name` field merge by key (e.g., cameras merge by camera name). +- **Single config file**: all settings in one YAML file, fully managed by HA via API. +- **Backup on mutation**: `config.yaml.bak` created before each overwrite. - **Restart**: required for all config changes. --- From 50ddd400f4f50fd8a70b9118c987e61ea43e96ea Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 21:44:36 -0800 Subject: [PATCH 22/31] docs: address second round of LLM review feedback 1. Remove event_prefix customization - always use "homesec" to ensure compatibility with HA integration which listens for homesec_alert 2. Update decision snapshot - /health returns 200 (degraded) when Postgres down; data endpoints return 503 3. Use Supervisor API for add-on discovery - detect_addon() now uses Supervisor API instead of probing localhost:8080 4. Clarify add-on options are bootstrap-only - options generate initial config, then API owns all changes. Avoids split-brain. 5. Sync Key Interfaces - ClipRepository methods now show ClipStateData return types Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-2-ha-notifier.md | 11 +++++------ docs/ha-phase-3-addon.md | 9 ++++++++- docs/ha-phase-4-ha-integration.md | 14 +++++++++++--- docs/home-assistant-implementation-plan.md | 8 ++++---- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/docs/ha-phase-2-ha-notifier.md b/docs/ha-phase-2-ha-notifier.md index 9a1dcd3f..893ed0c1 100644 --- a/docs/ha-phase-2-ha-notifier.md +++ b/docs/ha-phase-2-ha-notifier.md @@ -34,8 +34,8 @@ class HomeAssistantNotifierConfig(BaseModel): url_env: str | None = None # e.g., "HA_URL" -> http://homeassistant.local:8123 token_env: str | None = None # e.g., "HA_TOKEN" -> long-lived access token - # Event configuration - event_prefix: str = "homesec" # Event: homesec_alert + # Note: event prefix is always "homesec" (not configurable). + # This ensures compatibility with the HA integration which listens for homesec_alert. ``` ### Constraints @@ -100,11 +100,11 @@ def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: """Get the URL and headers for the HA Events API. Supervisor mode: - URL: http://supervisor/core/api/events/{prefix}_{event_type} + URL: http://supervisor/core/api/events/homesec_{event_type} Auth: Bearer {SUPERVISOR_TOKEN} Standalone mode: - URL: {resolved_url}/api/events/{prefix}_{event_type} + URL: {resolved_url}/api/events/homesec_{event_type} Auth: Bearer {resolved_token} """ ... @@ -116,7 +116,7 @@ def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: - Use aiohttp for HTTP requests - Events API endpoint: `POST /api/events/{event_type}` - Supervisor URL: `http://supervisor/core/api/events/...` -- Event name: `{event_prefix}_alert` +- Event name: `homesec_alert` (prefix is hardcoded, not configurable) --- @@ -155,7 +155,6 @@ notifiers: # For standalone mode, provide HA URL and token: # url_env: HA_URL # http://homeassistant.local:8123 # token_env: HA_TOKEN # Long-lived access token from HA - event_prefix: homesec # Optional, default is "homesec" ``` --- diff --git a/docs/ha-phase-3-addon.md b/docs/ha-phase-3-addon.md index fa9301ec..7ebdaf5b 100644 --- a/docs/ha-phase-3-addon.md +++ b/docs/ha-phase-3-addon.md @@ -110,6 +110,10 @@ options: # environment variables by the run script. Config YAML stores env var # names (e.g., `token_env: DROPBOX_TOKEN`), not actual secret values. # This follows HomeSec's existing pattern for credential management. +# +# Bootstrap-only: These options are used to generate the initial config.yaml. +# After first run, use the REST API or HA integration to modify configuration. +# Changing options here won't affect an existing config file. startup: services stage: stable @@ -214,7 +218,7 @@ Waits for PostgreSQL, generates config if missing, runs HomeSec: - Reads options from `/data/options.json` via Bashio - Maps secret options (dropbox_token, openai_api_key) to environment variables - Waits for `pg_isready` -- Generates initial config if not exists +- **Bootstrap only**: Generates initial config if not exists (options used only on first run) - Runs `python3 -m homesec.cli run --config /config/homesec/config.yaml` ### Constraints @@ -223,6 +227,9 @@ Waits for PostgreSQL, generates config if missing, runs HomeSec: - HomeSec must wait for PostgreSQL before starting - Generate default config with home_assistant notifier pre-configured - Use bundled PostgreSQL unless `database_url` option is set +- **Options are bootstrap-only**: After initial config generation, all changes go through + the API. Changing add-on options won't affect an existing config file. This avoids + split-brain between UI options and API-managed config. --- diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index 82d45226..28fbc713 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -86,7 +86,7 @@ CONF_VERIFY_SSL = "verify_ssl" # Defaults DEFAULT_PORT = 8080 DEFAULT_VERIFY_SSL = True -ADDON_HOSTNAME = "localhost" +ADDON_SLUG = "homesec" # Used for Supervisor API discovery # Motion sensor DEFAULT_MOTION_RESET_SECONDS = 30 @@ -143,8 +143,16 @@ async def validate_connection(hass, host, port, api_key=None, verify_ssl=True) - """ ... -async def detect_addon(hass) -> bool: - """Check if HomeSec add-on is running at localhost:8080.""" +async def detect_addon(hass) -> tuple[bool, str | None]: + """Detect HomeSec add-on via Supervisor API. + + Uses Supervisor API to check if the homesec add-on is installed and running. + Returns the add-on's hostname if found. + + Returns: (is_running: bool, hostname: str | None) + - (True, "abc123-homesec") if add-on running + - (False, None) if not installed or not running + """ ... ``` diff --git a/docs/home-assistant-implementation-plan.md b/docs/home-assistant-implementation-plan.md index f39176fd..b5073d8e 100644 --- a/docs/home-assistant-implementation-plan.md +++ b/docs/home-assistant-implementation-plan.md @@ -17,7 +17,7 @@ This document provides an overview of the Home Assistant integration for HomeSec - **Real-time events**: Use HA Events API (not MQTT). Add-on gets `SUPERVISOR_TOKEN` automatically - **No MQTT required**: Integration uses HA Events API directly. Existing MQTT notifier remains for Node-RED/other systems - **Last write wins**: No optimistic concurrency in v1 (single HA instance assumption) -- **API during Postgres outage**: Return 503 Service Unavailable +- **API during Postgres outage**: `/health` returns 200 with `status: "degraded"`; data endpoints (`/clips`, `/stats`) return 503 - **Restart acceptable**: API writes validated config to disk and returns `restart_required`; HA can trigger restart --- @@ -96,9 +96,9 @@ These interfaces are defined across phases. See individual phase docs for detail - `remove_camera(...) -> ConfigUpdateResult` ### ClipRepository Extensions (Phase 1) -- `get_clip(clip_id) -> Clip | None` -- `list_clips(...) -> tuple[list[Clip], int]` (supports `alerted`, `risk_level`, `activity_type` filters) -- `delete_clip(clip_id) -> None` +- `get_clip(clip_id) -> ClipStateData | None` +- `list_clips(...) -> tuple[list[ClipStateData], int]` (supports `alerted`, `risk_level`, `activity_type` filters) +- `delete_clip(clip_id) -> ClipStateData` (marks deleted, returns data for storage cleanup) - `count_clips_since(since: datetime) -> int` - `count_alerts_since(since: datetime) -> int` From 3db8cbdf7037084fe7e59eec791693dc2eb0cee2 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 22:03:12 -0800 Subject: [PATCH 23/31] docs: complete Phase 1 spec with missing response models and implementation details Added response models: - StatsResponse (clips_today, alerts_today, cameras, uptime) - ConfigResponse (full config as dict) - DiagnosticsResponse (detailed component status) - ClipResponse/ClipListResponse (clip data for API) Added implementation notes: - Camera health comes from existing ClipSource.is_healthy()/last_heartbeat() - FastAPI replaces existing aiohttp HealthServer - /system/restart uses SIGTERM (existing signal handler) - API server lifecycle in Application.run() Updated file changes: - Remove src/homesec/health/ (replaced by FastAPI) - Remove aiohttp dependency if unused elsewhere Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 124 +++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 0b1922b8..4f872f1f 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -332,6 +332,67 @@ class HealthResponse(BaseModel): > This allows the add-on watchdog to avoid restart loops when only the DB is down. > Use `/diagnostics` for detailed component status. +**Stats Response Model** +```python +class StatsResponse(BaseModel): + clips_today: int + alerts_today: int + cameras_total: int + cameras_online: int + uptime_seconds: float # Time since Application started +``` + +**Config Response Model** +```python +class ConfigResponse(BaseModel): + """Returns the full config (secrets shown as env var names, not values).""" + config: dict # The full Config model as dict +``` + +**Diagnostics Response Model** +```python +class ComponentStatus(BaseModel): + status: str # "ok", "error" + error: str | None = None # Error message if failed + latency_ms: float | None = None + +class CameraStatus(BaseModel): + healthy: bool + enabled: bool + last_heartbeat: float | None + frames_processed: int + errors_recent: int # Errors in last hour + +class DiagnosticsResponse(BaseModel): + status: str # "healthy", "degraded", "unhealthy" + uptime_seconds: float + postgres: ComponentStatus + storage: ComponentStatus + cameras: dict[str, CameraStatus] +``` + +**Clip Response Models** +```python +class ClipResponse(BaseModel): + id: str + camera: str + status: str # "recording", "uploading", "complete", "failed", "deleted" + created_at: datetime + activity_type: str | None = None + risk_level: str | None = None + summary: str | None = None + detected_objects: list[str] = [] + storage_uri: str | None = None + view_url: str | None = None + alerted: bool = False + +class ClipListResponse(BaseModel): + clips: list[ClipResponse] + total: int # Total matching (for pagination) + page: int + page_size: int +``` + ### Constraints - All config-mutating endpoints return `restart_required: True` @@ -376,6 +437,62 @@ No CLI changes needed. The existing `--config` flag already accepts a single con --- +## 1.6 Implementation Notes + +### Camera Health Source + +Camera health (`CameraResponse.healthy`, `last_heartbeat`) comes from existing infrastructure: +- `ClipSource.is_healthy()` and `ClipSource.last_heartbeat()` already exist +- `Application` holds references to sources +- API routes access sources via `Application` to build responses + +### Replacing HealthServer + +The existing aiohttp-based `HealthServer` (`src/homesec/health/server.py`) is replaced by FastAPI. +Remove `HealthServer` and its config; FastAPI's `/health` endpoint provides the same functionality. + +### Restart Mechanism + +`POST /api/v1/system/restart` sends SIGTERM to self: + +```python +import os, signal + +@router.post("/api/v1/system/restart") +async def restart(): + os.kill(os.getpid(), signal.SIGTERM) + return {"message": "Shutdown initiated"} +``` + +Existing signal handler in `app.py` handles graceful shutdown. Supervisor/add-on handles restart. + +### API Server Lifecycle + +In `Application.run()`: + +```python +async def run(self): + # ... initialize components ... + + # Start API server (replaces old HealthServer) + if self.config.server.enabled: + self._api_server = APIServer(create_app(self), self.config.server.host, self.config.server.port) + await self._api_server.start() + + # Track start time for uptime + self._start_time = time.time() + + # Wait for shutdown signal + await self._shutdown_event.wait() + + # Shutdown (stop API first, then sources) + if self._api_server: + await self._api_server.stop() + # ... stop other components ... +``` + +--- + ## File Changes Summary | File | Change | @@ -385,10 +502,11 @@ No CLI changes needed. The existing `--config` flag already accepts a single con | `src/homesec/api/dependencies.py` | Request dependencies | | `src/homesec/api/routes/*.py` | All route modules | | `src/homesec/config/manager.py` | Config persistence (single file with backup) | -| `src/homesec/models/config.py` | Add `FastAPIServerConfig` | +| `src/homesec/models/config.py` | Add `FastAPIServerConfig`, remove `HealthConfig` | | `src/homesec/repository/clip_repository.py` | Add read/list/count methods | -| `src/homesec/app.py` | Integrate API server | -| `pyproject.toml` | Add fastapi, uvicorn dependencies | +| `src/homesec/app.py` | Integrate API server, remove HealthServer usage | +| `src/homesec/health/` | Remove (replaced by FastAPI) | +| `pyproject.toml` | Add fastapi, uvicorn; remove aiohttp if unused elsewhere | --- From a017533c16f05af6db2ba730cfe246dfd33905ca Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 22:11:32 -0800 Subject: [PATCH 24/31] docs: address final Phase 1 review feedback 1. StateStore extensions: New read methods (list_clips, get_clip, count_clips_since, count_alerts_since, mark_clip_deleted) added to StateStore interface. ClipRepository delegates to these. All DB access goes through StateStore for consistency. 2. Auth bypass: /health and /diagnostics are public (no auth). Allows watchdog and monitoring tools to check health without token. 3. DB-down behavior: Clear 503 rule for non-health endpoints. /health and /diagnostics return 200 with degraded status. All other endpoints return 503 with DB_UNAVAILABLE error. Updated file changes to include interfaces.py and state/postgres.py. Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 66 +++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 4f872f1f..81711c93 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -59,6 +59,7 @@ async def get_homesec_app(request: Request) -> Application: async def verify_api_key(request: Request, app=Depends(get_homesec_app)) -> None: """Verify API key if authentication is enabled. + - Skips auth for public paths: /api/v1/health, /api/v1/diagnostics - Checks app.config.server.auth_enabled - Expects Authorization: Bearer - Raises HTTPException 401 on failure @@ -252,13 +253,48 @@ class ClipRepository: return await self._state.ping() ``` +### StateStore Extensions + +**File**: `src/homesec/state/postgres.py` + +`ClipRepository` delegates to new `StateStore` methods (all DB access through StateStore for consistency): + +```python +class StateStore(Protocol): + # ... existing methods ... + + # NEW: Read methods for API + async def get_clip(self, clip_id: str) -> ClipStateData | None: ... + + async def list_clips( + self, + *, + camera: str | None = None, + status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: ... + + async def mark_clip_deleted(self, clip_id: str) -> ClipStateData: ... + + async def count_clips_since(self, since: datetime) -> int: ... + + async def count_alerts_since(self, since: datetime) -> int: ... +``` + ### Constraints - Must use async SQLAlchemy - Counts should be efficient (use SQL COUNT, not fetch all) - `count_alerts_since` counts events where `event_type='notification_sent'` - `list_clips` returns tuple of (items, total_count) for pagination -- `delete_clip` only marks deleted in DB; API route coordinates storage deletion +- `mark_clip_deleted` only marks deleted in DB; API route coordinates storage deletion +- All new methods added to `StateStore` interface and `PostgresStateStore` implementation --- @@ -393,6 +429,30 @@ class ClipListResponse(BaseModel): page_size: int ``` +### Auth Bypass + +`/health` and `/diagnostics` are public (no auth required). This allows: +- Add-on watchdog to check health without token +- External monitoring tools to probe health + +All other endpoints require auth when `auth_enabled: true`. + +### DB-Down Behavior + +| Endpoint | DB Down Response | +|----------|------------------| +| `/health` | 200, `status: "degraded"`, `postgres: "unavailable"` | +| `/diagnostics` | 200, shows component error details | +| All other endpoints | 503 Service Unavailable | + +Non-health endpoints return 503 with: +```json +{ + "detail": "Database unavailable", + "error_code": "DB_UNAVAILABLE" +} +``` + ### Constraints - All config-mutating endpoints return `restart_required: True` @@ -503,7 +563,9 @@ async def run(self): | `src/homesec/api/routes/*.py` | All route modules | | `src/homesec/config/manager.py` | Config persistence (single file with backup) | | `src/homesec/models/config.py` | Add `FastAPIServerConfig`, remove `HealthConfig` | -| `src/homesec/repository/clip_repository.py` | Add read/list/count methods | +| `src/homesec/interfaces.py` | Add new StateStore methods to protocol | +| `src/homesec/state/postgres.py` | Implement new StateStore methods | +| `src/homesec/repository/clip_repository.py` | Add read/list/count methods (delegate to StateStore) | | `src/homesec/app.py` | Integrate API server, remove HealthServer usage | | `src/homesec/health/` | Remove (replaced by FastAPI) | | `pyproject.toml` | Add fastapi, uvicorn; remove aiohttp if unused elsewhere | From 5d072a6d630a176e2f6890f442cb0247c5cb2388 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 22:18:36 -0800 Subject: [PATCH 25/31] docs: fix response models to match existing code - ClipResponse.status: Use actual ClipStatus enum values (queued_local, uploaded, analyzed, done, error, deleted) - CameraStatus: Remove non-existent fields (frames_processed, errors_recent). Only use existing data from ClipSource. Co-Authored-By: Claude Opus 4.5 --- docs/ha-phase-1-rest-api.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/ha-phase-1-rest-api.md b/docs/ha-phase-1-rest-api.md index 81711c93..101fab4e 100644 --- a/docs/ha-phase-1-rest-api.md +++ b/docs/ha-phase-1-rest-api.md @@ -395,9 +395,7 @@ class ComponentStatus(BaseModel): class CameraStatus(BaseModel): healthy: bool enabled: bool - last_heartbeat: float | None - frames_processed: int - errors_recent: int # Errors in last hour + last_heartbeat: float | None # Monotonic timestamp from ClipSource class DiagnosticsResponse(BaseModel): status: str # "healthy", "degraded", "unhealthy" @@ -412,7 +410,7 @@ class DiagnosticsResponse(BaseModel): class ClipResponse(BaseModel): id: str camera: str - status: str # "recording", "uploading", "complete", "failed", "deleted" + status: str # ClipStatus enum: "queued_local", "uploaded", "analyzed", "done", "error", "deleted" created_at: datetime activity_type: str | None = None risk_level: str | None = None From dbf27fbd966d094c54ad71f9e01b5855189664fd Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Sun, 1 Feb 2026 23:36:28 -0800 Subject: [PATCH 26/31] Phase 1 implementation --- config/example.yaml | 10 +- pyproject.toml | 3 + src/homesec/api/__init__.py | 5 + src/homesec/api/dependencies.py | 54 ++ src/homesec/api/routes/__init__.py | 33 + src/homesec/api/routes/cameras.py | 158 +++++ src/homesec/api/routes/clips.py | 136 ++++ src/homesec/api/routes/config.py | 29 + src/homesec/api/routes/health.py | 126 ++++ src/homesec/api/routes/stats.py | 42 ++ src/homesec/api/routes/system.py | 17 + src/homesec/api/server.py | 88 +++ src/homesec/app.py | 87 ++- src/homesec/config/manager.py | 130 ++++ src/homesec/health/__init__.py | 5 - src/homesec/health/server.py | 228 ------ src/homesec/interfaces.py | 39 + src/homesec/models/__init__.py | 4 +- src/homesec/models/clip.py | 2 + src/homesec/models/config.py | 25 +- src/homesec/repository/clip_repository.py | 84 ++- src/homesec/state/postgres.py | 196 ++++- tests/homesec/mocks/state_store.py | 67 ++ tests/homesec/test_api_routes.py | 825 ++++++++++++++++++++++ tests/homesec/test_app.py | 16 +- tests/homesec/test_clip_repository.py | 219 +++++- tests/homesec/test_config_manager.py | 152 ++++ tests/homesec/test_health.py | 465 ++++++------ uv.lock | 106 +++ 29 files changed, 2835 insertions(+), 516 deletions(-) create mode 100644 src/homesec/api/__init__.py create mode 100644 src/homesec/api/dependencies.py create mode 100644 src/homesec/api/routes/__init__.py create mode 100644 src/homesec/api/routes/cameras.py create mode 100644 src/homesec/api/routes/clips.py create mode 100644 src/homesec/api/routes/config.py create mode 100644 src/homesec/api/routes/health.py create mode 100644 src/homesec/api/routes/stats.py create mode 100644 src/homesec/api/routes/system.py create mode 100644 src/homesec/api/server.py create mode 100644 src/homesec/config/manager.py delete mode 100644 src/homesec/health/__init__.py delete mode 100644 src/homesec/health/server.py create mode 100644 tests/homesec/test_api_routes.py create mode 100644 tests/homesec/test_config_manager.py diff --git a/config/example.yaml b/config/example.yaml index 73df3255..f01c0afb 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -196,8 +196,12 @@ alert_policy: # filter_workers: 4 # vlm_workers: 2 -# Health check server -# health: +# FastAPI server +# server: +# enabled: true # host: "0.0.0.0" # port: 8080 -# mqtt_is_critical: false +# cors_origins: +# - "*" +# auth_enabled: false +# api_key_env: "HOMESEC_API_KEY" diff --git a/pyproject.toml b/pyproject.toml index 0641a444..338482df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ dependencies = [ "dropbox>=12.0.2", "fire>=0.7.1", + "fastapi>=0.111.0", "opencv-python>=4.12.0.88", "paho-mqtt>=2.1.0", "pydantic>=2.0.0", @@ -36,6 +37,7 @@ dependencies = [ "alembic>=1.13.0", "asyncpg>=0.29.0", "ultralytics>=8.3.226", + "uvicorn>=0.30.0", "pyyaml>=6.0.3", "aiohttp>=3.13.2", "anyio>=4.0.0", @@ -71,6 +73,7 @@ packages = ["src/homesec"] [dependency-groups] dev = [ "asyncpg-stubs>=0.31.1", + "httpx>=0.27.0", "ipykernel>=7.1.0", "mypy>=1.19.1", "nbstripout>=0.8.2", diff --git a/src/homesec/api/__init__.py b/src/homesec/api/__init__.py new file mode 100644 index 00000000..e0dd4b67 --- /dev/null +++ b/src/homesec/api/__init__.py @@ -0,0 +1,5 @@ +"""HomeSec FastAPI application.""" + +from homesec.api.server import APIServer, create_app + +__all__ = ["APIServer", "create_app"] diff --git a/src/homesec/api/dependencies.py b/src/homesec/api/dependencies.py new file mode 100644 index 00000000..b0d2266a --- /dev/null +++ b/src/homesec/api/dependencies.py @@ -0,0 +1,54 @@ +"""FastAPI dependency helpers.""" + +from __future__ import annotations + +import secrets +from typing import TYPE_CHECKING, cast + +from fastapi import Depends, HTTPException, Request + +if TYPE_CHECKING: + from homesec.app import Application + + +class DatabaseUnavailableError(RuntimeError): + """Raised when database is unavailable for API requests.""" + + +async def get_homesec_app(request: Request) -> Application: + """Get the HomeSec Application instance from request state.""" + app = cast("Application | None", getattr(request.app.state, "homesec", None)) + if app is None: + raise HTTPException(status_code=503, detail="Application not initialized") + return app + + +async def verify_api_key(request: Request, app: Application = Depends(get_homesec_app)) -> None: + """Verify API key if authentication is enabled.""" + path = request.url.path + if path in ("/api/v1/health", "/api/v1/diagnostics"): + return + + server_config = app.config.server + if not server_config.auth_enabled: + return + + api_key = server_config.get_api_key() + if not api_key: + raise HTTPException(status_code=500, detail="API key not configured") + + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Unauthorized") + + token = auth_header.removeprefix("Bearer ").strip() + if not secrets.compare_digest(token, api_key): + raise HTTPException(status_code=401, detail="Unauthorized") + + +async def require_database(app: Application = Depends(get_homesec_app)) -> None: + """Ensure the database is reachable for data endpoints.""" + repository = app.repository + ok = await repository.ping() + if not ok: + raise DatabaseUnavailableError("Database unavailable") diff --git a/src/homesec/api/routes/__init__.py b/src/homesec/api/routes/__init__.py new file mode 100644 index 00000000..d85657c8 --- /dev/null +++ b/src/homesec/api/routes/__init__.py @@ -0,0 +1,33 @@ +"""API route registration.""" + +from __future__ import annotations + +from fastapi import Depends, FastAPI + +from homesec.api.dependencies import require_database, verify_api_key +from homesec.api.routes import cameras, clips, config, health, stats, system + + +def register_routes(app: FastAPI) -> None: + """Register all API routers.""" + app.include_router(health.router, dependencies=[Depends(verify_api_key)]) + app.include_router( + config.router, + dependencies=[Depends(verify_api_key), Depends(require_database)], + ) + app.include_router( + cameras.router, + dependencies=[Depends(verify_api_key), Depends(require_database)], + ) + app.include_router( + clips.router, + dependencies=[Depends(verify_api_key), Depends(require_database)], + ) + app.include_router( + stats.router, + dependencies=[Depends(verify_api_key), Depends(require_database)], + ) + app.include_router( + system.router, + dependencies=[Depends(verify_api_key), Depends(require_database)], + ) diff --git a/src/homesec/api/routes/cameras.py b/src/homesec/api/routes/cameras.py new file mode 100644 index 00000000..401179f5 --- /dev/null +++ b/src/homesec/api/routes/cameras.py @@ -0,0 +1,158 @@ +"""Camera CRUD endpoints.""" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel + +from homesec.api.dependencies import get_homesec_app +from homesec.models.config import CameraConfig + +if TYPE_CHECKING: + from homesec.app import Application + +router = APIRouter(tags=["cameras"]) + + +class CameraCreate(BaseModel): + name: str + enabled: bool = True + source_backend: str + source_config: dict[str, object] + + +class CameraUpdate(BaseModel): + enabled: bool | None = None + source_config: dict[str, object] | None = None + + +class CameraResponse(BaseModel): + name: str + enabled: bool + source_backend: str + healthy: bool + last_heartbeat: float | None + source_config: dict[str, object] + + +class ConfigChangeResponse(BaseModel): + restart_required: bool = True + camera: CameraResponse | None = None + + +def _source_config_to_dict(camera: CameraConfig) -> dict[str, object]: + config = camera.source.config + if hasattr(config, "model_dump"): + return config.model_dump(mode="json") + return dict(config) + + +def _camera_response(app: Application, camera: CameraConfig) -> CameraResponse: + source = app.get_source(camera.name) + if camera.enabled and source is not None: + healthy = source.is_healthy() + last_heartbeat = source.last_heartbeat() + else: + healthy = False + last_heartbeat = None + + return CameraResponse( + name=camera.name, + enabled=camera.enabled, + source_backend=camera.source.backend, + healthy=healthy, + last_heartbeat=last_heartbeat, + source_config=_source_config_to_dict(camera), + ) + + +@router.get("/api/v1/cameras", response_model=list[CameraResponse]) +async def list_cameras(app: Application = Depends(get_homesec_app)) -> list[CameraResponse]: + """List all cameras.""" + config = await asyncio.to_thread(app.config_manager.get_config) + return [_camera_response(app, camera) for camera in config.cameras] + + +@router.get("/api/v1/cameras/{name}", response_model=CameraResponse) +async def get_camera(name: str, app: Application = Depends(get_homesec_app)) -> CameraResponse: + """Get a single camera.""" + config = await asyncio.to_thread(app.config_manager.get_config) + camera = next((cam for cam in config.cameras if cam.name == name), None) + if camera is None: + raise HTTPException(status_code=404, detail="Camera not found") + return _camera_response(app, camera) + + +@router.post("/api/v1/cameras", response_model=ConfigChangeResponse, status_code=201) +async def create_camera( + payload: CameraCreate, app: Application = Depends(get_homesec_app) +) -> ConfigChangeResponse: + """Create a new camera.""" + try: + result = await app.config_manager.add_camera( + name=payload.name, + enabled=payload.enabled, + source_backend=payload.source_backend, + source_config=payload.source_config, + ) + except ValueError as exc: + detail = str(exc) + if "Camera not found" in detail: + raise HTTPException(status_code=404, detail=detail) from exc + raise HTTPException(status_code=400, detail=detail) from exc + + config = await asyncio.to_thread(app.config_manager.get_config) + camera = next((cam for cam in config.cameras if cam.name == payload.name), None) + return ConfigChangeResponse( + restart_required=result.restart_required, + camera=_camera_response(app, camera) if camera else None, + ) + + +@router.put("/api/v1/cameras/{name}", response_model=ConfigChangeResponse) +async def update_camera( + name: str, + payload: CameraUpdate, + app: Application = Depends(get_homesec_app), +) -> ConfigChangeResponse: + """Update a camera.""" + try: + result = await app.config_manager.update_camera( + camera_name=name, + enabled=payload.enabled, + source_config=payload.source_config, + ) + except ValueError as exc: + detail = str(exc) + if "Camera not found" in detail: + raise HTTPException(status_code=404, detail=detail) from exc + raise HTTPException(status_code=400, detail=detail) from exc + + config = await asyncio.to_thread(app.config_manager.get_config) + camera = next((cam for cam in config.cameras if cam.name == name), None) + if camera is None: + raise HTTPException(status_code=404, detail="Camera not found") + + return ConfigChangeResponse( + restart_required=result.restart_required, + camera=_camera_response(app, camera), + ) + + +@router.delete("/api/v1/cameras/{name}", response_model=ConfigChangeResponse) +async def delete_camera( + name: str, app: Application = Depends(get_homesec_app) +) -> ConfigChangeResponse: + """Delete a camera.""" + try: + result = await app.config_manager.remove_camera(camera_name=name) + except ValueError as exc: + detail = str(exc) + if "Camera not found" in detail: + raise HTTPException(status_code=404, detail=detail) from exc + raise HTTPException(status_code=400, detail=detail) from exc + + return ConfigChangeResponse(restart_required=result.restart_required, camera=None) diff --git a/src/homesec/api/routes/clips.py b/src/homesec/api/routes/clips.py new file mode 100644 index 00000000..212a37c3 --- /dev/null +++ b/src/homesec/api/routes/clips.py @@ -0,0 +1,136 @@ +"""Clip browsing endpoints.""" + +from __future__ import annotations + +import logging +from datetime import datetime +from typing import TYPE_CHECKING + +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel + +from homesec.api.dependencies import get_homesec_app + +if TYPE_CHECKING: + from homesec.app import Application +from homesec.models.clip import ClipStateData +from homesec.models.enums import ClipStatus + +router = APIRouter(tags=["clips"]) +logger = logging.getLogger(__name__) + + +class ClipResponse(BaseModel): + id: str + camera: str + status: str + created_at: datetime + activity_type: str | None = None + risk_level: str | None = None + summary: str | None = None + detected_objects: list[str] = [] + storage_uri: str | None = None + view_url: str | None = None + alerted: bool = False + + +class ClipListResponse(BaseModel): + clips: list[ClipResponse] + total: int + page: int + page_size: int + + +def _status_value(status: ClipStatus | str) -> str: + if isinstance(status, ClipStatus): + return status.value + return str(status) + + +def _clip_response(state: ClipStateData) -> ClipResponse: + analysis = state.analysis_result + detected = state.filter_result.detected_classes if state.filter_result else [] + alerted = state.alert_decision.notify if state.alert_decision else False + created_at = state.created_at or datetime.now() + clip_id = state.clip_id or "" + + return ClipResponse( + id=clip_id, + camera=state.camera_name, + status=_status_value(state.status), + created_at=created_at, + activity_type=analysis.activity_type if analysis else None, + risk_level=str(analysis.risk_level) if analysis else None, + summary=analysis.summary if analysis else None, + detected_objects=detected, + storage_uri=state.storage_uri, + view_url=state.view_url, + alerted=alerted, + ) + + +@router.get("/api/v1/clips", response_model=ClipListResponse) +async def list_clips( + camera: str | None = None, + status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=100), + app: Application = Depends(get_homesec_app), +) -> ClipListResponse: + """List clips with filtering and pagination.""" + offset = (page - 1) * page_size + clips, total = await app.repository.list_clips( + camera=camera, + status=status, + alerted=alerted, + risk_level=risk_level, + activity_type=activity_type, + since=since, + until=until, + offset=offset, + limit=page_size, + ) + + return ClipListResponse( + clips=[_clip_response(state) for state in clips], + total=total, + page=page, + page_size=page_size, + ) + + +@router.get("/api/v1/clips/{clip_id}", response_model=ClipResponse) +async def get_clip(clip_id: str, app: Application = Depends(get_homesec_app)) -> ClipResponse: + """Get a single clip.""" + state = await app.repository.get_clip(clip_id) + if state is None: + raise HTTPException(status_code=404, detail="Clip not found") + return _clip_response(state) + + +@router.delete("/api/v1/clips/{clip_id}", response_model=ClipResponse) +async def delete_clip(clip_id: str, app: Application = Depends(get_homesec_app)) -> ClipResponse: + """Delete a clip and its storage object.""" + try: + state = await app.repository.delete_clip(clip_id) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + if state.storage_uri: + try: + await app.storage.delete(state.storage_uri) + except Exception as exc: + logger.error( + "Storage delete failed for clip %s: %s", + clip_id, + exc, + exc_info=exc, + ) + raise HTTPException(status_code=500, detail="Storage deletion failed") from exc + + return _clip_response(state) diff --git a/src/homesec/api/routes/config.py b/src/homesec/api/routes/config.py new file mode 100644 index 00000000..6239b87a --- /dev/null +++ b/src/homesec/api/routes/config.py @@ -0,0 +1,29 @@ +"""Configuration endpoints.""" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +from fastapi import APIRouter, Depends +from pydantic import BaseModel + +from homesec.api.dependencies import get_homesec_app + +if TYPE_CHECKING: + from homesec.app import Application + +router = APIRouter(tags=["config"]) + + +class ConfigResponse(BaseModel): + """Returns the full config (secrets shown as env var names, not values).""" + + config: dict[str, object] + + +@router.get("/api/v1/config", response_model=ConfigResponse) +async def get_config(app: Application = Depends(get_homesec_app)) -> ConfigResponse: + """Return full configuration.""" + config = await asyncio.to_thread(app.config_manager.get_config) + return ConfigResponse(config=config.model_dump(mode="json")) diff --git a/src/homesec/api/routes/health.py b/src/homesec/api/routes/health.py new file mode 100644 index 00000000..b0bd05f3 --- /dev/null +++ b/src/homesec/api/routes/health.py @@ -0,0 +1,126 @@ +"""Health and diagnostics endpoints.""" + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING, Any + +from fastapi import APIRouter, Depends +from fastapi.responses import JSONResponse +from pydantic import BaseModel + +from homesec.api.dependencies import get_homesec_app + +if TYPE_CHECKING: + from homesec.app import Application + +router = APIRouter(tags=["health"]) + + +class HealthResponse(BaseModel): + status: str + pipeline: str + postgres: str + cameras_online: int + + +class ComponentStatus(BaseModel): + status: str + error: str | None = None + latency_ms: float | None = None + + +class CameraStatus(BaseModel): + healthy: bool + enabled: bool + last_heartbeat: float | None + + +class DiagnosticsResponse(BaseModel): + status: str + uptime_seconds: float + postgres: ComponentStatus + storage: ComponentStatus + cameras: dict[str, CameraStatus] + + +@router.get("/api/v1/health", response_model=HealthResponse) +async def get_health(app: Application = Depends(get_homesec_app)) -> HealthResponse | JSONResponse: + """Basic health check.""" + pipeline_running = app.pipeline_running + postgres_ok = await app.repository.ping() + cameras_online = sum(1 for source in app.sources if source.is_healthy()) + + if not pipeline_running: + status = "unhealthy" + elif postgres_ok: + status = "healthy" + else: + status = "degraded" + + response = HealthResponse( + status=status, + pipeline="running" if pipeline_running else "stopped", + postgres="connected" if postgres_ok else "unavailable", + cameras_online=cameras_online, + ) + + if not pipeline_running: + return JSONResponse(status_code=503, content=response.model_dump(mode="json")) + return response + + +@router.get("/api/v1/diagnostics", response_model=DiagnosticsResponse) +async def get_diagnostics(app: Application = Depends(get_homesec_app)) -> DiagnosticsResponse: + """Detailed component diagnostics.""" + pipeline_running = app.pipeline_running + + async def _check(ping: Any) -> ComponentStatus: + start = time.perf_counter() + try: + ok = await ping() + latency_ms = (time.perf_counter() - start) * 1000 + except Exception as exc: # pragma: no cover - defensive + return ComponentStatus(status="error", error=str(exc)) + + if ok: + return ComponentStatus(status="ok", latency_ms=latency_ms) + return ComponentStatus(status="error", error="unavailable", latency_ms=latency_ms) + + postgres_status = await _check(app.repository.ping) + storage_status = await _check(app.storage.ping) + + cameras: dict[str, CameraStatus] = {} + for camera in app.config.cameras: + source = app.get_source(camera.name) + if camera.enabled and source is not None: + healthy = source.is_healthy() + last_heartbeat = source.last_heartbeat() + else: + healthy = False + last_heartbeat = None + + cameras[camera.name] = CameraStatus( + healthy=healthy, + enabled=camera.enabled, + last_heartbeat=last_heartbeat, + ) + + if not pipeline_running: + status = "unhealthy" + elif ( + postgres_status.status == "error" + or storage_status.status == "error" + or any(cam.enabled and not cam.healthy for cam in cameras.values()) + ): + status = "degraded" + else: + status = "healthy" + + return DiagnosticsResponse( + status=status, + uptime_seconds=app.uptime_seconds, + postgres=postgres_status, + storage=storage_status, + cameras=cameras, + ) diff --git a/src/homesec/api/routes/stats.py b/src/homesec/api/routes/stats.py new file mode 100644 index 00000000..0ff00e2f --- /dev/null +++ b/src/homesec/api/routes/stats.py @@ -0,0 +1,42 @@ +"""System statistics endpoints.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from fastapi import APIRouter, Depends +from pydantic import BaseModel + +from homesec.api.dependencies import get_homesec_app + +if TYPE_CHECKING: + from homesec.app import Application + +router = APIRouter(tags=["stats"]) + + +class StatsResponse(BaseModel): + clips_today: int + alerts_today: int + cameras_total: int + cameras_online: int + uptime_seconds: float + + +@router.get("/api/v1/stats", response_model=StatsResponse) +async def get_stats(app: Application = Depends(get_homesec_app)) -> StatsResponse: + """Return system statistics.""" + today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + clips_today = await app.repository.count_clips_since(today_start) + alerts_today = await app.repository.count_alerts_since(today_start) + cameras_total = len(app.config.cameras) + cameras_online = sum(1 for source in app.sources if source.is_healthy()) + + return StatsResponse( + clips_today=clips_today, + alerts_today=alerts_today, + cameras_total=cameras_total, + cameras_online=cameras_online, + uptime_seconds=app.uptime_seconds, + ) diff --git a/src/homesec/api/routes/system.py b/src/homesec/api/routes/system.py new file mode 100644 index 00000000..1064c00b --- /dev/null +++ b/src/homesec/api/routes/system.py @@ -0,0 +1,17 @@ +"""System control endpoints.""" + +from __future__ import annotations + +import os +import signal + +from fastapi import APIRouter + +router = APIRouter(tags=["system"]) + + +@router.post("/api/v1/system/restart") +async def restart_system() -> dict[str, str]: + """Request a graceful restart.""" + os.kill(os.getpid(), signal.SIGTERM) + return {"message": "Shutdown initiated"} diff --git a/src/homesec/api/server.py b/src/homesec/api/server.py new file mode 100644 index 00000000..86a7620a --- /dev/null +++ b/src/homesec/api/server.py @@ -0,0 +1,88 @@ +"""FastAPI server wiring for HomeSec.""" + +from __future__ import annotations + +import asyncio +import logging +from typing import TYPE_CHECKING, Any, cast + +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse + +from homesec.api.dependencies import DatabaseUnavailableError +from homesec.api.routes import register_routes + +if TYPE_CHECKING: + from homesec.app import Application + +logger = logging.getLogger(__name__) + + +def create_app(app_instance: Application) -> FastAPI: + """Create the FastAPI application.""" + app = FastAPI(title="HomeSec API", version="1.0.0") + app.state.homesec = app_instance + + server_config = app_instance.config.server + app.add_middleware( + CORSMiddleware, + allow_origins=server_config.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + @app.exception_handler(DatabaseUnavailableError) + async def _db_unavailable_handler( + request: object, exc: DatabaseUnavailableError + ) -> JSONResponse: + _ = request + _ = exc + return JSONResponse( + status_code=503, + content={"detail": "Database unavailable", "error_code": "DB_UNAVAILABLE"}, + ) + + register_routes(app) + return app + + +class APIServer: + """Manages the API server lifecycle.""" + + def __init__(self, app: FastAPI, host: str, port: int) -> None: + self._app = app + self._host = host + self._port = port + self._server: uvicorn.Server | None = None + self._task: asyncio.Task[None] | None = None + + async def start(self) -> None: + """Start the API server in the background.""" + if self._task is not None: + return + + config = uvicorn.Config( + self._app, + host=self._host, + port=self._port, + loop="asyncio", + log_level="info", + access_log=False, + ) + self._server = uvicorn.Server(config) + cast(Any, self._server).install_signal_handlers = False + self._task = asyncio.create_task(self._server.serve()) + logger.info("API server started: http://%s:%d", self._host, self._port) + + async def stop(self) -> None: + """Stop the API server.""" + if self._server is None or self._task is None: + return + self._server.should_exit = True + await self._task + self._task = None + self._server = None + logger.info("API server stopped") diff --git a/src/homesec/app.py b/src/homesec/app.py index 937c4b4d..4457c783 100644 --- a/src/homesec/app.py +++ b/src/homesec/app.py @@ -5,11 +5,13 @@ import asyncio import logging import signal +import time from pathlib import Path from typing import TYPE_CHECKING +from homesec.api import APIServer, create_app from homesec.config import load_config, resolve_env_var, validate_config, validate_plugin_names -from homesec.health import HealthServer +from homesec.config.manager import ConfigManager from homesec.interfaces import EventStore from homesec.notifiers.multiplex import MultiplexNotifier, NotifierEntry from homesec.pipeline import ClipPipeline @@ -63,8 +65,11 @@ def __init__(self, config_path: Path) -> None: self._filter: ObjectFilter | None = None self._vlm: VLMAnalyzer | None = None self._sources: list[ClipSource] = [] + self._sources_by_camera: dict[str, ClipSource] = {} self._pipeline: ClipPipeline | None = None - self._health_server: HealthServer | None = None + self._api_server: APIServer | None = None + self._config_manager = ConfigManager(config_path) + self._start_time: float | None = None # Shutdown state self._shutdown_event = asyncio.Event() @@ -87,14 +92,23 @@ async def run(self) -> None: # Set up signal handlers self._setup_signal_handlers() - # Start health server - if self._health_server: - await self._health_server.start() + # Start API server + server_cfg = self._require_config().server + if server_cfg.enabled: + self._api_server = APIServer( + create_app(self), + host=server_cfg.host, + port=server_cfg.port, + ) + await self._api_server.start() # Start sources for source in self._sources: await source.start() + # Track start time for uptime + self._start_time = time.time() + logger.info("Application started. Waiting for clips...") # Wait for shutdown signal @@ -160,20 +174,6 @@ async def _create_components(self) -> None: for source in self._sources: source.register_callback(self._pipeline.on_new_clip) - # Create health server - health_cfg = config.health - self._health_server = HealthServer( - host=health_cfg.host, - port=health_cfg.port, - ) - self._health_server.set_components( - state_store=self._state_store, - storage=self._storage, - notifier=self._notifier, - sources=self._sources, - mqtt_is_critical=health_cfg.mqtt_is_critical, - ) - logger.info("All components created") def _create_storage(self, config: Config) -> StorageBackend: @@ -250,8 +250,11 @@ def _create_alert_policy(self, config: Config) -> AlertPolicy: def _create_sources(self, config: Config) -> list[ClipSource]: """Create clip sources based on config using plugin registry.""" sources: list[ClipSource] = [] + self._sources_by_camera = {} for camera in config.cameras: + if not camera.enabled: + continue source_cfg = camera.source source = load_source_plugin( source_backend=source_cfg.backend, @@ -259,6 +262,7 @@ def _create_sources(self, config: Config) -> list[ClipSource]: camera_name=camera.name, ) sources.append(source) + self._sources_by_camera[camera.name] = source return sources @@ -300,6 +304,10 @@ async def shutdown(self) -> None: """Graceful shutdown of all components.""" logger.info("Shutting down application...") + # Stop API server + if self._api_server: + await self._api_server.stop() + # Stop sources first if self._sources: await asyncio.gather( @@ -311,10 +319,6 @@ async def shutdown(self) -> None: if self._pipeline: await self._pipeline.shutdown() - # Stop health server - if self._health_server: - await self._health_server.stop() - # Close filter and VLM plugins if self._filter: await self._filter.shutdown() @@ -334,3 +338,40 @@ async def shutdown(self) -> None: await self._notifier.shutdown() logger.info("Application shutdown complete") + + @property + def config(self) -> Config: + return self._require_config() + + @property + def repository(self) -> ClipRepository: + if self._repository is None: + raise RuntimeError("Repository not initialized") + return self._repository + + @property + def storage(self) -> StorageBackend: + if self._storage is None: + raise RuntimeError("Storage not initialized") + return self._storage + + @property + def sources(self) -> list[ClipSource]: + return list(self._sources) + + @property + def config_manager(self) -> ConfigManager: + return self._config_manager + + @property + def pipeline_running(self) -> bool: + return self._pipeline is not None and not self._shutdown_event.is_set() + + @property + def uptime_seconds(self) -> float: + if self._start_time is None: + return 0.0 + return time.time() - self._start_time + + def get_source(self, camera_name: str) -> ClipSource | None: + return self._sources_by_camera.get(camera_name) diff --git a/src/homesec/config/manager.py b/src/homesec/config/manager.py new file mode 100644 index 00000000..1db57882 --- /dev/null +++ b/src/homesec/config/manager.py @@ -0,0 +1,130 @@ +"""Configuration persistence manager for HomeSec.""" + +from __future__ import annotations + +import asyncio +import os +import shutil +from pathlib import Path + +import yaml +from pydantic import BaseModel + +from homesec.config.loader import ConfigError, load_config, load_config_from_dict +from homesec.models.config import CameraConfig, CameraSourceConfig, Config + + +class ConfigUpdateResult(BaseModel): + """Result of a config update operation.""" + + restart_required: bool = True + + +class ConfigManager: + """Manages configuration persistence (single file, last-write-wins). + + On mutations, backs up current config to {path}.bak before overwriting. + """ + + def __init__(self, config_path: Path) -> None: + self._config_path = config_path + + def get_config(self) -> Config: + """Get the current configuration.""" + return load_config(self._config_path) + + async def add_camera( + self, + name: str, + enabled: bool, + source_backend: str, + source_config: dict[str, object], + ) -> ConfigUpdateResult: + """Add a new camera to the config.""" + config = await asyncio.to_thread(self.get_config) + + if any(camera.name == name for camera in config.cameras): + raise ValueError(f"Camera already exists: {name}") + + config.cameras.append( + CameraConfig( + name=name, + enabled=enabled, + source=CameraSourceConfig(backend=source_backend, config=source_config), + ) + ) + + validated = await self._validate_config(config) + await self._save_config(validated) + return ConfigUpdateResult() + + async def update_camera( + self, + camera_name: str, + enabled: bool | None, + source_config: dict[str, object] | None, + ) -> ConfigUpdateResult: + """Update an existing camera in the config.""" + config = await asyncio.to_thread(self.get_config) + + camera = next((cam for cam in config.cameras if cam.name == camera_name), None) + if camera is None: + raise ValueError(f"Camera not found: {camera_name}") + + if enabled is not None: + camera.enabled = enabled + if source_config is not None: + camera.source = CameraSourceConfig( + backend=camera.source.backend, + config=source_config, + ) + + validated = await self._validate_config(config) + await self._save_config(validated) + return ConfigUpdateResult() + + async def remove_camera( + self, + camera_name: str, + ) -> ConfigUpdateResult: + """Remove a camera from the config.""" + config = await asyncio.to_thread(self.get_config) + + updated = [camera for camera in config.cameras if camera.name != camera_name] + if len(updated) == len(config.cameras): + raise ValueError(f"Camera not found: {camera_name}") + + config.cameras = updated + + validated = await self._validate_config(config) + await self._save_config(validated) + return ConfigUpdateResult() + + async def _validate_config(self, config: Config) -> Config: + """Validate configuration via the standard loader path.""" + payload = config.model_dump(mode="json") + try: + return await asyncio.to_thread(load_config_from_dict, payload) + except ConfigError as exc: + raise ValueError(str(exc)) from exc + + async def _save_config(self, config: Config) -> None: + """Save config to disk with backup.""" + + def _write() -> None: + backup_path = Path(str(self._config_path) + ".bak") + if self._config_path.exists(): + shutil.copy2(self._config_path, backup_path) + + payload = config.model_dump(mode="json") + tmp_path = self._config_path.with_suffix(self._config_path.suffix + ".tmp") + tmp_path.parent.mkdir(parents=True, exist_ok=True) + + with tmp_path.open("w", encoding="utf-8") as handle: + yaml.safe_dump(payload, handle, sort_keys=False) + handle.flush() + os.fsync(handle.fileno()) + + os.replace(tmp_path, self._config_path) + + await asyncio.to_thread(_write) diff --git a/src/homesec/health/__init__.py b/src/homesec/health/__init__.py deleted file mode 100644 index 0b157ba3..00000000 --- a/src/homesec/health/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Health check components.""" - -from homesec.health.server import HealthServer - -__all__ = ["HealthServer"] diff --git a/src/homesec/health/server.py b/src/homesec/health/server.py deleted file mode 100644 index 42549f22..00000000 --- a/src/homesec/health/server.py +++ /dev/null @@ -1,228 +0,0 @@ -"""HTTP health check endpoint.""" - -from __future__ import annotations - -import logging -import time -from typing import TYPE_CHECKING, Any - -from aiohttp import web - -if TYPE_CHECKING: - from homesec.interfaces import ClipSource, Notifier, StateStore, StorageBackend - -logger = logging.getLogger(__name__) - - -class HealthServer: - """HTTP server for health checks. - - Provides /health endpoint returning component status. - """ - - def __init__( - self, - host: str = "0.0.0.0", - port: int = 8080, - ) -> None: - """Initialize health server. - - Args: - host: Host to bind to - port: Port to bind to - """ - self.host = host - self.port = port - - # Components to check (set via set_components) - self._state_store: StateStore | None = None - self._storage: StorageBackend | None = None - self._notifier: Notifier | None = None - self._sources: list[ClipSource] = [] - self._mqtt_is_critical = False - - # Server state - self._app: web.Application | None = None - self._runner: web.AppRunner | None = None - self._site: web.TCPSite | None = None - - logger.info("HealthServer initialized: %s:%d", host, port) - - def set_components( - self, - *, - state_store: StateStore | None = None, - storage: StorageBackend | None = None, - notifier: Notifier | None = None, - sources: list[ClipSource] | None = None, - mqtt_is_critical: bool = False, - ) -> None: - """Set components to check. - - Args: - state_store: State store to ping - storage: Storage backend to ping - notifier: Notifier to ping - sources: List of clip sources to check - mqtt_is_critical: Whether MQTT failure is critical (unhealthy vs degraded) - """ - self._state_store = state_store - self._storage = storage - self._notifier = notifier - self._sources = sources or [] - self._mqtt_is_critical = mqtt_is_critical - - async def start(self) -> None: - """Start HTTP server.""" - self._app = web.Application() - self._app.router.add_get("/health", self._health_handler) - - self._runner = web.AppRunner(self._app) - await self._runner.setup() - - self._site = web.TCPSite(self._runner, self.host, self.port) - await self._site.start() - - logger.info("HealthServer started: http://%s:%d/health", self.host, self.port) - - async def stop(self) -> None: - """Stop HTTP server.""" - if self._runner: - await self._runner.cleanup() - - self._app = None - self._runner = None - self._site = None - - logger.info("HealthServer stopped") - - async def _health_handler(self, request: web.Request) -> web.Response: - """Handle GET /health request.""" - health_data = await self.compute_health() - return web.json_response(health_data) - - async def compute_health(self) -> dict[str, Any]: - """Compute health status and return JSON data. - - Returns: - Health data dict with status, checks, warnings - """ - sources = self._get_source_health() - # Run component checks - checks = { - "db": await self._check_db(), - "storage": await self._check_storage(), - "mqtt": await self._check_mqtt(), - "sources": self._check_sources(), - } - - # Compute overall status - status = self._compute_status(checks) - - # Generate warnings - warnings = self._compute_warnings() - - return { - "status": status, - "checks": checks, - "warnings": warnings, - "sources": sources, - } - - async def _check_db(self) -> bool: - """Check if state store is healthy.""" - return await self._check_component(self._state_store, "State store") - - async def _check_storage(self) -> bool: - """Check if storage backend is healthy.""" - return await self._check_component(self._storage, "Storage") - - async def _check_mqtt(self) -> bool: - """Check if notifier is healthy.""" - return await self._check_component(self._notifier, "Notifier") - - def _check_sources(self) -> bool: - """Check if all clip sources are healthy.""" - if not self._sources: - return True # No sources configured - - # All sources must be healthy - return all(source.is_healthy() for source in self._sources) - - def _get_source_health(self) -> list[dict[str, object]]: - """Return per-source health detail.""" - if not self._sources: - return [] - - current_time = time.monotonic() - details: list[dict[str, object]] = [] - for source in self._sources: - camera_name = getattr(source, "camera_name", "unknown") - last_heartbeat = source.last_heartbeat() - details.append( - { - "name": camera_name, - "healthy": source.is_healthy(), - "last_heartbeat": last_heartbeat, - "last_heartbeat_age_s": round(current_time - last_heartbeat, 3), - } - ) - return details - - async def _check_component( - self, - component: StateStore | StorageBackend | Notifier | None, - label: str, - ) -> bool: - if component is None: - return True - - try: - return await component.ping() - except Exception as e: - logger.warning("%s health check failed: %s", label, e, exc_info=True) - return False - - def _compute_status(self, checks: dict[str, bool]) -> str: - """Compute overall health status. - - Args: - checks: Dict of component check results - - Returns: - "healthy", "degraded", or "unhealthy" - """ - # Critical checks (unhealthy if fail) - if not checks["sources"]: - return "unhealthy" - - if not checks["storage"]: - return "unhealthy" - - # MQTT can be critical (configurable) - if not checks["mqtt"] and self._mqtt_is_critical: - return "unhealthy" - - # Non-critical checks (degraded if fail) - if not checks["db"] or not checks["mqtt"]: - return "degraded" - - return "healthy" - - def _compute_warnings(self) -> list[str]: - """Generate warnings for stale heartbeats. - - Returns: - List of warning messages - """ - warnings: list[str] = [] - - # Check source heartbeats (warn if > 2 minutes) - current_time = time.monotonic() - for source in self._sources: - heartbeat_age = current_time - source.last_heartbeat() - if heartbeat_age > 120: # 2 minutes - camera_name = getattr(source, "camera_name", "unknown") - warnings.append(f"source_{camera_name}_heartbeat_stale") - - return warnings diff --git a/src/homesec/interfaces.py b/src/homesec/interfaces.py index 68297bbd..0df88315 100644 --- a/src/homesec/interfaces.py +++ b/src/homesec/interfaces.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import TYPE_CHECKING +from homesec.models.enums import ClipStatus + if TYPE_CHECKING: from homesec.models.alert import Alert, AlertDecision from homesec.models.clip import Clip, ClipStateData @@ -133,6 +135,43 @@ async def get(self, clip_id: str) -> ClipStateData | None: """Retrieve clip state. Returns None if not found.""" raise NotImplementedError + @abstractmethod + async def get_clip(self, clip_id: str) -> ClipStateData | None: + """Get clip state by ID.""" + raise NotImplementedError + + @abstractmethod + async def list_clips( + self, + *, + camera: str | None = None, + status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: + """List clips with filtering and pagination.""" + raise NotImplementedError + + @abstractmethod + async def mark_clip_deleted(self, clip_id: str) -> ClipStateData: + """Mark a clip as deleted.""" + raise NotImplementedError + + @abstractmethod + async def count_clips_since(self, since: datetime) -> int: + """Count clips created since the given timestamp.""" + raise NotImplementedError + + @abstractmethod + async def count_alerts_since(self, since: datetime) -> int: + """Count alert events since the given timestamp.""" + raise NotImplementedError + @abstractmethod async def list_candidate_clips_for_cleanup( self, diff --git a/src/homesec/models/__init__.py b/src/homesec/models/__init__.py index b8ff187f..8e692bfa 100644 --- a/src/homesec/models/__init__.py +++ b/src/homesec/models/__init__.py @@ -8,7 +8,7 @@ CameraSourceConfig, ConcurrencyConfig, Config, - HealthConfig, + FastAPIServerConfig, NotifierConfig, RetentionConfig, RetryConfig, @@ -44,7 +44,7 @@ "FilterConfig", "FilterOverrides", "FilterResult", - "HealthConfig", + "FastAPIServerConfig", "NotifierConfig", "RetentionConfig", "RetryConfig", diff --git a/src/homesec/models/clip.py b/src/homesec/models/clip.py index 48d0937f..66b2829a 100644 --- a/src/homesec/models/clip.py +++ b/src/homesec/models/clip.py @@ -32,6 +32,7 @@ class ClipStateData(BaseModel): """Lightweight snapshot of current clip state (stored in clip_states.data JSONB).""" schema_version: int = 1 + clip_id: str | None = None camera_name: str # High-level status for queries @@ -46,6 +47,7 @@ class ClipStateData(BaseModel): filter_result: FilterResult | None = None analysis_result: AnalysisResult | None = None alert_decision: AlertDecision | None = None + created_at: datetime | None = None @property def upload_completed(self) -> bool: diff --git a/src/homesec/models/config.py b/src/homesec/models/config.py index e482cc9c..2800569a 100644 --- a/src/homesec/models/config.py +++ b/src/homesec/models/config.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from typing import Any from pydantic import BaseModel, Field, field_validator, model_validator @@ -100,12 +101,27 @@ class RetryConfig(BaseModel): backoff_s: float = Field(default=1.0, ge=0.0) -class HealthConfig(BaseModel): - """Health endpoint configuration.""" +class FastAPIServerConfig(BaseModel): + """Configuration for the FastAPI server.""" + enabled: bool = True host: str = "0.0.0.0" port: int = 8080 - mqtt_is_critical: bool = False + cors_origins: list[str] = Field(default_factory=lambda: ["*"]) + auth_enabled: bool = False + api_key_env: str | None = None # Env var name, not the key itself + + def get_api_key(self) -> str | None: + """Resolve API key from environment variable.""" + if not self.api_key_env: + return None + return os.environ.get(self.api_key_env) + + @model_validator(mode="after") + def _validate_auth(self) -> FastAPIServerConfig: + if self.auth_enabled and not self.api_key_env: + raise ValueError("server.api_key_env is required when server.auth_enabled is true") + return self class CameraSourceConfig(BaseModel): @@ -128,6 +144,7 @@ class CameraConfig(BaseModel): """Camera configuration and clip source selection.""" name: str + enabled: bool = True source: CameraSourceConfig @@ -142,7 +159,7 @@ class Config(BaseModel): retention: RetentionConfig = Field(default_factory=RetentionConfig) concurrency: ConcurrencyConfig = Field(default_factory=ConcurrencyConfig) retry: RetryConfig = Field(default_factory=RetryConfig) - health: HealthConfig = Field(default_factory=HealthConfig) + server: FastAPIServerConfig = Field(default_factory=FastAPIServerConfig) filter: FilterConfig vlm: VLMConfig alert_policy: AlertPolicyConfig diff --git a/src/homesec/repository/clip_repository.py b/src/homesec/repository/clip_repository.py index 0e65ae29..8a8d0690 100644 --- a/src/homesec/repository/clip_repository.py +++ b/src/homesec/repository/clip_repository.py @@ -477,6 +477,84 @@ async def mark_done(self, clip_id: str) -> ClipStateData | None: await self._safe_upsert(clip_id, state) return state + async def get_clip(self, clip_id: str) -> ClipStateData | None: + """Get clip state by ID.""" + return await self._safe_get(clip_id) + + async def list_clips( + self, + *, + camera: str | None = None, + status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: + """List clips with filtering and pagination.""" + try: + return await self._run_with_retries( + label="State store list clips", + clip_id="list", + op=lambda: self._state.list_clips( + camera=camera, + status=status, + alerted=alerted, + risk_level=risk_level, + activity_type=activity_type, + since=since, + until=until, + offset=offset, + limit=limit, + ), + ) + except Exception as exc: + logger.error("State store list clips failed: %s", exc, exc_info=exc) + return ([], 0) + + async def delete_clip(self, clip_id: str) -> ClipStateData: + """Mark clip as deleted in database and return state.""" + return await self._run_with_retries( + label="State store mark deleted", + clip_id=clip_id, + op=lambda: self._state.mark_clip_deleted(clip_id), + ) + + async def count_clips_since(self, since: datetime) -> int: + """Count clips created since the given timestamp.""" + try: + return await self._run_with_retries( + label="State store count clips", + clip_id="count", + op=lambda: self._state.count_clips_since(since), + ) + except Exception as exc: + logger.error("State store count clips failed: %s", exc, exc_info=exc) + return 0 + + async def count_alerts_since(self, since: datetime) -> int: + """Count alert events since the given timestamp.""" + try: + return await self._run_with_retries( + label="State store count alerts", + clip_id="count", + op=lambda: self._state.count_alerts_since(since), + ) + except Exception as exc: + logger.error("State store count alerts failed: %s", exc, exc_info=exc) + return 0 + + async def ping(self) -> bool: + """Health check - verify database is reachable.""" + try: + return await self._state.ping() + except Exception as exc: + logger.error("State store ping failed: %s", exc, exc_info=exc) + return False + async def _load_state(self, clip_id: str, *, action: str) -> ClipStateData | None: state = await self._safe_get(clip_id) if state is None: @@ -485,11 +563,14 @@ async def _load_state(self, clip_id: str, *, action: str) -> ClipStateData | Non async def _safe_get(self, clip_id: str) -> ClipStateData | None: try: - return await self._run_with_retries( + state = await self._run_with_retries( label="State store get", clip_id=clip_id, op=lambda: self._state.get(clip_id), ) + if state is not None and state.clip_id is None: + state.clip_id = clip_id + return state except Exception as exc: logger.error( "State store get failed for %s after retries: %s", @@ -500,6 +581,7 @@ async def _safe_get(self, clip_id: str) -> ClipStateData | None: return None async def _safe_upsert(self, clip_id: str, state: ClipStateData) -> None: + state.clip_id = clip_id try: await self._run_with_retries( label="State store upsert", diff --git a/src/homesec/state/postgres.py b/src/homesec/state/postgres.py index 15d7a8d7..468530be 100644 --- a/src/homesec/state/postgres.py +++ b/src/homesec/state/postgres.py @@ -19,6 +19,7 @@ or_, select, text, + update, ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -28,7 +29,7 @@ from homesec.interfaces import EventStore, StateStore from homesec.models.clip import ClipStateData -from homesec.models.enums import EventType +from homesec.models.enums import ClipStatus, EventType from homesec.models.events import ( AlertDecisionMadeEvent, ClipDeletedEvent, @@ -212,6 +213,10 @@ async def get(self, clip_id: str) -> ClipStateData | None: Graceful degradation: returns None if DB unavailable or error occurs. """ + return await self.get_clip(clip_id) + + async def get_clip(self, clip_id: str) -> ClipStateData | None: + """Get clip state by ID.""" if self._engine is None: logger.warning("StateStore not initialized, returning None for %s", clip_id) return None @@ -219,14 +224,17 @@ async def get(self, clip_id: str) -> ClipStateData | None: try: async with self._engine.connect() as conn: result = await conn.execute( - select(ClipState.data).where(ClipState.clip_id == clip_id) + select(ClipState.data, ClipState.created_at).where(ClipState.clip_id == clip_id) ) - raw = result.scalar_one_or_none() - if raw is None: + row = result.one_or_none() + + if row is None: return None - # Parse JSON and validate with Pydantic + raw, created_at = row data_dict = self._parse_state_data(raw) + data_dict["clip_id"] = clip_id + data_dict["created_at"] = created_at return ClipStateData.model_validate(data_dict) except Exception as e: logger.error( @@ -237,6 +245,144 @@ async def get(self, clip_id: str) -> ClipStateData | None: ) return None + async def list_clips( + self, + *, + camera: str | None = None, + status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: + """List clips with filtering and pagination.""" + if self._engine is None: + logger.warning("StateStore not initialized, returning empty clip list") + return ([], 0) + + status_expr = func.jsonb_extract_path_text(ClipState.data, "status") + camera_expr = func.jsonb_extract_path_text(ClipState.data, "camera_name") + alerted_expr = func.jsonb_extract_path_text(ClipState.data, "alert_decision", "notify") + risk_expr = func.jsonb_extract_path_text(ClipState.data, "analysis_result", "risk_level") + activity_expr = func.jsonb_extract_path_text( + ClipState.data, "analysis_result", "activity_type" + ) + + conditions = [] + if camera is not None: + conditions.append(camera_expr == camera) + if status is not None: + conditions.append(status_expr == status.value) + if alerted is True: + conditions.append(alerted_expr == "true") + elif alerted is False: + conditions.append(or_(alerted_expr == "false", alerted_expr.is_(None))) + if risk_level is not None: + conditions.append(risk_expr == risk_level.lower()) + if activity_type is not None: + conditions.append(activity_expr == activity_type) + if since is not None: + conditions.append(ClipState.created_at >= since) + if until is not None: + conditions.append(ClipState.created_at <= until) + + where_clause = and_(*conditions) if conditions else None + + query = select(ClipState.clip_id, ClipState.data, ClipState.created_at) + if where_clause is not None: + query = query.where(where_clause) + query = query.order_by(ClipState.created_at.desc(), ClipState.clip_id.desc()) + query = query.offset(int(offset)).limit(int(limit)) + + count_query = select(func.count()).select_from(ClipState) + if where_clause is not None: + count_query = count_query.where(where_clause) + + try: + async with self._engine.connect() as conn: + result = await conn.execute(query) + rows = result.all() + count_result = await conn.execute(count_query) + total = int(count_result.scalar() or 0) + except Exception as e: + logger.error("Failed to list clips: %s", e, exc_info=True) + return ([], 0) + + items: list[ClipStateData] = [] + for clip_id, raw, created_at in rows: + try: + data_dict = self._parse_state_data(raw) + data_dict["clip_id"] = clip_id + data_dict["created_at"] = created_at + items.append(ClipStateData.model_validate(data_dict)) + except Exception as exc: + logger.warning("Failed parsing clip state %s: %s", clip_id, exc, exc_info=True) + + return (items, total) + + async def mark_clip_deleted(self, clip_id: str) -> ClipStateData: + """Mark a clip as deleted.""" + state = await self.get_clip(clip_id) + if state is None: + raise ValueError(f"Clip not found: {clip_id}") + + state.status = ClipStatus.DELETED + state.clip_id = clip_id + + if self._engine is None: + raise RuntimeError("StateStore not initialized") + + payload = state.model_dump(mode="json") + stmt = ( + update(ClipState) + .where(ClipState.clip_id == clip_id) + .values(data=payload, updated_at=func.now()) + ) + async with self._engine.begin() as conn: + await conn.execute(stmt) + + return state + + async def count_clips_since(self, since: datetime) -> int: + """Count clips created since the given timestamp.""" + if self._engine is None: + return 0 + + query = select(func.count()).select_from(ClipState).where(ClipState.created_at >= since) + try: + async with self._engine.connect() as conn: + result = await conn.execute(query) + return int(result.scalar() or 0) + except Exception as e: + logger.warning("Failed to count clips: %s", e, exc_info=True) + return 0 + + async def count_alerts_since(self, since: datetime) -> int: + """Count alert events (notification_sent) since the given timestamp.""" + if self._engine is None: + return 0 + + query = ( + select(func.count()) + .select_from(ClipEvent) + .where( + and_( + ClipEvent.event_type == EventType.NOTIFICATION_SENT, + ClipEvent.timestamp >= since, + ) + ) + ) + try: + async with self._engine.connect() as conn: + result = await conn.execute(query) + return int(result.scalar() or 0) + except Exception as e: + logger.warning("Failed to count alerts: %s", e, exc_info=True) + return 0 + async def list_candidate_clips_for_cleanup( self, *, @@ -299,6 +445,8 @@ async def list_candidate_clips_for_cleanup( for clip_id, raw, created_at in rows: try: data_dict = self._parse_state_data(raw) + data_dict["clip_id"] = clip_id + data_dict["created_at"] = created_at state = ClipStateData.model_validate(data_dict) except Exception as exc: logger.warning( @@ -504,6 +652,44 @@ async def upsert(self, clip_id: str, data: ClipStateData) -> None: async def get(self, clip_id: str) -> ClipStateData | None: return None + async def get_clip(self, clip_id: str) -> ClipStateData | None: + return None + + async def list_clips( + self, + *, + camera: str | None = None, + status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: + _ = camera + _ = status + _ = alerted + _ = risk_level + _ = activity_type + _ = since + _ = until + _ = offset + _ = limit + return ([], 0) + + async def mark_clip_deleted(self, clip_id: str) -> ClipStateData: + raise ValueError(f"Clip not found: {clip_id}") + + async def count_clips_since(self, since: datetime) -> int: + _ = since + return 0 + + async def count_alerts_since(self, since: datetime) -> int: + _ = since + return 0 + async def list_candidate_clips_for_cleanup( self, *, diff --git a/tests/homesec/mocks/state_store.py b/tests/homesec/mocks/state_store.py index ef21f710..093d3a7a 100644 --- a/tests/homesec/mocks/state_store.py +++ b/tests/homesec/mocks/state_store.py @@ -6,6 +6,7 @@ from datetime import datetime from homesec.models.clip import ClipStateData +from homesec.models.enums import ClipStatus class MockStateStore: @@ -56,6 +57,72 @@ async def get(self, clip_id: str) -> ClipStateData | None: return self.states.get(clip_id) + async def get_clip(self, clip_id: str) -> ClipStateData | None: + """Retrieve clip state by ID (mock implementation).""" + return await self.get(clip_id) + + async def list_clips( + self, + *, + camera: str | None = None, + status: ClipStatus | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: datetime | None = None, + until: datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: + items = list(self.states.values()) + + if camera is not None: + items = [item for item in items if item.camera_name == camera] + if status is not None: + items = [item for item in items if str(item.status) == status.value] + if alerted is True: + items = [item for item in items if item.alert_decision and item.alert_decision.notify] + elif alerted is False: + items = [ + item for item in items if not (item.alert_decision and item.alert_decision.notify) + ] + if risk_level is not None: + items = [ + item + for item in items + if item.analysis_result and str(item.analysis_result.risk_level) == risk_level + ] + if activity_type is not None: + items = [ + item + for item in items + if item.analysis_result and item.analysis_result.activity_type == activity_type + ] + if since is not None: + items = [item for item in items if item.created_at and item.created_at >= since] + if until is not None: + items = [item for item in items if item.created_at and item.created_at <= until] + + total = len(items) + sliced = items[int(offset) : int(offset) + int(limit)] + return (sliced, total) + + async def mark_clip_deleted(self, clip_id: str) -> ClipStateData: + state = self.states.get(clip_id) + if state is None: + raise ValueError(f"Clip not found: {clip_id}") + state.status = ClipStatus.DELETED + self.states[clip_id] = state + return state + + async def count_clips_since(self, since: datetime) -> int: + _ = since + return len(self.states) + + async def count_alerts_since(self, since: datetime) -> int: + _ = since + return 0 + async def list_candidate_clips_for_cleanup( self, *, diff --git a/tests/homesec/test_api_routes.py b/tests/homesec/test_api_routes.py new file mode 100644 index 00000000..0807831c --- /dev/null +++ b/tests/homesec/test_api_routes.py @@ -0,0 +1,825 @@ +"""Tests for FastAPI camera and clip routes.""" + +from __future__ import annotations + +import datetime as dt +import os +import signal +from types import SimpleNamespace + +import pytest +import yaml +from fastapi.testclient import TestClient +from pydantic import BaseModel + +from homesec.api.server import create_app +from homesec.config.manager import ConfigManager +from homesec.models.alert import AlertDecision +from homesec.models.clip import ClipStateData +from homesec.models.config import CameraConfig, CameraSourceConfig, FastAPIServerConfig +from homesec.models.enums import ClipStatus, RiskLevel +from homesec.models.filter import FilterResult +from homesec.models.vlm import AnalysisResult + + +class _StubRepository: + def __init__( + self, + clips: list[ClipStateData] | None = None, + ok: bool = True, + clips_count: int | None = None, + alerts_count: int | None = None, + ) -> None: + self._clips = clips or [] + self._ok = ok + self._clips_count = clips_count + self._alerts_count = alerts_count + + async def ping(self) -> bool: + return self._ok + + async def list_clips( + self, + *, + camera: str | None = None, + status: object | None = None, + alerted: bool | None = None, + risk_level: str | None = None, + activity_type: str | None = None, + since: dt.datetime | None = None, + until: dt.datetime | None = None, + offset: int = 0, + limit: int = 50, + ) -> tuple[list[ClipStateData], int]: + _ = camera + _ = status + _ = alerted + _ = risk_level + _ = activity_type + _ = since + _ = until + total = len(self._clips) + return (self._clips[offset : offset + limit], total) + + async def get_clip(self, clip_id: str) -> ClipStateData | None: + for clip in self._clips: + if clip.clip_id == clip_id: + return clip + return None + + async def delete_clip(self, clip_id: str) -> ClipStateData: + clip = await self.get_clip(clip_id) + if clip is None: + raise ValueError(f"Clip not found: {clip_id}") + return clip + + async def count_clips_since(self, since: dt.datetime) -> int: + _ = since + if self._clips_count is not None: + return self._clips_count + return len(self._clips) + + async def count_alerts_since(self, since: dt.datetime) -> int: + _ = since + if self._alerts_count is not None: + return self._alerts_count + return 0 + + +class _StubStorage: + def __init__(self, *, ok: bool = True, fail_delete: bool = False) -> None: + self._ok = ok + self._fail_delete = fail_delete + + async def ping(self) -> bool: + return self._ok + + async def delete(self, storage_uri: str) -> None: + _ = storage_uri + if self._fail_delete: + raise RuntimeError("delete failed") + return None + + +class _StubSource: + def __init__(self, healthy: bool = True, heartbeat: float = 0.0) -> None: + self._healthy = healthy + self._heartbeat = heartbeat + + def is_healthy(self) -> bool: + return self._healthy + + def last_heartbeat(self) -> float: + return self._heartbeat + + +class _StubApp: + def __init__( + self, + *, + config_manager: ConfigManager, + repository: _StubRepository, + storage: _StubStorage, + sources_by_name: dict[str, _StubSource] | None = None, + server_config: FastAPIServerConfig | None = None, + pipeline_running: bool = True, + ) -> None: + self.config_manager = config_manager + self.repository = repository + self.storage = storage + self._sources_by_name = sources_by_name or {} + self.sources = list(self._sources_by_name.values()) + self._config = SimpleNamespace( + server=server_config or FastAPIServerConfig(), + cameras=[ + CameraConfig( + name=name, + enabled=True, + source=CameraSourceConfig( + backend="local_folder", + config={"watch_dir": "/tmp"}, + ), + ) + for name in self._sources_by_name + ], + ) + self._pipeline_running = pipeline_running + self.uptime_seconds = 0.0 + + @property + def config(self): # type: ignore[override] + return self._config + + @property + def pipeline_running(self) -> bool: + return self._pipeline_running + + def get_source(self, camera_name: str) -> _StubSource | None: + return self._sources_by_name.get(camera_name) + + +def _write_config(tmp_path, cameras: list[dict]) -> ConfigManager: + payload = { + "version": 1, + "cameras": cameras, + "storage": {"backend": "dropbox", "config": {"root": "/homecam"}}, + "state_store": {"dsn": "postgresql://user:pass@localhost/db"}, + "notifiers": [{"backend": "mqtt", "config": {"host": "localhost"}}], + "filter": {"backend": "yolo", "config": {}}, + "vlm": { + "backend": "openai", + "config": {"api_key_env": "OPENAI_API_KEY", "model": "gpt-4o"}, + }, + "alert_policy": {"backend": "default", "config": {}}, + } + path = tmp_path / "config.yaml" + path.write_text(yaml.safe_dump(payload, sort_keys=False)) + return ConfigManager(path) + + +def _client(app: _StubApp) -> TestClient: + return TestClient(create_app(app)) + + +def test_create_camera(tmp_path) -> None: + """POST /cameras should create a camera.""" + # Given a config with no cameras + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When creating a camera + response = client.post( + "/api/v1/cameras", + json={ + "name": "front", + "enabled": True, + "source_backend": "local_folder", + "source_config": {"watch_dir": "/tmp"}, + }, + ) + + # Then it is created + assert response.status_code == 201 + payload = response.json() + assert payload["restart_required"] is True + assert payload["camera"]["name"] == "front" + + +def test_create_camera_duplicate_returns_400(tmp_path) -> None: + """POST /cameras should reject duplicate names.""" + # Given a config with an existing camera + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When creating a camera with the same name + response = client.post( + "/api/v1/cameras", + json={ + "name": "front", + "enabled": True, + "source_backend": "local_folder", + "source_config": {"watch_dir": "/tmp"}, + }, + ) + + # Then it returns a 400 + assert response.status_code == 400 + + +def test_get_camera(tmp_path) -> None: + """GET /cameras/{name} should return a camera.""" + # Given a config with one camera + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When requesting the camera + response = client.get("/api/v1/cameras/front") + + # Then it returns the camera + assert response.status_code == 200 + payload = response.json() + assert payload["name"] == "front" + assert payload["enabled"] is True + + +def test_get_camera_missing_returns_404(tmp_path) -> None: + """GET /cameras/{name} should return 404 when missing.""" + # Given a config with no cameras + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When requesting a missing camera + response = client.get("/api/v1/cameras/missing") + + # Then it returns 404 + assert response.status_code == 404 + + +def test_list_cameras_includes_health_fields(tmp_path) -> None: + """GET /cameras should include health fields.""" + # Given a config with one camera and an unhealthy source + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + sources = {"front": _StubSource(healthy=False, heartbeat=12.3)} + app = _StubApp( + config_manager=manager, + repository=_StubRepository(), + storage=_StubStorage(), + sources_by_name=sources, + ) + client = _client(app) + + # When listing cameras + response = client.get("/api/v1/cameras") + + # Then it includes health data + assert response.status_code == 200 + payload = response.json() + assert payload[0]["healthy"] is False + assert payload[0]["last_heartbeat"] == 12.3 + + +def test_list_cameras_serializes_model_config(tmp_path) -> None: + """GET /cameras should serialize BaseModel configs.""" + # Given a config with a BaseModel-backed source config + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + config = manager.get_config() + + class _ModelConfig(BaseModel): + watch_dir: str + + config.cameras[0].source.config = _ModelConfig(watch_dir="/tmp") + + class _StaticConfigManager: + def __init__(self, cfg) -> None: + self._cfg = cfg + + def get_config(self): + return self._cfg + + sources = {"front": _StubSource(healthy=True)} + app = _StubApp( + config_manager=_StaticConfigManager(config), + repository=_StubRepository(), + storage=_StubStorage(), + sources_by_name=sources, + ) + client = _client(app) + + # When listing cameras + response = client.get("/api/v1/cameras") + + # Then the config is serialized as a dict + assert response.status_code == 200 + payload = response.json() + assert payload[0]["source_config"]["watch_dir"] == "/tmp" + + +def test_delete_camera(tmp_path) -> None: + """DELETE /cameras/{name} should remove a camera.""" + # Given a config with one camera + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When deleting the camera + response = client.delete("/api/v1/cameras/front") + + # Then it is removed + assert response.status_code == 200 + payload = response.json() + assert payload["restart_required"] is True + + +def test_delete_camera_missing_returns_404(tmp_path) -> None: + """DELETE /cameras/{name} should return 404 when missing.""" + # Given a config with no cameras + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When deleting a missing camera + response = client.delete("/api/v1/cameras/missing") + + # Then it returns 404 + assert response.status_code == 404 + + +def test_update_camera(tmp_path) -> None: + """PUT /cameras/{name} should update camera fields.""" + # Given a config with one camera + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When updating the camera + response = client.put( + "/api/v1/cameras/front", + json={"enabled": False, "source_config": {"watch_dir": "/new"}}, + ) + + # Then it returns updated fields + assert response.status_code == 200 + payload = response.json() + assert payload["camera"]["enabled"] is False + assert payload["camera"]["source_config"]["watch_dir"] == "/new" + + +def test_update_camera_invalid_config_returns_400(tmp_path) -> None: + """PUT /cameras/{name} should return 400 for invalid config.""" + # Given a config with one camera + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When updating with an invalid source config + response = client.put( + "/api/v1/cameras/front", + json={"source_config": {"poll_interval": -1.0}}, + ) + + # Then it returns 400 + assert response.status_code == 400 + + +def test_update_camera_missing_returns_404(tmp_path) -> None: + """PUT /cameras/{name} should return 404 when missing.""" + # Given a config with no cameras + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When updating a missing camera + response = client.put("/api/v1/cameras/missing", json={"enabled": False}) + + # Then it returns 404 + assert response.status_code == 404 + + +def test_get_config_returns_full_config(tmp_path) -> None: + """GET /config should return the full configuration.""" + # Given a config with one camera + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When requesting config + response = client.get("/api/v1/config") + + # Then it returns the config payload + assert response.status_code == 200 + payload = response.json() + assert payload["config"]["version"] == 1 + assert payload["config"]["cameras"][0]["name"] == "front" + assert payload["config"]["server"]["enabled"] is True + + +def test_list_clips_pagination(tmp_path) -> None: + """GET /clips should paginate results.""" + # Given 100 clips in the repository + now = dt.datetime.now(dt.timezone.utc) + clips = [ + ClipStateData( + clip_id=f"clip-{idx}", + camera_name="front", + status="uploaded", + local_path=f"/tmp/{idx}.mp4", + created_at=now + dt.timedelta(seconds=idx), + ) + for idx in range(100) + ] + manager = _write_config(tmp_path, cameras=[]) + repository = _StubRepository(clips=clips) + app = _StubApp(config_manager=manager, repository=repository, storage=_StubStorage()) + client = _client(app) + + # When requesting page 2 + response = client.get("/api/v1/clips?page=2&page_size=10") + + # Then it returns the second page + assert response.status_code == 200 + payload = response.json() + assert payload["total"] == 100 + assert payload["page"] == 2 + assert payload["page_size"] == 10 + assert len(payload["clips"]) == 10 + + +def test_get_clip_includes_analysis_and_alert_details(tmp_path) -> None: + """GET /clips/{id} should include analysis, detection, and alert fields.""" + # Given a clip with analysis and alert details + created_at = dt.datetime(2024, 1, 1, tzinfo=dt.timezone.utc) + clip = ClipStateData( + clip_id="clip-1", + camera_name="front", + status=ClipStatus.ANALYZED, + local_path="/tmp/clip-1.mp4", + storage_uri="dropbox://clip-1.mp4", + view_url="https://example/view", + created_at=created_at, + filter_result=FilterResult( + detected_classes=["person", "package"], + confidence=0.91, + model="yolo", + sampled_frames=12, + ), + analysis_result=AnalysisResult( + risk_level=RiskLevel.HIGH, + activity_type="delivery", + summary="Package drop", + ), + alert_decision=AlertDecision( + notify=True, + notify_reason="risk_level=high", + ), + ) + manager = _write_config(tmp_path, cameras=[]) + repository = _StubRepository(clips=[clip]) + app = _StubApp(config_manager=manager, repository=repository, storage=_StubStorage()) + client = _client(app) + + # When requesting the clip + response = client.get("/api/v1/clips/clip-1") + + # Then it returns detailed clip data + assert response.status_code == 200 + payload = response.json() + assert payload["id"] == "clip-1" + assert payload["camera"] == "front" + assert payload["status"] == "analyzed" + returned_created_at = dt.datetime.fromisoformat(payload["created_at"].replace("Z", "+00:00")) + assert returned_created_at == created_at + assert payload["detected_objects"] == ["person", "package"] + assert payload["activity_type"] == "delivery" + assert payload["risk_level"] == "high" + assert payload["summary"] == "Package drop" + assert payload["alerted"] is True + assert payload["storage_uri"] == "dropbox://clip-1.mp4" + assert payload["view_url"] == "https://example/view" + + +def test_get_clip_missing_returns_404(tmp_path) -> None: + """GET /clips/{id} should return 404 when missing.""" + # Given an empty repository + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When requesting a missing clip + response = client.get("/api/v1/clips/missing") + + # Then it returns 404 + assert response.status_code == 404 + + +def test_delete_clip_storage_failure_returns_500(tmp_path) -> None: + """DELETE /clips/{id} should return 500 if storage deletion fails.""" + # Given a clip with a storage URI + clip = ClipStateData( + clip_id="clip-1", + camera_name="front", + status="uploaded", + local_path="/tmp/clip-1.mp4", + storage_uri="dropbox://clip-1.mp4", + ) + manager = _write_config(tmp_path, cameras=[]) + repository = _StubRepository(clips=[clip]) + storage = _StubStorage(fail_delete=True) + app = _StubApp(config_manager=manager, repository=repository, storage=storage) + client = _client(app) + + # When deleting the clip + response = client.delete("/api/v1/clips/clip-1") + + # Then it returns 500 + assert response.status_code == 500 + + +def test_delete_clip_missing_returns_404(tmp_path) -> None: + """DELETE /clips/{id} should return 404 when missing.""" + # Given an empty repository + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + + # When deleting a missing clip + response = client.delete("/api/v1/clips/missing") + + # Then it returns 404 + assert response.status_code == 404 + + +def test_delete_clip_success_removes_storage(tmp_path) -> None: + """DELETE /clips/{id} should return clip data on success.""" + # Given a clip stored in storage + clip = ClipStateData( + clip_id="clip-2", + camera_name="front", + status="uploaded", + local_path="/tmp/clip-2.mp4", + storage_uri="dropbox://clip-2.mp4", + ) + manager = _write_config(tmp_path, cameras=[]) + repository = _StubRepository(clips=[clip]) + + class _TrackingStorage(_StubStorage): + def __init__(self) -> None: + super().__init__(ok=True, fail_delete=False) + self.deleted: list[str] = [] + + async def delete(self, storage_uri: str) -> None: + self.deleted.append(storage_uri) + await super().delete(storage_uri) + + storage = _TrackingStorage() + app = _StubApp(config_manager=manager, repository=repository, storage=storage) + client = _client(app) + + # When deleting the clip + response = client.delete("/api/v1/clips/clip-2") + + # Then it returns clip data and deletes storage + assert response.status_code == 200 + payload = response.json() + assert payload["id"] == "clip-2" + assert payload["storage_uri"] == "dropbox://clip-2.mp4" + assert storage.deleted == ["dropbox://clip-2.mp4"] + + +def test_stats_endpoint_returns_counts(tmp_path) -> None: + """GET /stats should return clip and alert counts.""" + # Given a repository with known counts + manager = _write_config(tmp_path, cameras=[]) + repository = _StubRepository(clips_count=7, alerts_count=2) + app = _StubApp(config_manager=manager, repository=repository, storage=_StubStorage()) + client = _client(app) + + # When requesting stats + response = client.get("/api/v1/stats") + + # Then it returns the counts + assert response.status_code == 200 + payload = response.json() + assert payload["clips_today"] == 7 + assert payload["alerts_today"] == 2 + + +def test_stats_endpoint_includes_camera_counts_and_uptime(tmp_path) -> None: + """GET /stats should include camera totals and uptime.""" + # Given an app with two cameras and one healthy source + manager = _write_config( + tmp_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + }, + { + "name": "back", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + }, + ], + ) + sources = { + "front": _StubSource(healthy=True), + "back": _StubSource(healthy=False), + } + repository = _StubRepository(clips_count=0, alerts_count=0) + app = _StubApp( + config_manager=manager, + repository=repository, + storage=_StubStorage(), + sources_by_name=sources, + ) + app.uptime_seconds = 12.5 + client = _client(app) + + # When requesting stats + response = client.get("/api/v1/stats") + + # Then it returns camera totals and uptime + assert response.status_code == 200 + payload = response.json() + assert payload["cameras_total"] == 2 + assert payload["cameras_online"] == 1 + assert payload["uptime_seconds"] == 12.5 + + +def test_system_restart_calls_sigterm(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: + """POST /system/restart should request SIGTERM.""" + # Given a running app + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp(config_manager=manager, repository=_StubRepository(), storage=_StubStorage()) + client = _client(app) + calls: list[tuple[int, int]] = [] + + def _fake_kill(pid: int, sig: int) -> None: + calls.append((pid, sig)) + + monkeypatch.setattr(os, "kill", _fake_kill) + + # When requesting restart + response = client.post("/api/v1/system/restart") + + # Then it issues SIGTERM + assert response.status_code == 200 + assert calls + assert calls[0][0] == os.getpid() + assert calls[0][1] == signal.SIGTERM + + +def test_db_unavailable_returns_503(tmp_path) -> None: + """Non-health endpoints should return 503 when DB is down.""" + # Given a repository that is unavailable + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp( + config_manager=manager, + repository=_StubRepository(ok=False), + storage=_StubStorage(), + ) + client = _client(app) + + # When requesting a data endpoint + response = client.get("/api/v1/cameras") + + # Then it returns 503 with error code + assert response.status_code == 503 + payload = response.json() + assert payload["error_code"] == "DB_UNAVAILABLE" + + +def test_auth_required_when_enabled(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: + """Auth should be enforced for non-public endpoints.""" + # Given auth is enabled + monkeypatch.setenv("HOMESEC_API_KEY", "secret") + server_config = FastAPIServerConfig(auth_enabled=True, api_key_env="HOMESEC_API_KEY") + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp( + config_manager=manager, + repository=_StubRepository(), + storage=_StubStorage(), + server_config=server_config, + ) + client = _client(app) + + # When missing Authorization header + response = client.get("/api/v1/cameras") + + # Then it returns 401 + assert response.status_code == 401 + + # When using an incorrect token + response = client.get("/api/v1/cameras", headers={"Authorization": "Bearer wrong"}) + + # Then it returns 401 + assert response.status_code == 401 + + # When using the correct token + response = client.get("/api/v1/cameras", headers={"Authorization": "Bearer secret"}) + + # Then it returns 200 + assert response.status_code == 200 + + # When hitting a public endpoint without auth + response = client.get("/api/v1/health") + + # Then it returns 200 + assert response.status_code == 200 + + +def test_auth_env_missing_returns_500(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: + """Auth should return 500 when API key is not configured.""" + # Given auth is enabled but env var missing + monkeypatch.delenv("HOMESEC_API_KEY", raising=False) + server_config = FastAPIServerConfig(auth_enabled=True, api_key_env="HOMESEC_API_KEY") + manager = _write_config(tmp_path, cameras=[]) + app = _StubApp( + config_manager=manager, + repository=_StubRepository(), + storage=_StubStorage(), + server_config=server_config, + ) + client = _client(app) + + # When requesting an authenticated endpoint + response = client.get("/api/v1/cameras") + + # Then it returns 500 + assert response.status_code == 500 diff --git a/tests/homesec/test_app.py b/tests/homesec/test_app.py index 6f3641c1..0c699509 100644 --- a/tests/homesec/test_app.py +++ b/tests/homesec/test_app.py @@ -112,6 +112,12 @@ async def start(self) -> None: def is_healthy(self) -> bool: return True + def last_heartbeat(self) -> float: + return 0.0 + + async def ping(self) -> bool: + return True + async def shutdown(self, timeout: float | None = None) -> None: self.shutdown_called = True @@ -178,8 +184,8 @@ def _mock_validate(*args, **kwargs): @pytest.mark.asyncio -async def test_application_wires_pipeline_and_health(_mock_plugins: None) -> None: - """Application should wire pipeline, sources, and health server.""" +async def test_application_wires_pipeline_and_sources(_mock_plugins: None) -> None: + """Application should wire pipeline and sources.""" # Given a config and stubbed dependencies config = _make_config( [ @@ -195,18 +201,14 @@ async def test_application_wires_pipeline_and_health(_mock_plugins: None) -> Non # When creating components await app._create_components() - # Then pipeline, sources, and health server are wired + # Then pipeline and sources are wired assert app._pipeline is not None - assert app._health_server is not None assert app._sources for source in app._sources: callback = source._callback assert callback is not None assert getattr(callback, "__self__", None) is app._pipeline assert getattr(callback, "__func__", None) is app._pipeline.on_new_clip.__func__ - assert app._health_server._state_store is app._state_store - assert app._health_server._storage is app._storage - assert app._health_server._notifier is app._notifier @pytest.mark.asyncio diff --git a/tests/homesec/test_clip_repository.py b/tests/homesec/test_clip_repository.py index 81f7971e..add306d0 100644 --- a/tests/homesec/test_clip_repository.py +++ b/tests/homesec/test_clip_repository.py @@ -6,15 +6,14 @@ from pathlib import Path import pytest +from sqlalchemy import text +from homesec.models.alert import AlertDecision from homesec.models.clip import Clip -from homesec.models.enums import RiskLevel +from homesec.models.enums import ClipStatus, RiskLevel from homesec.models.events import ClipRecheckedEvent from homesec.models.filter import FilterResult -from homesec.models.vlm import ( - AnalysisResult, - SequenceAnalysis, -) +from homesec.models.vlm import AnalysisResult, SequenceAnalysis from homesec.repository import ClipRepository from homesec.state.postgres import PostgresEventStore, PostgresStateStore @@ -55,6 +54,216 @@ async def test_initialize_clip(postgres_dsn: str, tmp_path: Path, clean_test_db: await state_store.shutdown() +@pytest.mark.asyncio +async def test_count_clips_since(postgres_dsn: str, tmp_path: Path, clean_test_db: None) -> None: + """count_clips_since should include clips created after the cutoff.""" + # Given: A repository with two clips created around a cutoff + state_store = PostgresStateStore(postgres_dsn) + await state_store.initialize() + event_store = state_store.create_event_store() + repository = ClipRepository(state_store, event_store) + + clip_old = Clip( + clip_id="test-clip-old", + camera_name="front_door", + local_path=tmp_path / "old.mp4", + start_ts=datetime.now(), + end_ts=datetime.now(), + duration_s=1.0, + source_backend="test", + ) + await repository.initialize_clip(clip_old) + clip_new = Clip( + clip_id="test-clip-new", + camera_name="front_door", + local_path=tmp_path / "new.mp4", + start_ts=datetime.now(), + end_ts=datetime.now(), + duration_s=1.0, + source_backend="test", + ) + await repository.initialize_clip(clip_new) + + # Given: Normalize timestamps using database time to avoid clock skew + assert state_store._engine is not None + async with state_store._engine.begin() as conn: + result = await conn.execute(text("SELECT now()")) + db_now = result.scalar_one() + await conn.execute( + text("UPDATE clip_states SET created_at = :created_at WHERE clip_id = :clip_id"), + {"created_at": db_now - timedelta(hours=2), "clip_id": clip_old.clip_id}, + ) + + cutoff = db_now - timedelta(hours=1) + + # When: Counting clips since the cutoff + count = await repository.count_clips_since(cutoff) + + # Then: Only the newer clip is counted + assert count == 1 + + await state_store.shutdown() + + +@pytest.mark.asyncio +async def test_count_alerts_since(postgres_dsn: str, tmp_path: Path, clean_test_db: None) -> None: + """count_alerts_since should count notification_sent events.""" + # Given: A repository with one notification_sent event + state_store = PostgresStateStore(postgres_dsn) + await state_store.initialize() + event_store = state_store.create_event_store() + repository = ClipRepository(state_store, event_store) + + clip = Clip( + clip_id="test-clip-alert", + camera_name="front_door", + local_path=tmp_path / "alert.mp4", + start_ts=datetime.now(), + end_ts=datetime.now(), + duration_s=1.0, + source_backend="test", + ) + await repository.initialize_clip(clip) + await repository.record_notification_sent( + clip_id=clip.clip_id, + notifier_name="test", + dedupe_key=clip.clip_id, + ) + + # When: Counting alerts since a recent timestamp + since = datetime.now() - timedelta(minutes=1) + count = await repository.count_alerts_since(since) + + # Then: The alert is counted + assert count == 1 + + await state_store.shutdown() + + +@pytest.mark.asyncio +async def test_list_clips_filters(postgres_dsn: str, tmp_path: Path, clean_test_db: None) -> None: + """list_clips should apply filters correctly.""" + # Given: Two clips with different cameras and risk levels + state_store = PostgresStateStore(postgres_dsn) + await state_store.initialize() + event_store = state_store.create_event_store() + repository = ClipRepository(state_store, event_store) + + clip_front = Clip( + clip_id="test-clip-front", + camera_name="front_door", + local_path=tmp_path / "front.mp4", + start_ts=datetime.now(), + end_ts=datetime.now(), + duration_s=1.0, + source_backend="test", + ) + clip_back = Clip( + clip_id="test-clip-back", + camera_name="back_door", + local_path=tmp_path / "back.mp4", + start_ts=datetime.now(), + end_ts=datetime.now(), + duration_s=1.0, + source_backend="test", + ) + await repository.initialize_clip(clip_front) + await repository.initialize_clip(clip_back) + + await repository.record_vlm_completed( + clip_id=clip_front.clip_id, + result=AnalysisResult( + risk_level="high", + activity_type="suspicious_behavior", + summary="Suspicious", + analysis=None, + ), + prompt_tokens=None, + completion_tokens=None, + duration_ms=10, + ) + await repository.record_vlm_completed( + clip_id=clip_back.clip_id, + result=AnalysisResult( + risk_level="low", + activity_type="passerby", + summary="Normal", + analysis=None, + ), + prompt_tokens=None, + completion_tokens=None, + duration_ms=10, + ) + + await repository.record_alert_decision( + clip_id=clip_front.clip_id, + decision=AlertDecision(notify=True, notify_reason="risk_level=high"), + detected_classes=["person"], + vlm_risk="high", + ) + await repository.record_alert_decision( + clip_id=clip_back.clip_id, + decision=AlertDecision(notify=False, notify_reason="low risk"), + detected_classes=["person"], + vlm_risk="low", + ) + + # When: Listing clips by camera, alert status, and risk level + clips_by_camera, total_by_camera = await repository.list_clips(camera="front_door") + clips_alerted, total_alerted = await repository.list_clips(alerted=True) + clips_high, total_high = await repository.list_clips(risk_level="high") + + # Then: Each filter returns the expected clip + assert total_by_camera == 1 + assert clips_by_camera[0].camera_name == "front_door" + + assert total_alerted == 1 + assert clips_alerted[0].clip_id == clip_front.clip_id + + assert total_high == 1 + assert clips_high[0].clip_id == clip_front.clip_id + + await state_store.shutdown() + + +@pytest.mark.asyncio +async def test_delete_clip_marks_deleted( + postgres_dsn: str, tmp_path: Path, clean_test_db: None +) -> None: + """delete_clip should mark clip as deleted.""" + # Given: A clip with uploaded storage + state_store = PostgresStateStore(postgres_dsn) + await state_store.initialize() + event_store = state_store.create_event_store() + repository = ClipRepository(state_store, event_store) + + clip = Clip( + clip_id="test-clip-delete", + camera_name="front_door", + local_path=tmp_path / "delete.mp4", + start_ts=datetime.now(), + end_ts=datetime.now(), + duration_s=1.0, + source_backend="test", + ) + await repository.initialize_clip(clip) + await repository.record_upload_completed( + clip_id=clip.clip_id, + storage_uri="dropbox://delete.mp4", + view_url=None, + duration_ms=10, + ) + + # When: Deleting the clip + state = await repository.delete_clip(clip.clip_id) + + # Then: State is marked deleted + assert state.status == ClipStatus.DELETED + assert state.storage_uri == "dropbox://delete.mp4" + + await state_store.shutdown() + + @pytest.mark.asyncio async def test_record_upload_completed( postgres_dsn: str, tmp_path: Path, clean_test_db: None diff --git a/tests/homesec/test_config_manager.py b/tests/homesec/test_config_manager.py new file mode 100644 index 00000000..a58835b7 --- /dev/null +++ b/tests/homesec/test_config_manager.py @@ -0,0 +1,152 @@ +"""Tests for ConfigManager.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + +from homesec.config.manager import ConfigManager + + +def _write_config(path: Path, cameras: list[dict[str, object]]) -> ConfigManager: + payload = { + "version": 1, + "cameras": cameras, + "storage": {"backend": "dropbox", "config": {"root": "/homecam"}}, + "state_store": {"dsn": "postgresql://user:pass@localhost/db"}, + "notifiers": [{"backend": "mqtt", "config": {"host": "localhost"}}], + "filter": {"backend": "yolo", "config": {}}, + "vlm": { + "backend": "openai", + "config": {"api_key_env": "OPENAI_API_KEY", "model": "gpt-4o"}, + }, + "alert_policy": {"backend": "default", "config": {}}, + } + path.write_text(yaml.safe_dump(payload, sort_keys=False)) + return ConfigManager(path) + + +@pytest.mark.asyncio +async def test_config_manager_add_update_remove_camera(tmp_path: Path) -> None: + """ConfigManager should add, update, and remove cameras.""" + # Given a config with no cameras + config_path = tmp_path / "config.yaml" + manager = _write_config(config_path, cameras=[]) + + # When adding a camera + await manager.add_camera( + name="front", + enabled=True, + source_backend="local_folder", + source_config={"watch_dir": "/tmp"}, + ) + + # Then the camera is persisted and backup exists + config = manager.get_config() + assert any(camera.name == "front" for camera in config.cameras) + assert (config_path.with_suffix(".yaml.bak")).exists() + + # When updating the camera + await manager.update_camera( + camera_name="front", + enabled=False, + source_config={"watch_dir": "/new"}, + ) + + # Then the camera is updated + config = manager.get_config() + updated = next(camera for camera in config.cameras if camera.name == "front") + assert updated.enabled is False + assert updated.source.config["watch_dir"] == "/new" + + # When removing the camera + await manager.remove_camera(camera_name="front") + + # Then the camera is removed + config = manager.get_config() + assert not config.cameras + + +@pytest.mark.asyncio +async def test_config_manager_add_duplicate_raises(tmp_path: Path) -> None: + """ConfigManager should reject duplicate camera names.""" + # Given a config with one camera + config_path = tmp_path / "config.yaml" + manager = _write_config( + config_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + + # When adding a duplicate camera + with pytest.raises(ValueError): + await manager.add_camera( + name="front", + enabled=True, + source_backend="local_folder", + source_config={"watch_dir": "/tmp"}, + ) + + # Then it raises a validation error + + +@pytest.mark.asyncio +async def test_config_manager_update_missing_raises(tmp_path: Path) -> None: + """ConfigManager should error when updating a missing camera.""" + # Given a config with no cameras + config_path = tmp_path / "config.yaml" + manager = _write_config(config_path, cameras=[]) + + # When updating a missing camera + with pytest.raises(ValueError): + await manager.update_camera(camera_name="missing", enabled=False, source_config=None) + + # Then it raises a validation error + + +@pytest.mark.asyncio +async def test_config_manager_remove_missing_raises(tmp_path: Path) -> None: + """ConfigManager should error when removing a missing camera.""" + # Given a config with no cameras + config_path = tmp_path / "config.yaml" + manager = _write_config(config_path, cameras=[]) + + # When removing a missing camera + with pytest.raises(ValueError): + await manager.remove_camera(camera_name="missing") + + # Then it raises a validation error + + +@pytest.mark.asyncio +async def test_config_manager_invalid_update_raises(tmp_path: Path) -> None: + """ConfigManager should reject invalid camera config updates.""" + # Given a config with one camera + config_path = tmp_path / "config.yaml" + manager = _write_config( + config_path, + cameras=[ + { + "name": "front", + "enabled": True, + "source": {"backend": "local_folder", "config": {"watch_dir": "/tmp"}}, + } + ], + ) + + # When updating with invalid source config + with pytest.raises(ValueError): + await manager.update_camera( + camera_name="front", + enabled=None, + source_config={"poll_interval": -1.0}, + ) + + # Then it raises a validation error diff --git a/tests/homesec/test_health.py b/tests/homesec/test_health.py index f5461e57..a253f8ac 100644 --- a/tests/homesec/test_health.py +++ b/tests/homesec/test_health.py @@ -1,252 +1,251 @@ -"""Tests for health check endpoint.""" +"""Tests for FastAPI health endpoints.""" from __future__ import annotations -import time +from types import SimpleNamespace -import pytest +from fastapi.testclient import TestClient -from homesec.health import HealthServer -from homesec.sources import LocalFolderSource, LocalFolderSourceConfig -from tests.homesec.mocks import MockNotifier, MockStateStore, MockStorage +from homesec.api.server import create_app +from homesec.models.config import CameraConfig, CameraSourceConfig, FastAPIServerConfig -class TestHealthServer: - """Test HealthServer implementation.""" +class _StubRepository: + def __init__(self, ok: bool) -> None: + self._ok = ok - @pytest.mark.asyncio - async def test_all_healthy(self, tmp_path) -> None: - """Test health status when all components are healthy.""" - server = HealthServer() + async def ping(self) -> bool: + return self._ok - # Given healthy components - storage = MockStorage() - state_store = MockStateStore() - notifier = MockNotifier() - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path)), camera_name="test" - ) - - server.set_components( - storage=storage, - state_store=state_store, - notifier=notifier, - sources=[source], - ) - - # When computing health - health = await server.compute_health() - - # Then health is healthy with no warnings - assert health["status"] == "healthy" - assert health["checks"]["db"] is True - assert health["checks"]["storage"] is True - assert health["checks"]["mqtt"] is True - assert health["checks"]["sources"] is True - assert health["warnings"] == [] - assert len(health["sources"]) == 1 - assert health["sources"][0]["name"] == "test" - assert health["sources"][0]["healthy"] is True - - @pytest.mark.asyncio - async def test_degraded_when_db_down(self, tmp_path) -> None: - """Test degraded status when DB is down.""" - server = HealthServer() - - # Given a failing state store - storage = MockStorage() - state_store = MockStateStore(simulate_failure=True) - notifier = MockNotifier() - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path)), camera_name="test" - ) - - server.set_components( - storage=storage, - state_store=state_store, - notifier=notifier, - sources=[source], - ) - - # When computing health - health = await server.compute_health() - - # Then status is degraded - assert health["status"] == "degraded" - assert health["checks"]["db"] is False - assert health["checks"]["storage"] is True - - @pytest.mark.asyncio - async def test_degraded_when_mqtt_down(self, tmp_path) -> None: - """Test degraded status when MQTT is down (non-critical).""" - server = HealthServer() - - # Given a failing notifier and mqtt_is_critical=False - storage = MockStorage() - state_store = MockStateStore() - notifier = MockNotifier(simulate_failure=True) - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path)), camera_name="test" - ) - server.set_components( - storage=storage, - state_store=state_store, - notifier=notifier, - sources=[source], - mqtt_is_critical=False, # Non-critical - ) +class _StubStorage: + def __init__(self, ok: bool = True) -> None: + self._ok = ok - # When computing health - health = await server.compute_health() + async def ping(self) -> bool: + return self._ok - # Then status is degraded - assert health["status"] == "degraded" - assert health["checks"]["mqtt"] is False + async def delete(self, storage_uri: str) -> None: + _ = storage_uri + return None - @pytest.mark.asyncio - async def test_unhealthy_when_storage_down(self, tmp_path) -> None: - """Test unhealthy status when storage is down.""" - server = HealthServer() - # Given failing storage - storage = MockStorage(simulate_failure=True) - state_store = MockStateStore() - notifier = MockNotifier() - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path)), camera_name="test" - ) +class _StubSource: + def __init__(self, healthy: bool = True, heartbeat: float = 1.0) -> None: + self._healthy = healthy + self._heartbeat = heartbeat - server.set_components( - storage=storage, - state_store=state_store, - notifier=notifier, - sources=[source], - ) + def is_healthy(self) -> bool: + return self._healthy - # When computing health - health = await server.compute_health() - - # Then status is unhealthy - assert health["status"] == "unhealthy" - assert health["checks"]["storage"] is False - - @pytest.mark.asyncio - async def test_unhealthy_when_sources_down(self, tmp_path) -> None: - """Test unhealthy status when sources are down.""" - server = HealthServer() - - # Given a missing source watch directory - storage = MockStorage() - state_store = MockStateStore() - notifier = MockNotifier() - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path / "nonexistent")), - camera_name="test", - ) + def last_heartbeat(self) -> float: + return self._heartbeat - # When the watch dir is removed - (tmp_path / "nonexistent").rmdir() - server.set_components( - storage=storage, - state_store=state_store, - notifier=notifier, - sources=[source], +class _StubApp: + def __init__( + self, + *, + repository: _StubRepository, + storage: _StubStorage, + sources_by_name: dict[str, _StubSource], + pipeline_running: bool, + cameras: list[CameraConfig] | None = None, + ) -> None: + if cameras is None: + cameras = [ + CameraConfig( + name=name, + enabled=True, + source=CameraSourceConfig( + backend="local_folder", + config={"watch_dir": "/tmp"}, + ), + ) + for name in sources_by_name + ] + self._config = SimpleNamespace( + server=FastAPIServerConfig(), + cameras=cameras, ) - - # Then health is unhealthy due to sources - health = await server.compute_health() - - assert health["status"] == "unhealthy" - assert health["checks"]["sources"] is False - assert len(health["sources"]) == 1 - assert health["sources"][0]["name"] == "test" - assert health["sources"][0]["healthy"] is False - - @pytest.mark.asyncio - async def test_unhealthy_when_mqtt_critical_and_down(self, tmp_path) -> None: - """Test unhealthy status when MQTT is critical and down.""" - server = HealthServer() - - # Given a failing notifier with mqtt_is_critical=True - storage = MockStorage() - state_store = MockStateStore() - notifier = MockNotifier(simulate_failure=True) - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path)), camera_name="test" - ) - - server.set_components( - storage=storage, - state_store=state_store, - notifier=notifier, - sources=[source], - mqtt_is_critical=True, # Critical! - ) - - # When computing health - health = await server.compute_health() - - # Then status is unhealthy - assert health["status"] == "unhealthy" - assert health["checks"]["mqtt"] is False - - @pytest.mark.asyncio - async def test_heartbeat_warnings(self, tmp_path) -> None: - """Test warnings for stale heartbeats.""" - server = HealthServer() - - # Given a source with a stale heartbeat - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path)), camera_name="front_door" - ) - - # When heartbeat is set far in the past - source._last_heartbeat = time.monotonic() - 180 # 3 minutes ago - - server.set_components(sources=[source]) - - # Then a warning is emitted - health = await server.compute_health() - - assert len(health["warnings"]) == 1 - assert "source_front_door_heartbeat_stale" in health["warnings"] - assert len(health["sources"]) == 1 - assert health["sources"][0]["name"] == "front_door" - assert health["sources"][0]["last_heartbeat_age_s"] >= 180 - - @pytest.mark.asyncio - async def test_no_warnings_for_fresh_heartbeat(self, tmp_path) -> None: - """Test no warnings when heartbeat is fresh.""" - server = HealthServer() - - # Given a source with a fresh heartbeat - source = LocalFolderSource( - LocalFolderSourceConfig(watch_dir=str(tmp_path)), camera_name="front_door" - ) - - server.set_components(sources=[source]) - - # When computing health - health = await server.compute_health() - - # Then no warnings are present - assert health["warnings"] == [] - - @pytest.mark.asyncio - async def test_health_with_no_components(self) -> None: - """Test health status with no components configured.""" - # Given: No components configured - server = HealthServer() - - # When: Computing health - health = await server.compute_health() - - # Then: Health is healthy - assert health["status"] == "healthy" - assert health["checks"]["db"] is True - assert health["checks"]["storage"] is True - assert health["checks"]["mqtt"] is True - assert health["sources"] == [] - assert health["checks"]["sources"] is True + self.repository = repository + self.storage = storage + self.sources = list(sources_by_name.values()) + self._sources_by_name = sources_by_name + self._pipeline_running = pipeline_running + self.uptime_seconds = 123.0 + + @property + def config(self): # type: ignore[override] + return self._config + + @property + def pipeline_running(self) -> bool: + return self._pipeline_running + + def get_source(self, camera_name: str) -> _StubSource | None: + return self._sources_by_name.get(camera_name) + + +def _client(app: _StubApp) -> TestClient: + return TestClient(create_app(app)) + + +def test_health_healthy() -> None: + """Health should be healthy when pipeline and DB are up.""" + # Given a running pipeline with healthy DB and camera + app = _StubApp( + repository=_StubRepository(ok=True), + storage=_StubStorage(ok=True), + sources_by_name={"front": _StubSource(healthy=True)}, + pipeline_running=True, + ) + client = _client(app) + + # When requesting health + response = client.get("/api/v1/health") + + # Then it reports healthy + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "healthy" + assert payload["postgres"] == "connected" + assert payload["pipeline"] == "running" + assert payload["cameras_online"] == 1 + + +def test_health_degraded_when_db_down() -> None: + """Health should be degraded when DB is down.""" + # Given a running pipeline with DB down + app = _StubApp( + repository=_StubRepository(ok=False), + storage=_StubStorage(ok=True), + sources_by_name={"front": _StubSource(healthy=True)}, + pipeline_running=True, + ) + client = _client(app) + + # When requesting health + response = client.get("/api/v1/health") + + # Then it reports degraded + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "degraded" + assert payload["postgres"] == "unavailable" + + +def test_health_unhealthy_when_pipeline_stopped() -> None: + """Health should be unhealthy when pipeline is stopped.""" + # Given a stopped pipeline + app = _StubApp( + repository=_StubRepository(ok=True), + storage=_StubStorage(ok=True), + sources_by_name={"front": _StubSource(healthy=True)}, + pipeline_running=False, + ) + client = _client(app) + + # When requesting health + response = client.get("/api/v1/health") + + # Then it returns 503 unhealthy + assert response.status_code == 503 + payload = response.json() + assert payload["status"] == "unhealthy" + assert payload["pipeline"] == "stopped" + + +def test_diagnostics_reports_camera_status() -> None: + """Diagnostics should include per-camera health.""" + # Given a running pipeline with one camera + app = _StubApp( + repository=_StubRepository(ok=True), + storage=_StubStorage(ok=True), + sources_by_name={"front": _StubSource(healthy=True, heartbeat=42.0)}, + pipeline_running=True, + ) + client = _client(app) + + # When requesting diagnostics + response = client.get("/api/v1/diagnostics") + + # Then camera status is included + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "healthy" + assert payload["cameras"]["front"]["healthy"] is True + assert payload["cameras"]["front"]["last_heartbeat"] == 42.0 + + +def test_diagnostics_degraded_when_camera_unhealthy() -> None: + """Diagnostics should be degraded when a camera is unhealthy.""" + # Given a running pipeline with an unhealthy camera + app = _StubApp( + repository=_StubRepository(ok=True), + storage=_StubStorage(ok=True), + sources_by_name={"front": _StubSource(healthy=False, heartbeat=10.0)}, + pipeline_running=True, + ) + client = _client(app) + + # When requesting diagnostics + response = client.get("/api/v1/diagnostics") + + # Then it reports degraded and camera is unhealthy + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "degraded" + assert payload["cameras"]["front"]["healthy"] is False + + +def test_diagnostics_degraded_when_storage_down_and_camera_disabled() -> None: + """Diagnostics should be degraded when storage is unavailable.""" + # Given a running pipeline with storage down and a disabled camera + camera = CameraConfig( + name="front", + enabled=False, + source=CameraSourceConfig( + backend="local_folder", + config={"watch_dir": "/tmp"}, + ), + ) + app = _StubApp( + repository=_StubRepository(ok=True), + storage=_StubStorage(ok=False), + sources_by_name={}, + pipeline_running=True, + cameras=[camera], + ) + client = _client(app) + + # When requesting diagnostics + response = client.get("/api/v1/diagnostics") + + # Then it reports degraded and camera is offline + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "degraded" + assert payload["storage"]["status"] == "error" + assert payload["cameras"]["front"]["healthy"] is False + assert payload["cameras"]["front"]["last_heartbeat"] is None + + +def test_diagnostics_unhealthy_when_pipeline_stopped() -> None: + """Diagnostics should be unhealthy when pipeline is stopped.""" + # Given a stopped pipeline + app = _StubApp( + repository=_StubRepository(ok=True), + storage=_StubStorage(ok=True), + sources_by_name={"front": _StubSource(healthy=True)}, + pipeline_running=False, + ) + client = _client(app) + + # When requesting diagnostics + response = client.get("/api/v1/diagnostics") + + # Then it reports unhealthy + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "unhealthy" diff --git a/uv.lock b/uv.lock index 5744a93b..bc2496b1 100644 --- a/uv.lock +++ b/uv.lock @@ -167,6 +167,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -488,6 +497,18 @@ wheels = [ { 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 = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -851,6 +872,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + [[package]] name = "fastjsonschema" version = "2.21.2" @@ -1123,6 +1159,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "homesec" version = "1.2.2" @@ -1133,6 +1178,7 @@ dependencies = [ { name = "anyio" }, { name = "asyncpg" }, { name = "dropbox" }, + { name = "fastapi" }, { name = "fire" }, { name = "greenlet" }, { name = "opencv-python" }, @@ -1143,11 +1189,13 @@ dependencies = [ { name = "pyyaml" }, { name = "sqlalchemy" }, { name = "ultralytics" }, + { name = "uvicorn" }, ] [package.dev-dependencies] dev = [ { name = "asyncpg-stubs" }, + { name = "httpx" }, { name = "ipykernel" }, { name = "mypy" }, { name = "nbstripout" }, @@ -1165,6 +1213,7 @@ requires-dist = [ { name = "anyio", specifier = ">=4.0.0" }, { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dropbox", specifier = ">=12.0.2" }, + { name = "fastapi", specifier = ">=0.111.0" }, { name = "fire", specifier = ">=0.7.1" }, { name = "greenlet", specifier = ">=3.3.0" }, { name = "opencv-python", specifier = ">=4.12.0.88" }, @@ -1175,11 +1224,13 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0.3" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "ultralytics", specifier = ">=8.3.226" }, + { name = "uvicorn", specifier = ">=0.30.0" }, ] [package.metadata.requires-dev] dev = [ { name = "asyncpg-stubs", specifier = ">=0.31.1" }, + { name = "httpx", specifier = ">=0.27.0" }, { name = "ipykernel", specifier = ">=7.1.0" }, { name = "mypy", specifier = ">=1.19.1" }, { name = "nbstripout", specifier = ">=0.8.2" }, @@ -1190,6 +1241,34 @@ dev = [ { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -3404,6 +3483,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + [[package]] name = "stone" version = "3.3.1" @@ -3706,6 +3798,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + [[package]] name = "wcwidth" version = "0.2.14" From 69fd97ba3612f41bbfce0a10f6f0eec9f160a3fa Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Feb 2026 11:56:37 -0800 Subject: [PATCH 27/31] Phase 2 implementation : add home assistant notifier Implement HA Events API notifier with supervisor/standalone auth. Normalize detected_objects from filter results and pass through alerts. Add docs/examples plus high-coverage tests for notifier behavior. --- config/example.yaml | 8 + docs/ha-phase-2-ha-notifier.md | 9 +- docs/ha-phase-4-ha-integration.md | 2 + src/homesec/models/alert.py | 1 + src/homesec/models/config.py | 11 + src/homesec/pipeline/core.py | 3 + .../plugins/notifiers/home_assistant.py | 220 ++++++++ tests/homesec/test_home_assistant_notifier.py | 493 ++++++++++++++++++ 8 files changed, 743 insertions(+), 4 deletions(-) create mode 100644 src/homesec/plugins/notifiers/home_assistant.py create mode 100644 tests/homesec/test_home_assistant_notifier.py diff --git a/config/example.yaml b/config/example.yaml index f01c0afb..9f761b85 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -111,6 +111,14 @@ notifiers: qos: 1 retain: false + # Home Assistant Events API (recommended for HA users) + # - backend: home_assistant + # config: + # # When running as HA add-on, no configuration needed (uses SUPERVISOR_TOKEN) + # # For standalone mode, provide HA URL and token: + # # url_env: HA_URL # http://homeassistant.local:8123 + # # token_env: HA_TOKEN # Long-lived access token from HA + # SendGrid email # - backend: sendgrid_email # config: diff --git a/docs/ha-phase-2-ha-notifier.md b/docs/ha-phase-2-ha-notifier.md index 893ed0c1..08274928 100644 --- a/docs/ha-phase-2-ha-notifier.md +++ b/docs/ha-phase-2-ha-notifier.md @@ -88,7 +88,8 @@ class HomeAssistantNotifier(Notifier): - view_url: str | None - storage_uri: str | None - timestamp: ISO8601 string - - detected_objects: list[str] (if analysis present) + - detected_objects: list[str] (normalized from filter detections; values: + person, vehicle, animal, package, object, unknown) """ ... ``` @@ -112,7 +113,7 @@ def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: ### Constraints -- All event publishing is best-effort (log errors, don't raise) +- All event publishing is best-effort (raise to pipeline for retry/recording; clip continues) - Use aiohttp for HTTP requests - Events API endpoint: `POST /api/events/{event_type}` - Supervisor URL: `http://supervisor/core/api/events/...` @@ -124,7 +125,7 @@ def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: ### homesec_alert -Fired when an alert is generated after VLM analysis. +Fired when an alert is generated. ```json { @@ -136,7 +137,7 @@ Fired when an alert is generated after VLM analysis. "view_url": "https://dropbox.com/...", "storage_uri": "dropbox:///clips/abc123.mp4", "timestamp": "2026-02-01T10:30:00Z", - "detected_objects": ["person"] + "detected_objects": ["person", "vehicle"] } ``` diff --git a/docs/ha-phase-4-ha-integration.md b/docs/ha-phase-4-ha-integration.md index 28fbc713..bdf8671c 100644 --- a/docs/ha-phase-4-ha-integration.md +++ b/docs/ha-phase-4-ha-integration.md @@ -208,6 +208,8 @@ class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): Event: homesec_alert Note: Camera health is polled via REST API, not event-driven. + Note: Event payloads are intentionally minimal (stable taxonomy); fetch + richer clip details via REST using clip_id when needed. """ ... diff --git a/src/homesec/models/alert.py b/src/homesec/models/alert.py index 09e3586d..5dfc4763 100644 --- a/src/homesec/models/alert.py +++ b/src/homesec/models/alert.py @@ -29,6 +29,7 @@ class Alert(BaseModel): notify_reason: str summary: str | None analysis: SequenceAnalysis | None = None + detected_classes: list[str] | None = None ts: datetime dedupe_key: str # Same as clip_id for MVP upload_failed: bool # True if storage_uri is None due to upload failure diff --git a/src/homesec/models/config.py b/src/homesec/models/config.py index 2800569a..f1d741c8 100644 --- a/src/homesec/models/config.py +++ b/src/homesec/models/config.py @@ -79,6 +79,17 @@ def _normalize_backend(cls, value: Any) -> Any: return value +class HomeAssistantNotifierConfig(BaseModel): + """Configuration for Home Assistant notifier. + + When running as a Home Assistant add-on, SUPERVISOR_TOKEN is used automatically. + For standalone mode, url_env and token_env must be provided. + """ + + url_env: str | None = None + token_env: str | None = None + + class RetentionConfig(BaseModel): """Retention configuration for local storage.""" diff --git a/src/homesec/pipeline/core.py b/src/homesec/pipeline/core.py index 61cd225c..3a2936ca 100644 --- a/src/homesec/pipeline/core.py +++ b/src/homesec/pipeline/core.py @@ -251,6 +251,7 @@ async def _process_clip(self, clip: Clip) -> None: clip, alert_decision, analysis_result, + filter_res.detected_classes, storage_uri, view_url, upload_failed, @@ -472,6 +473,7 @@ async def _notify_stage( clip: Clip, decision: AlertDecision, analysis_result: AnalysisResult | None, + detected_classes: list[str], storage_uri: str | None, view_url: str | None, upload_failed: bool, @@ -488,6 +490,7 @@ async def _notify_stage( notify_reason=decision.notify_reason, summary=analysis_result.summary if analysis_result else None, analysis=analysis_result.analysis if analysis_result else None, + detected_classes=detected_classes, ts=datetime.now(), dedupe_key=clip.clip_id, upload_failed=upload_failed, diff --git a/src/homesec/plugins/notifiers/home_assistant.py b/src/homesec/plugins/notifiers/home_assistant.py new file mode 100644 index 00000000..33ef5cf7 --- /dev/null +++ b/src/homesec/plugins/notifiers/home_assistant.py @@ -0,0 +1,220 @@ +"""Home Assistant notifier plugin (Events API).""" + +from __future__ import annotations + +import asyncio +import logging +import os +from typing import Any + +aiohttp: Any + +try: + import aiohttp as _aiohttp +except Exception: + aiohttp = None +else: + aiohttp = _aiohttp + +from homesec.interfaces import Notifier +from homesec.models.alert import Alert +from homesec.models.config import HomeAssistantNotifierConfig +from homesec.plugins.registry import PluginType, plugin + +logger = logging.getLogger(__name__) + +_EVENT_PREFIX = "homesec" +_SUPERVISOR_URL = "http://supervisor/core" +_OBJECT_ORDER = ("person", "vehicle", "animal", "package", "object", "unknown") +_PERSON_CLASSES = {"person", "human"} +_VEHICLE_CLASSES = { + "car", + "truck", + "bus", + "motorcycle", + "motorbike", + "bicycle", + "bike", + "scooter", + "van", + "vehicle", + "train", + "boat", + "ship", + "airplane", +} +_ANIMAL_CLASSES = { + "dog", + "cat", + "bird", + "horse", + "sheep", + "cow", + "bear", + "zebra", + "giraffe", + "animal", +} +_PACKAGE_CLASSES = {"package", "parcel", "box", "bag"} + + +def _ensure_aiohttp_dependencies() -> None: + """Fail fast with a clear error if aiohttp is missing.""" + if aiohttp is None: + raise RuntimeError( + "Missing dependency for Home Assistant notifier. Install with: uv pip install aiohttp" + ) + + +@plugin(plugin_type=PluginType.NOTIFIER, name="home_assistant") +class HomeAssistantNotifier(Notifier): + """Push HomeSec alerts to Home Assistant via the Events API.""" + + config_cls = HomeAssistantNotifierConfig + + @classmethod + def create(cls, config: HomeAssistantNotifierConfig) -> Notifier: + return cls(config) + + def __init__(self, config: HomeAssistantNotifierConfig) -> None: + _ensure_aiohttp_dependencies() + self._session: aiohttp.ClientSession | None = None + self._shutdown_called = False + self._timeout_s = 10.0 + + supervisor_token = os.getenv("SUPERVISOR_TOKEN") + self._supervisor_mode = bool(supervisor_token) + self._base_url: str | None = None + self._token: str | None = None + + if self._supervisor_mode: + self._base_url = _SUPERVISOR_URL + self._token = supervisor_token + else: + if not config.url_env or not config.token_env: + raise ValueError( + "home_assistant notifier requires url_env and token_env when " + "SUPERVISOR_TOKEN is not set" + ) + + self._base_url = os.getenv(config.url_env) + self._token = os.getenv(config.token_env) + + if not self._base_url: + logger.warning("Home Assistant URL not found in env: %s", config.url_env) + if not self._token: + logger.warning("Home Assistant token not found in env: %s", config.token_env) + + if self._base_url: + self._base_url = self._base_url.rstrip("/") + + async def send(self, alert: Alert) -> None: + """Send alert notification to Home Assistant.""" + if self._shutdown_called: + raise RuntimeError("Notifier has been shut down") + + url, headers = self._get_url_and_headers("alert") + payload = self._build_event_data(alert) + session = await self._get_session() + + try: + async with session.post(url, json=payload, headers=headers) as response: + if response.status >= 400: + details = await response.text() + raise RuntimeError( + f"Home Assistant event send failed: HTTP {response.status}: {details}" + ) + await response.read() + except asyncio.TimeoutError as exc: + raise asyncio.TimeoutError("Home Assistant event send timed out") from exc + + logger.info("Home Assistant event sent: clip_id=%s", alert.clip_id) + + async def ping(self) -> bool: + """Health check - verify Home Assistant is reachable.""" + if self._shutdown_called: + return False + if not self._base_url or not self._token: + return False + + session = await self._get_session() + url = f"{self._base_url}/api/" + headers = {"Authorization": f"Bearer {self._token}"} + + try: + async with session.get(url, headers=headers) as response: + if response.status >= 400: + return False + await response.read() + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + logger.warning("Home Assistant ping failed: %s", exc) + return False + + return True + + async def shutdown(self, timeout: float | None = None) -> None: + """Cleanup resources - close HTTP session.""" + _ = timeout + if self._shutdown_called: + return + self._shutdown_called = True + + if self._session and not self._session.closed: + await self._session.close() + + def _get_url_and_headers(self, event_type: str) -> tuple[str, dict[str, str]]: + if not self._base_url: + raise RuntimeError("Home Assistant URL is missing") + if not self._token: + raise RuntimeError("Home Assistant token is missing") + + event_name = f"{_EVENT_PREFIX}_{event_type}" + url = f"{self._base_url}/api/events/{event_name}" + headers = {"Authorization": f"Bearer {self._token}"} + return url, headers + + async def _get_session(self) -> aiohttp.ClientSession: + if self._session is None or self._session.closed: + if aiohttp is None: + raise RuntimeError("aiohttp dependency is required for Home Assistant notifier") + timeout = aiohttp.ClientTimeout(total=self._timeout_s) + self._session = aiohttp.ClientSession(timeout=timeout) + return self._session + + def _build_event_data(self, alert: Alert) -> dict[str, object]: + detected_objects = self._normalize_detected_objects(alert.detected_classes) + data: dict[str, object] = { + "camera": alert.camera_name, + "clip_id": alert.clip_id, + "activity_type": alert.activity_type or "unknown", + "risk_level": str(alert.risk_level) if alert.risk_level is not None else "unknown", + "summary": alert.summary or "", + "view_url": alert.view_url, + "storage_uri": alert.storage_uri, + "timestamp": alert.ts.isoformat(), + "detected_objects": detected_objects, + } + + return data + + def _normalize_detected_objects(self, detected_classes: list[str] | None) -> list[str]: + if not detected_classes: + return [] + + found: set[str] = set() + for class_name in detected_classes: + key = class_name.strip().lower() + if key in _PERSON_CLASSES: + found.add("person") + elif key in _VEHICLE_CLASSES: + found.add("vehicle") + elif key in _ANIMAL_CLASSES: + found.add("animal") + elif key in _PACKAGE_CLASSES: + found.add("package") + elif key == "unknown": + found.add("unknown") + else: + found.add("object") + + return [category for category in _OBJECT_ORDER if category in found] diff --git a/tests/homesec/test_home_assistant_notifier.py b/tests/homesec/test_home_assistant_notifier.py new file mode 100644 index 00000000..d4ac8807 --- /dev/null +++ b/tests/homesec/test_home_assistant_notifier.py @@ -0,0 +1,493 @@ +"""Tests for Home Assistant notifier plugin.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import aiohttp +import pytest + +from homesec.models.alert import Alert +from homesec.models.config import HomeAssistantNotifierConfig +from homesec.models.vlm import EntityTimeline, SequenceAnalysis +from homesec.plugins.notifiers.home_assistant import HomeAssistantNotifier + + +def _make_config(**overrides: Any) -> HomeAssistantNotifierConfig: + """Create HomeAssistantNotifierConfig with defaults.""" + defaults: dict[str, Any] = { + "url_env": "HA_URL", + "token_env": "HA_TOKEN", + } + defaults.update(overrides) + return HomeAssistantNotifierConfig(**defaults) + + +def _make_analysis() -> SequenceAnalysis: + return SequenceAnalysis( + sequence_description="sequence", + max_risk_level="low", + primary_activity="normal_visitor", + observations=["obs"], + entities_timeline=[ + EntityTimeline( + type="person", + first_seen_timestamp="00:00:01.00", + last_seen_timestamp="00:00:05.00", + description="Person near door", + movement="walking", + location="front door", + interaction="none", + ), + EntityTimeline( + type="vehicle", + first_seen_timestamp="00:00:02.00", + last_seen_timestamp="00:00:06.00", + description="Car in driveway", + movement="parked", + location="driveway", + interaction="none", + ), + EntityTimeline( + type="person", + first_seen_timestamp="00:00:03.00", + last_seen_timestamp="00:00:07.00", + description="Person still present", + movement="standing", + location="front door", + interaction="none", + ), + ], + requires_review=False, + frame_count=1, + video_start_time="00:00:00.00", + video_end_time="00:00:01.00", + ) + + +def _make_alert(**overrides: Any) -> Alert: + """Create a sample Alert with defaults.""" + defaults: dict[str, Any] = { + "clip_id": "clip_123", + "camera_name": "front_door", + "storage_uri": "mock://clip_123", + "view_url": "http://example.com/clip_123", + "risk_level": "low", + "activity_type": "delivery", + "notify_reason": "risk_level=low", + "summary": "Package delivered", + "analysis": None, + "detected_classes": ["person", "car", "dog"], + "ts": datetime.now(), + "dedupe_key": "clip_123", + "upload_failed": False, + "vlm_failed": False, + } + defaults.update(overrides) + return Alert(**defaults) + + +def _mock_http_response(status: int) -> AsyncMock: + """Create a mock aiohttp response.""" + response = AsyncMock() + response.status = status + response.text = AsyncMock(return_value="OK" if status < 400 else "Error") + response.read = AsyncMock(return_value=b"{}") + response.__aenter__ = AsyncMock(return_value=response) + response.__aexit__ = AsyncMock(return_value=None) + return response + + +def _patch_session( + monkeypatch: pytest.MonkeyPatch, + *, + post_cm: AsyncMock | None = None, + get_cm: AsyncMock | None = None, + capture: dict[str, Any] | None = None, + raise_on_post: Exception | None = None, + raise_on_get: Exception | None = None, +) -> MagicMock: + session = MagicMock() + + if raise_on_post is not None: + + def post(*_args: Any, **_kwargs: Any) -> None: + raise raise_on_post + + session.post = post + elif post_cm is not None: + + def post( + url: str, *, json: dict[str, Any], headers: dict[str, str], **_kwargs: Any + ) -> AsyncMock: + if capture is not None: + capture["url"] = url + capture["json"] = json + capture["headers"] = headers + return post_cm + + session.post = post + + if get_cm is not None: + session.get = MagicMock(return_value=get_cm) + elif raise_on_get is not None: + + def get(*_args: Any, **_kwargs: Any) -> None: + raise raise_on_get + + session.get = get + + async def _close() -> None: + session.closed = True + + session.close = AsyncMock(side_effect=_close) + session.closed = False + + monkeypatch.setattr( + "homesec.plugins.notifiers.home_assistant.aiohttp.ClientSession", + lambda **_kw: session, + ) + return session + + +class TestHomeAssistantNotifierModes: + """Tests for supervisor vs standalone mode selection.""" + + @pytest.mark.asyncio + async def test_supervisor_mode_uses_supervisor_url( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Supervisor mode uses supervisor URL and token.""" + # Given: SUPERVISOR_TOKEN is set + monkeypatch.setenv("SUPERVISOR_TOKEN", "super-token") + config = HomeAssistantNotifierConfig() + notifier = HomeAssistantNotifier(config) + captured: dict[str, Any] = {} + mock_response = _mock_http_response(200) + _patch_session(monkeypatch, post_cm=mock_response, capture=captured) + + # When: Sending an alert + await notifier.send(_make_alert(analysis=_make_analysis())) + + # Then: Uses supervisor URL and token + assert captured["url"] == "http://supervisor/core/api/events/homesec_alert" + assert captured["headers"]["Authorization"] == "Bearer super-token" + assert captured["json"]["camera"] == "front_door" + assert captured["json"]["clip_id"] == "clip_123" + assert captured["json"]["detected_objects"] == ["person", "vehicle", "animal"] + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_standalone_mode_uses_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Standalone mode uses configured env vars for URL and token.""" + # Given: Standalone mode with url/token env vars + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + config = _make_config() + notifier = HomeAssistantNotifier(config) + captured: dict[str, Any] = {} + mock_response = _mock_http_response(200) + _patch_session(monkeypatch, post_cm=mock_response, capture=captured) + + # When: Sending an alert + await notifier.send(_make_alert()) + + # Then: Uses standalone URL and token + assert captured["url"] == "http://ha.local:8123/api/events/homesec_alert" + assert captured["headers"]["Authorization"] == "Bearer ha-token" + + await notifier.shutdown() + + def test_missing_env_config_raises_value_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Standalone mode requires url_env and token_env.""" + # Given: Standalone mode with missing url_env + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + config = HomeAssistantNotifierConfig(token_env="HA_TOKEN") + + # When/Then: Creating notifier raises ValueError + with pytest.raises(ValueError, match="url_env"): + HomeAssistantNotifier(config) + + +class TestHomeAssistantNotifierSending: + """Tests for sending events and error handling.""" + + @pytest.mark.asyncio + async def test_send_raises_on_http_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """HTTP errors raise for retry handling.""" + # Given: Standalone notifier with HA returning 401 + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + mock_response = _mock_http_response(401) + _patch_session(monkeypatch, post_cm=mock_response) + + # When/Then: Sending an alert raises + with pytest.raises(RuntimeError, match="HTTP 401"): + await notifier.send(_make_alert()) + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_send_raises_on_connection_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Connection errors raise for retry handling.""" + # Given: Standalone notifier with connection error + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + _patch_session(monkeypatch, raise_on_post=aiohttp.ClientConnectionError("no route")) + + # When/Then: Sending an alert raises + with pytest.raises(aiohttp.ClientConnectionError, match="no route"): + await notifier.send(_make_alert()) + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_send_raises_on_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Timeouts raise a clearer exception.""" + # Given: Standalone notifier with timeout on post + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + _patch_session(monkeypatch, raise_on_post=asyncio.TimeoutError()) + + # When/Then: Sending an alert raises timeout + with pytest.raises(asyncio.TimeoutError, match="timed out"): + await notifier.send(_make_alert()) + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_send_raises_when_token_missing(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Missing auth token raises before HTTP request.""" + # Given: Standalone notifier missing token env value + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.delenv("HA_TOKEN", raising=False) + notifier = HomeAssistantNotifier(_make_config()) + + # When/Then: Sending an alert raises token error + with pytest.raises(RuntimeError, match="token is missing"): + await notifier.send(_make_alert()) + + @pytest.mark.asyncio + async def test_send_raises_when_url_missing(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Missing URL raises before HTTP request.""" + # Given: Standalone notifier missing URL env value + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.delenv("HA_URL", raising=False) + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + + # When/Then: Sending an alert raises URL error + with pytest.raises(RuntimeError, match="URL is missing"): + await notifier.send(_make_alert()) + + @pytest.mark.asyncio + async def test_send_raises_after_shutdown(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Send raises after shutdown is called.""" + # Given: Standalone notifier that has been shut down + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + await notifier.shutdown() + + # When/Then: Sending after shutdown raises + with pytest.raises(RuntimeError, match="shut down"): + await notifier.send(_make_alert()) + + @pytest.mark.asyncio + async def test_shutdown_is_idempotent(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Shutdown can be called multiple times safely.""" + # Given: Standalone notifier with an active session + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + mock_response = _mock_http_response(200) + session = _patch_session(monkeypatch, post_cm=mock_response) + await notifier.send(_make_alert()) + + # When: Calling shutdown multiple times + await notifier.shutdown() + await notifier.shutdown() + await notifier.shutdown() + + # Then: Session is closed (shutdown state observable) + assert session.closed is True + + +class TestHomeAssistantNotifierPing: + """Tests for notifier ping behavior.""" + + @pytest.mark.asyncio + async def test_ping_returns_true_on_success(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Ping returns True when API responds OK.""" + # Given: Standalone notifier with healthy API + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + mock_response = _mock_http_response(200) + _patch_session(monkeypatch, get_cm=mock_response) + + # When: Pinging + result = await notifier.ping() + + # Then: Ping is healthy + assert result is True + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_ping_returns_false_on_http_error(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Ping returns False when API responds with error.""" + # Given: Standalone notifier with API error + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + mock_response = _mock_http_response(401) + _patch_session(monkeypatch, get_cm=mock_response) + + # When: Pinging + result = await notifier.ping() + + # Then: Ping is unhealthy + assert result is False + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_ping_returns_false_on_connection_error( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Ping returns False when API is unreachable.""" + # Given: Standalone notifier with connection error + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + _patch_session(monkeypatch, raise_on_get=aiohttp.ClientConnectionError("no route")) + + # When: Pinging + result = await notifier.ping() + + # Then: Ping is unhealthy + assert result is False + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_ping_returns_false_after_shutdown(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Ping returns False after shutdown.""" + # Given: Standalone notifier that has been shut down + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + await notifier.shutdown() + + # When: Pinging after shutdown + result = await notifier.ping() + + # Then: Ping is unhealthy + assert result is False + + @pytest.mark.asyncio + async def test_ping_returns_false_when_url_missing( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Ping returns False when URL env is missing.""" + # Given: Standalone notifier missing URL env value + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.delenv("HA_URL", raising=False) + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + + # When: Pinging without URL + result = await notifier.ping() + + # Then: Ping is unhealthy + assert result is False + + @pytest.mark.asyncio + async def test_ping_returns_false_when_token_missing( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Ping returns False when token env is missing.""" + # Given: Standalone notifier missing token env value + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.delenv("HA_TOKEN", raising=False) + notifier = HomeAssistantNotifier(_make_config()) + + # When: Pinging without token + result = await notifier.ping() + + # Then: Ping is unhealthy + assert result is False + + +class TestHomeAssistantNotifierDetectedObjects: + """Tests for detected_objects normalization.""" + + @pytest.mark.asyncio + async def test_detected_objects_normalization(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Detected objects normalize and preserve stable ordering.""" + # Given: A notifier and alert with mixed detected classes + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + captured: dict[str, Any] = {} + mock_response = _mock_http_response(200) + _patch_session(monkeypatch, post_cm=mock_response, capture=captured) + alert = _make_alert(detected_classes=["PACKAGE", "bicycle", "unknown", "umbrella", "dog"]) + + # When: Sending an alert + await notifier.send(alert) + + # Then: Normalized detected_objects are stable and ordered + assert captured["json"]["detected_objects"] == [ + "vehicle", + "animal", + "package", + "object", + "unknown", + ] + + await notifier.shutdown() + + @pytest.mark.asyncio + async def test_detected_objects_empty_when_missing( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Detected objects is empty when no classes are provided.""" + # Given: A notifier and alert without detected classes + monkeypatch.delenv("SUPERVISOR_TOKEN", raising=False) + monkeypatch.setenv("HA_URL", "http://ha.local:8123") + monkeypatch.setenv("HA_TOKEN", "ha-token") + notifier = HomeAssistantNotifier(_make_config()) + captured: dict[str, Any] = {} + mock_response = _mock_http_response(200) + _patch_session(monkeypatch, post_cm=mock_response, capture=captured) + alert = _make_alert(detected_classes=None) + + # When: Sending an alert + await notifier.send(alert) + + # Then: detected_objects is empty + assert captured["json"]["detected_objects"] == [] + + await notifier.shutdown() From e170b06ee1d22f49510dfa1254a84cef4ef762cd Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Feb 2026 14:05:23 -0800 Subject: [PATCH 28/31] Phase 3 implementation: Home Assistant add-on - Add add-on scaffolding (manifest, repository.json, docs, translations, assets) - Add Dockerfile/build config, ingress config, and s6 services for Postgres/HomeSec - Bootstrap config generation and bundled Postgres password handling - Add shellcheck tooling and CI lint integration for add-on scripts --- .github/workflows/ci.yml | 6 + Makefile | 1 + docs/ha-phase-3-addon.md | 26 +++- homeassistant/Makefile | 29 ++++ homeassistant/addon/README.md | 15 ++ homeassistant/addon/homesec/CHANGELOG.md | 5 + homeassistant/addon/homesec/DOCS.md | 31 +++++ homeassistant/addon/homesec/Dockerfile | 35 +++++ homeassistant/addon/homesec/build.yaml | 5 + homeassistant/addon/homesec/config.yaml | 55 ++++++++ homeassistant/addon/homesec/icon.png | Bin 0 -> 1820 bytes homeassistant/addon/homesec/logo.png | Bin 0 -> 666 bytes .../rootfs/etc/nginx/includes/ingress.conf | 11 ++ .../s6-rc.d/homesec/dependencies.d/postgres | 0 .../rootfs/etc/s6-overlay/s6-rc.d/homesec/run | 130 ++++++++++++++++++ .../etc/s6-overlay/s6-rc.d/homesec/type | 1 + .../etc/s6-overlay/s6-rc.d/postgres-init/type | 1 + .../etc/s6-overlay/s6-rc.d/postgres-init/up | 48 +++++++ .../postgres/dependencies.d/postgres-init | 0 .../etc/s6-overlay/s6-rc.d/postgres/run | 2 + .../etc/s6-overlay/s6-rc.d/postgres/type | 1 + .../s6-rc.d/user/contents.d/homesec | 0 .../s6-rc.d/user/contents.d/postgres | 0 .../s6-rc.d/user/contents.d/postgres-init | 0 .../addon/homesec/translations/en.yaml | 31 +++++ repository.json | 14 ++ 26 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 homeassistant/Makefile create mode 100644 homeassistant/addon/README.md create mode 100644 homeassistant/addon/homesec/CHANGELOG.md create mode 100644 homeassistant/addon/homesec/DOCS.md create mode 100644 homeassistant/addon/homesec/Dockerfile create mode 100644 homeassistant/addon/homesec/build.yaml create mode 100644 homeassistant/addon/homesec/config.yaml create mode 100644 homeassistant/addon/homesec/icon.png create mode 100644 homeassistant/addon/homesec/logo.png create mode 100644 homeassistant/addon/homesec/rootfs/etc/nginx/includes/ingress.conf create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/dependencies.d/postgres create mode 100755 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/run create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/type create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/type create mode 100755 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres/dependencies.d/postgres-init create mode 100755 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres/run create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres/type create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/homesec create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/postgres create mode 100644 homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/postgres-init create mode 100644 homeassistant/addon/homesec/translations/en.yaml create mode 100644 repository.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3e49467..5c31816f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,12 @@ jobs: - name: Sync dependencies run: uv sync --group dev + - name: Install shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + + - name: Shellcheck add-on scripts + run: make -C homeassistant shellcheck + # Lint and typecheck run while Postgres boots - name: Lint run: make lint diff --git a/Makefile b/Makefile index 81ea9927..54ff0fa5 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,7 @@ typecheck: lint: uv run ruff check src tests uv run ruff format --check src tests + make -C homeassistant shellcheck lint-fix: uv run ruff check --fix src tests diff --git a/docs/ha-phase-3-addon.md b/docs/ha-phase-3-addon.md index 7ebdaf5b..2eef3e9f 100644 --- a/docs/ha-phase-3-addon.md +++ b/docs/ha-phase-3-addon.md @@ -91,6 +91,7 @@ schema: config_path: str? log_level: list(debug|info|warning|error)? database_url: str? # External DB (optional) + db_password: password? # Bundled DB password (optional) storage_type: list(local|dropbox)? storage_path: str? dropbox_token: password? # Mapped to DROPBOX_TOKEN env var @@ -102,6 +103,7 @@ options: config_path: /config/homesec/config.yaml log_level: info database_url: "" + db_password: "" storage_type: local storage_path: /media/homesec/clips vlm_enabled: false @@ -131,6 +133,8 @@ watchdog: http://[HOST]:[PORT:8080]/api/v1/health - `homeassistant_api: true` injects SUPERVISOR_TOKEN - Ingress provides secure access without port exposure - Watchdog ensures auto-restart on failure +- Bundled Postgres password is optional; if omitted, it is auto-generated and + stored at `/data/postgres/db_password` --- @@ -267,7 +271,7 @@ cameras: [] storage: backend: local # or dropbox based on options config: - path: /media/homesec/clips + root: /media/homesec/clips state_store: dsn_env: DATABASE_URL @@ -276,6 +280,26 @@ notifiers: - backend: home_assistant config: {} # Uses SUPERVISOR_TOKEN automatically +filter: + backend: yolo + config: + classes: + - person + +vlm: + backend: openai + trigger_classes: ["person"] + run_mode: never # trigger_only when VLM enabled + config: + api_key_env: OPENAI_API_KEY + model: "gpt-4o" + +alert_policy: + backend: default + enabled: true + config: + min_risk_level: medium + server: enabled: true host: 0.0.0.0 diff --git a/homeassistant/Makefile b/homeassistant/Makefile new file mode 100644 index 00000000..b15a3faa --- /dev/null +++ b/homeassistant/Makefile @@ -0,0 +1,29 @@ +SHELL := /bin/bash +.SHELLFLAGS := -eu -o pipefail -c + +ADDON_DIR := addon/homesec +SHELLCHECK ?= shellcheck + +S6_SCRIPTS := $(shell find $(ADDON_DIR)/rootfs/etc/s6-overlay/s6-rc.d -type f \( -name run -o -name up \)) + +.PHONY: help shellcheck addon-build + +help: + @echo "Targets:" + @echo "" + @echo " make shellcheck Run shellcheck on add-on s6 scripts" + @echo " make addon-build Build the HomeSec add-on Docker image" + +shellcheck: + @if ! command -v $(SHELLCHECK) >/dev/null; then \ + echo "shellcheck not found. Install shellcheck to run this target."; \ + exit 1; \ + fi + @if [ -z "$(S6_SCRIPTS)" ]; then \ + echo "No s6 scripts found under $(ADDON_DIR)/rootfs."; \ + exit 0; \ + fi + $(SHELLCHECK) --shell=bash $(S6_SCRIPTS) + +addon-build: + docker build -t homesec-addon $(ADDON_DIR) diff --git a/homeassistant/addon/README.md b/homeassistant/addon/README.md new file mode 100644 index 00000000..7ce6de7e --- /dev/null +++ b/homeassistant/addon/README.md @@ -0,0 +1,15 @@ +# HomeSec Home Assistant Add-on + +This repository provides the HomeSec add-on for Home Assistant OS/Supervised. + +## Install + +1. In Home Assistant, open **Settings → Add-ons → Add-on Store**. +2. Add this repository: `https://github.com/lan17/homesec`. +3. Install the **HomeSec** add-on and start it. + +## Notes + +- The add-on generates `/config/homesec/config.yaml` on first start. +- After initial bootstrap, configuration should be managed via the REST API or + the Home Assistant integration. diff --git a/homeassistant/addon/homesec/CHANGELOG.md b/homeassistant/addon/homesec/CHANGELOG.md new file mode 100644 index 00000000..d769aebb --- /dev/null +++ b/homeassistant/addon/homesec/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.2.2 + +- Initial HomeSec add-on release. diff --git a/homeassistant/addon/homesec/DOCS.md b/homeassistant/addon/homesec/DOCS.md new file mode 100644 index 00000000..22b3b7b8 --- /dev/null +++ b/homeassistant/addon/homesec/DOCS.md @@ -0,0 +1,31 @@ +# HomeSec Add-on + +HomeSec is a self-hosted AI video security pipeline. This add-on runs HomeSec +inside Home Assistant with optional bundled PostgreSQL and ingress access. + +## Configuration + +Options are **bootstrap-only**. On first start the add-on generates +`/config/homesec/config.yaml` from these options. After that, configure HomeSec +via the REST API or the Home Assistant integration. + +### Options + +- `config_path`: Path to HomeSec config file. +- `log_level`: Log level for HomeSec (`debug`, `info`, `warning`, `error`). +- `database_url`: Optional external Postgres DSN. If empty, bundled Postgres is used. +- `db_password`: Password for bundled Postgres (leave blank to auto-generate). +- `storage_type`: `local` or `dropbox`. +- `storage_path`: Root path for local storage (used when `storage_type=local`). +- `dropbox_token`: Dropbox token (used when `storage_type=dropbox`). +- `vlm_enabled`: Enable VLM analysis (OpenAI). When false, VLM is disabled. +- `openai_api_key`: OpenAI API key (required when `vlm_enabled=true`). +- `openai_model`: OpenAI model name (default: `gpt-4o`). + +## Notes + +- The add-on uses `SUPERVISOR_TOKEN` automatically for HA event notifications. +- Ingress provides secure access to the REST API. +- The bundled database is not exposed outside the add-on container. +- Auto-generated Postgres passwords are stored at `/data/postgres/db_password`. +- Changing `db_password` after initialization does not update the existing DB user. diff --git a/homeassistant/addon/homesec/Dockerfile b/homeassistant/addon/homesec/Dockerfile new file mode 100644 index 00000000..8dd18c0d --- /dev/null +++ b/homeassistant/addon/homesec/Dockerfile @@ -0,0 +1,35 @@ +ARG BUILD_FROM=ghcr.io/hassio-addons/base:15.0.8 +FROM ${BUILD_FROM} + +ARG HOMESEC_VERSION=1.2.2 + +ENV PYTHONUNBUFFERED=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-venv \ + ffmpeg \ + curl \ + ca-certificates \ + libgl1 \ + libglib2.0-0 \ + libsm6 \ + libxrender1 \ + libxext6 \ + && if apt-cache show postgresql-16 >/dev/null 2>&1; then \ + apt-get install -y --no-install-recommends postgresql-16 postgresql-client-16; \ + else \ + apt-get install -y --no-install-recommends postgresql postgresql-client; \ + fi \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m pip install --no-cache-dir --upgrade pip \ + && pip3 install --no-cache-dir "homesec==${HOMESEC_VERSION}" + +COPY rootfs/ / + +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8080/api/v1/health || exit 1 diff --git a/homeassistant/addon/homesec/build.yaml b/homeassistant/addon/homesec/build.yaml new file mode 100644 index 00000000..9edb38f7 --- /dev/null +++ b/homeassistant/addon/homesec/build.yaml @@ -0,0 +1,5 @@ +build_from: + amd64: ghcr.io/hassio-addons/base:15.0.8 + aarch64: ghcr.io/hassio-addons/base:15.0.8 +args: + HOMESEC_VERSION: "1.2.2" diff --git a/homeassistant/addon/homesec/config.yaml b/homeassistant/addon/homesec/config.yaml new file mode 100644 index 00000000..8ad07d19 --- /dev/null +++ b/homeassistant/addon/homesec/config.yaml @@ -0,0 +1,55 @@ +name: HomeSec +version: "1.2.2" +slug: homesec +description: Self-hosted AI video security pipeline +url: https://github.com/lan17/homesec +arch: + - amd64 + - aarch64 +init: false +homeassistant_api: true +hassio_api: true +host_network: false +ingress: true +ingress_port: 8080 +ingress_stream: true +panel_icon: mdi:cctv +panel_title: HomeSec + +ports: + 8080/tcp: null + +map: + - addon_config:rw + - media:rw + - share:rw + +schema: + config_path: str? + log_level: list(debug|info|warning|error)? + database_url: str? + db_password: password? + storage_type: list(local|dropbox)? + storage_path: str? + dropbox_token: password? + vlm_enabled: bool? + openai_api_key: password? + openai_model: str? + +options: + config_path: /config/homesec/config.yaml + log_level: info + database_url: "" + db_password: "" + storage_type: local + storage_path: /media/homesec/clips + vlm_enabled: false + openai_model: gpt-4o + +startup: services +stage: stable +advanced: true +privileged: [] +apparmor: true + +watchdog: http://[HOST]:[PORT:8080]/api/v1/health diff --git a/homeassistant/addon/homesec/icon.png b/homeassistant/addon/homesec/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..595dbd53b7b91268599f0f092cec97033fa4af6d GIT binary patch literal 1820 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4YzZe+U9(%ethE&{odrgs%fq~&%v5;;0RZY&8Dh``A#eb3GB&sKwxM7%FCRP|M43bEp@JL&L}&I+>G1 VxlA|pN&(XxgQu&X%Q~loCICP{S0w-d literal 0 HcmV?d00001 diff --git a/homeassistant/addon/homesec/rootfs/etc/nginx/includes/ingress.conf b/homeassistant/addon/homesec/rootfs/etc/nginx/includes/ingress.conf new file mode 100644 index 00000000..c79b5178 --- /dev/null +++ b/homeassistant/addon/homesec/rootfs/etc/nginx/includes/ingress.conf @@ -0,0 +1,11 @@ +location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; +} diff --git a/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/dependencies.d/postgres b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/dependencies.d/postgres new file mode 100644 index 00000000..e69de29b diff --git a/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/run b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/run new file mode 100755 index 00000000..f8c2ec97 --- /dev/null +++ b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/run @@ -0,0 +1,130 @@ +#!/command/with-contenv bashio +set -euo pipefail + +CONFIG_PATH="$(bashio::config 'config_path')" +LOG_LEVEL="$(bashio::config 'log_level')" +STORAGE_TYPE="$(bashio::config 'storage_type')" +STORAGE_PATH="$(bashio::config 'storage_path')" +VLM_ENABLED="$(bashio::config 'vlm_enabled')" +OPENAI_MODEL="$(bashio::config 'openai_model')" + +DB_USER="homesec" +DB_NAME="homesec" +PASS_FILE="/data/postgres/db_password" + +if bashio::config.has_value 'database_url'; then + DATABASE_URL_OPTION="$(bashio::config 'database_url')" + export DATABASE_URL="${DATABASE_URL_OPTION}" + USING_EXTERNAL_DB=true +else + if [ -s "${PASS_FILE}" ]; then + DB_PASS="$(cat "${PASS_FILE}")" + elif bashio::config.has_value 'db_password'; then + DB_PASS="$(bashio::config 'db_password')" + if [ -s "/data/postgres/data/PG_VERSION" ]; then + bashio::log.warning "PostgreSQL already initialized; assuming db_password matches." + fi + if [ -n "${DB_PASS}" ]; then + umask 077 + printf '%s' "${DB_PASS}" > "${PASS_FILE}" + fi + else + bashio::log.error "Database password missing for bundled Postgres." + bashio::log.error "Set db_password option or delete /data/postgres to re-init." + exit 1 + fi + + export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASS}@127.0.0.1:5432/${DB_NAME}" + USING_EXTERNAL_DB=false +fi + +if bashio::config.has_value 'dropbox_token'; then + DROPBOX_TOKEN="$(bashio::config 'dropbox_token')" + export DROPBOX_TOKEN +fi + +if bashio::config.has_value 'openai_api_key'; then + OPENAI_API_KEY="$(bashio::config 'openai_api_key')" + export OPENAI_API_KEY +else + export OPENAI_API_KEY="disabled" +fi + +if [ "${USING_EXTERNAL_DB}" = "false" ]; then + bashio::log.info "Waiting for PostgreSQL..." + until pg_isready -h 127.0.0.1 -p 5432 -U "${DB_USER}" >/dev/null 2>&1; do + sleep 2 + done +fi + +mkdir -p "$(dirname "${CONFIG_PATH}")" + +if [ ! -s "${CONFIG_PATH}" ]; then + bashio::log.info "Generating default HomeSec config at ${CONFIG_PATH}" + + if [ "${VLM_ENABLED}" = "true" ]; then + VLM_RUN_MODE="trigger_only" + else + VLM_RUN_MODE="never" + fi + + if [ "${STORAGE_TYPE}" = "dropbox" ]; then + STORAGE_BLOCK=$(cat < "${CONFIG_PATH}" +version: 1 + +cameras: [] + +storage: +${STORAGE_BLOCK} + +state_store: + dsn_env: DATABASE_URL + +notifiers: + - backend: home_assistant + config: {} + +filter: + backend: yolo + config: + classes: + - person + +vlm: + backend: openai + trigger_classes: ["person"] + run_mode: ${VLM_RUN_MODE} + config: + api_key_env: OPENAI_API_KEY + model: "${OPENAI_MODEL}" + +alert_policy: + backend: default + enabled: true + config: + min_risk_level: medium + +server: + enabled: true + host: 0.0.0.0 + port: 8080 +EOF_CONFIG +fi + +exec python3 -m homesec.cli run --config "${CONFIG_PATH}" --log_level "${LOG_LEVEL^^}" diff --git a/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/type b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/type new file mode 100644 index 00000000..1780f9f4 --- /dev/null +++ b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/homesec/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/type b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/type new file mode 100644 index 00000000..3d92b15f --- /dev/null +++ b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/type @@ -0,0 +1 @@ +oneshot \ No newline at end of file diff --git a/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up new file mode 100755 index 00000000..9aa722e7 --- /dev/null +++ b/homeassistant/addon/homesec/rootfs/etc/s6-overlay/s6-rc.d/postgres-init/up @@ -0,0 +1,48 @@ +#!/command/with-contenv bashio +set -euo pipefail + +PGDATA="/data/postgres/data" +DB_USER="homesec" +DB_NAME="homesec" +PASS_FILE="/data/postgres/db_password" + +if [ -s "${PASS_FILE}" ]; then + DB_PASS="$(cat "${PASS_FILE}")" +elif bashio::config.has_value 'db_password'; then + DB_PASS="$(bashio::config 'db_password')" + if [ -s "${PGDATA}/PG_VERSION" ]; then + bashio::log.warning "PostgreSQL already initialized; assuming db_password matches." + fi + if [ -n "${DB_PASS}" ]; then + umask 077 + printf '%s' "${DB_PASS}" > "${PASS_FILE}" + fi +else + if [ -s "${PGDATA}/PG_VERSION" ]; then + bashio::log.error "PostgreSQL already initialized but db_password is missing." + bashio::log.error "Set db_password option or remove /data/postgres to re-init." + exit 1 + fi + umask 077 + DB_PASS="$(head -c 32 /dev/urandom | sha256sum | awk '{print $1}')" + printf '%s' "${DB_PASS}" > "${PASS_FILE}" +fi + +if [ ! -s "${PGDATA}/PG_VERSION" ]; then + bashio::log.info "Initializing PostgreSQL data directory..." + mkdir -p /data/postgres + chown -R postgres:postgres /data/postgres + + su postgres -s /bin/sh -c "initdb -D '${PGDATA}' --encoding=UTF8 --locale=C" + su postgres -s /bin/sh -c "pg_ctl -D '${PGDATA}' -o \"-c listen_addresses='127.0.0.1'\" -w start" + su postgres -s /bin/sh -c "psql -v ON_ERROR_STOP=1 --username postgres < Date: Tue, 3 Feb 2026 21:54:03 -0800 Subject: [PATCH 29/31] Phase 4 implementation: HA integration Add Home Assistant custom integration with config flow, coordinator, and entities. Add HA integration tests and harness dependencies. Wire HA lint/tests into Makefile and CI; adjust pytest settings for HA plugin isolation. --- .github/workflows/ci.yml | 11 + Makefile | 17 +- .../custom_components/homesec/__init__.py | 37 + .../homesec/binary_sensor.py | 98 + .../custom_components/homesec/config_flow.py | 277 ++ .../custom_components/homesec/const.py | 35 + .../custom_components/homesec/coordinator.py | 240 + .../custom_components/homesec/diagnostics.py | 27 + .../custom_components/homesec/entity.py | 59 + .../custom_components/homesec/manifest.json | 14 + .../custom_components/homesec/sensor.py | 154 + .../custom_components/homesec/strings.json | 39 + .../custom_components/homesec/switch.py | 51 + .../homesec/translations/en.json | 39 + homeassistant/integration/hacs.json | 5 + homeassistant/integration/tests/conftest.py | 99 + .../integration/tests/test_config_flow.py | 142 + .../integration/tests/test_coordinator.py | 130 + .../integration/tests/test_entities.py | 88 + pyproject.toml | 13 +- tests/homesec/conftest.py | 20 +- tests/homesec/test_state_store.py | 2 +- uv.lock | 4099 +++++++++++------ 23 files changed, 4385 insertions(+), 1311 deletions(-) create mode 100644 homeassistant/integration/custom_components/homesec/__init__.py create mode 100644 homeassistant/integration/custom_components/homesec/binary_sensor.py create mode 100644 homeassistant/integration/custom_components/homesec/config_flow.py create mode 100644 homeassistant/integration/custom_components/homesec/const.py create mode 100644 homeassistant/integration/custom_components/homesec/coordinator.py create mode 100644 homeassistant/integration/custom_components/homesec/diagnostics.py create mode 100644 homeassistant/integration/custom_components/homesec/entity.py create mode 100644 homeassistant/integration/custom_components/homesec/manifest.json create mode 100644 homeassistant/integration/custom_components/homesec/sensor.py create mode 100644 homeassistant/integration/custom_components/homesec/strings.json create mode 100644 homeassistant/integration/custom_components/homesec/switch.py create mode 100644 homeassistant/integration/custom_components/homesec/translations/en.json create mode 100644 homeassistant/integration/hacs.json create mode 100644 homeassistant/integration/tests/conftest.py create mode 100644 homeassistant/integration/tests/test_config_flow.py create mode 100644 homeassistant/integration/tests/test_coordinator.py create mode 100644 homeassistant/integration/tests/test_entities.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c31816f..7d9f7b08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + - name: Setup uv and Python uses: astral-sh/setup-uv@v3 with: @@ -56,9 +61,15 @@ jobs: - name: Lint run: make lint + - name: HA integration lint + run: make ha-lint + - name: Type check run: make typecheck + - name: HA integration tests + run: make ha-test + # Wait for Postgres before migrations - name: Wait for Postgres run: | diff --git a/Makefile b/Makefile index 54ff0fa5..3a4864fe 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL := /bin/bash .SHELLFLAGS := -eu -o pipefail -c -.PHONY: help up down docker-build docker-push run db test coverage typecheck lint check db-migrate db-migration publish +.PHONY: help up down docker-build docker-push run db test coverage typecheck lint check db-migrate db-migration publish ha-lint ha-test help: @echo "Targets:" @@ -20,6 +20,8 @@ help: @echo " make typecheck Run mypy" @echo " make lint Run ruff linter" @echo " make check Run lint + typecheck + test" + @echo " make ha-lint Run ruff on HA integration code" + @echo " make ha-test Run HA integration tests" @echo "" @echo " Database:" @echo " make db-migrate Run migrations" @@ -34,6 +36,9 @@ HOMESEC_LOG_LEVEL ?= INFO DOCKER_IMAGE ?= homesec DOCKER_TAG ?= latest DOCKERHUB_USER ?= $(shell echo $${DOCKERHUB_USER:-}) +HA_INTEGRATION_DIR := homeassistant/integration +HA_INTEGRATION_SRC := $(HA_INTEGRATION_DIR)/custom_components +HA_INTEGRATION_TESTS := $(HA_INTEGRATION_DIR)/tests # Docker up: @@ -78,12 +83,20 @@ lint: uv run ruff check src tests uv run ruff format --check src tests make -C homeassistant shellcheck + make ha-lint + +ha-lint: + uv run ruff check $(HA_INTEGRATION_SRC) $(HA_INTEGRATION_TESTS) + uv run ruff format --check $(HA_INTEGRATION_SRC) $(HA_INTEGRATION_TESTS) + +ha-test: + uv run pytest $(HA_INTEGRATION_TESTS) -q lint-fix: uv run ruff check --fix src tests uv run ruff format src tests -check: lint typecheck test +check: lint typecheck test ha-lint ha-test # Database db-migrate: diff --git a/homeassistant/integration/custom_components/homesec/__init__.py b/homeassistant/integration/custom_components/homesec/__init__.py new file mode 100644 index 00000000..d9cf3a64 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/__init__.py @@ -0,0 +1,37 @@ +"""HomeSec integration setup.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS +from .coordinator import HomesecCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up HomeSec from a config entry.""" + coordinator = HomesecCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + await coordinator.async_subscribe_events() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a HomeSec config entry.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.async_unsubscribe_events() + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return unload_ok + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/integration/custom_components/homesec/binary_sensor.py b/homeassistant/integration/custom_components/homesec/binary_sensor.py new file mode 100644 index 00000000..757f7bd9 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/binary_sensor.py @@ -0,0 +1,98 @@ +"""Binary sensor platform for HomeSec.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_CAMERAS, DOMAIN +from .coordinator import HomesecCoordinator +from .entity import HomesecCameraEntity + + +@dataclass(frozen=True) +class HomesecBinarySensorDescription(BinarySensorEntityDescription): + """Description for HomeSec binary sensors.""" + + +CAMERA_BINARY_SENSORS: tuple[HomesecBinarySensorDescription, ...] = ( + HomesecBinarySensorDescription( + key="motion", + name="Motion", + icon="mdi:motion-sensor", + ), + HomesecBinarySensorDescription( + key="person", + name="Person", + icon="mdi:account", + ), + HomesecBinarySensorDescription( + key="online", + name="Online", + icon="mdi:wifi", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up HomeSec binary sensors from a config entry.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + + camera_names = entry.data.get(CONF_CAMERAS, []) + entities: list[BinarySensorEntity] = [] + + for camera_name in camera_names: + entities.extend( + HomesecCameraBinarySensor(coordinator, camera_name, description) + for description in CAMERA_BINARY_SENSORS + ) + + async_add_entities(entities) + + +class HomesecCameraBinarySensor(HomesecCameraEntity, BinarySensorEntity): + """Camera-level HomeSec binary sensors.""" + + entity_description: HomesecBinarySensorDescription + + def __init__( + self, + coordinator: HomesecCoordinator, + camera_name: str, + description: HomesecBinarySensorDescription, + ) -> None: + super().__init__(coordinator, camera_name) + self.entity_description = description + self._attr_unique_id = f"homesec_{camera_name}_{description.key}" + + @property + def is_on(self) -> bool | None: + key = self.entity_description.key + camera = self._get_camera_data() or {} + alert = self._get_latest_alert() or {} + + if key == "online": + enabled = camera.get("enabled") + healthy = camera.get("healthy") + if enabled is None or healthy is None: + return None + return bool(enabled and healthy) + + if key == "motion": + return self._is_motion_active() + + if key == "person": + detected = alert.get("detected_objects", []) + if isinstance(detected, list) and "person" in detected: + return True + return alert.get("activity_type") == "person" + + return None diff --git a/homeassistant/integration/custom_components/homesec/config_flow.py b/homeassistant/integration/custom_components/homesec/config_flow.py new file mode 100644 index 00000000..116218ed --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/config_flow.py @@ -0,0 +1,277 @@ +"""Config flow for HomeSec.""" + +from __future__ import annotations + +import asyncio +import os +from typing import Any +from urllib.parse import urlsplit + +import aiohttp +import async_timeout +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + ADDON_SLUG, + CONF_ADDON, + CONF_API_KEY, + CONF_CAMERAS, + CONF_HOST, + CONF_PORT, + CONF_VERIFY_SSL, + DEFAULT_MOTION_RESET_SECONDS, + DEFAULT_PORT, + DEFAULT_VERIFY_SSL, +) + + +class CannotConnect(Exception): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(Exception): + """Error to indicate there is invalid auth.""" + + +class HomesecConfigFlow(config_entries.ConfigFlow, domain="homesec"): + """Handle a config flow for HomeSec.""" + + VERSION = 1 + + def __init__(self) -> None: + self._config_data: dict[str, Any] = {} + self._cameras: list[str] = [] + self._title: str = "HomeSec" + + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Initial step - check for add-on first, then manual.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + addon_running, hostname = await detect_addon(self.hass) + if addon_running and hostname: + self._config_data = { + CONF_ADDON: True, + CONF_HOST: hostname, + CONF_PORT: DEFAULT_PORT, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + } + return await self.async_step_addon() + + return await self.async_step_manual() + + async def async_step_addon(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle add-on auto-discovery confirmation.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + info = await validate_connection( + self.hass, + self._config_data[CONF_HOST], + self._config_data[CONF_PORT], + None, + self._config_data[CONF_VERIFY_SSL], + ) + self._title = info.get("title", "HomeSec") + self._cameras = info.get("cameras", []) + return await self.async_step_cameras() + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + + return self.async_show_form( + step_id="addon", + data_schema=vol.Schema({}), + errors=errors, + ) + + async def async_step_manual(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle manual setup for standalone HomeSec.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + info = await validate_connection( + self.hass, + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input.get(CONF_API_KEY), + user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ) + self._title = info.get("title", "HomeSec") + self._cameras = info.get("cameras", []) + self._config_data = { + CONF_ADDON: False, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_API_KEY: user_input.get(CONF_API_KEY), + CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + } + return await self.async_step_cameras() + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + + schema = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + } + ) + return self.async_show_form(step_id="manual", data_schema=schema, errors=errors) + + async def async_step_cameras(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle camera selection step.""" + if user_input is not None: + selected = list(user_input.get(CONF_CAMERAS, [])) + self._config_data[CONF_CAMERAS] = selected + + await self.async_set_unique_id( + f"homesec_{self._config_data[CONF_HOST]}_{self._config_data[CONF_PORT]}" + ) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=self._title, data=self._config_data) + + if not self._cameras: + self._config_data[CONF_CAMERAS] = [] + await self.async_set_unique_id( + f"homesec_{self._config_data[CONF_HOST]}_{self._config_data[CONF_PORT]}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=self._title, data=self._config_data) + + options = {name: name for name in self._cameras} + schema = vol.Schema( + {vol.Optional(CONF_CAMERAS, default=self._cameras): cv.multi_select(options)} + ) + + return self.async_show_form(step_id="cameras", data_schema=schema) + + @staticmethod + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options flow for HomeSec.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + self.config_entry = config_entry + + async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Manage options: scan_interval, motion_reset_seconds.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = self.config_entry.options + schema = vol.Schema( + { + vol.Optional( + "scan_interval", + default=options.get("scan_interval", 30), + ): vol.Coerce(int), + vol.Optional( + "motion_reset_seconds", + default=options.get("motion_reset_seconds", DEFAULT_MOTION_RESET_SECONDS), + ): vol.Coerce(int), + } + ) + return self.async_show_form(step_id="init", data_schema=schema) + + +async def validate_connection( + hass: HomeAssistant, + host: str, + port: int, + api_key: str | None = None, + verify_ssl: bool = True, +) -> dict[str, Any]: + """Validate connection to HomeSec API.""" + base_url = _format_base_url(host, port) + session = async_get_clientsession(hass) + + async def _request(path: str, auth: bool = True) -> Any: + headers = {} + if auth and api_key: + headers["Authorization"] = f"Bearer {api_key}" + + try: + async with ( + async_timeout.timeout(10), + session.get( + f"{base_url}{path}", + headers=headers, + ssl=verify_ssl, + ) as response, + ): + if response.status in (401, 403): + raise InvalidAuth + if response.status >= 400: + raise CannotConnect + return await response.json() + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + raise CannotConnect from exc + + await _request("/api/v1/health", auth=False) + cameras = await _request("/api/v1/cameras", auth=True) + + return { + "title": "HomeSec", + "version": "unknown", + "cameras": [camera.get("name") for camera in cameras if camera.get("name")], + } + + +async def detect_addon(hass: HomeAssistant) -> tuple[bool, str | None]: + """Detect HomeSec add-on via Supervisor API.""" + token = os.getenv("SUPERVISOR_TOKEN") + if not token: + return False, None + + session = async_get_clientsession(hass) + url = f"http://supervisor/addons/{ADDON_SLUG}/info" + headers = {"Authorization": f"Bearer {token}"} + + try: + async with async_timeout.timeout(10), session.get(url, headers=headers) as response: + if response.status != 200: + return False, None + data = await response.json() + except (aiohttp.ClientError, asyncio.TimeoutError): + return False, None + + addon = data.get("data", {}) + if not addon.get("installed") or addon.get("state") != "started": + return False, None + + hostname = addon.get("hostname") + if not hostname: + return False, None + + return True, hostname + + +def _format_base_url(host: str, port: int) -> str: + host = host.rstrip("/") + if host.startswith("http://") or host.startswith("https://"): + base = host + else: + base = f"http://{host}" + + parsed = urlsplit(base) + if parsed.port is None and port: + return f"{base}:{port}" + return base diff --git a/homeassistant/integration/custom_components/homesec/const.py b/homeassistant/integration/custom_components/homesec/const.py new file mode 100644 index 00000000..cc9f4f62 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/const.py @@ -0,0 +1,35 @@ +"""Constants for the HomeSec integration.""" + +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.const import Platform + +DOMAIN = "homesec" + +# Config keys +CONF_HOST = "host" +CONF_PORT = "port" +CONF_API_KEY = "api_key" +CONF_VERIFY_SSL = "verify_ssl" +CONF_CAMERAS = "cameras" +CONF_ADDON = "addon" + +# Defaults +DEFAULT_PORT = 8080 +DEFAULT_VERIFY_SSL = True +ADDON_SLUG = "homesec" + +# Motion sensor +DEFAULT_MOTION_RESET_SECONDS = 30 + +# Platforms +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] + +# Update intervals +SCAN_INTERVAL_SECONDS = 30 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=SCAN_INTERVAL_SECONDS) + +# Events +EVENT_ALERT = "homesec_alert" diff --git a/homeassistant/integration/custom_components/homesec/coordinator.py b/homeassistant/integration/custom_components/homesec/coordinator.py new file mode 100644 index 00000000..299c4fab --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/coordinator.py @@ -0,0 +1,240 @@ +"""Data update coordinator for HomeSec.""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import Callable +from dataclasses import dataclass +from datetime import timedelta +from typing import Any + +import aiohttp +import async_timeout +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_API_KEY, + CONF_CAMERAS, + CONF_HOST, + CONF_PORT, + CONF_VERIFY_SSL, + DEFAULT_MOTION_RESET_SECONDS, + DEFAULT_SCAN_INTERVAL, + EVENT_ALERT, +) + +_LOGGER = logging.getLogger(__name__) + + +class HomesecAuthError(HomeAssistantError): + """Error raised when authentication fails.""" + + +@dataclass +class HomesecApiError(Exception): + """Error raised for HomeSec API errors.""" + + status: int | None + message: str + + +class HomesecCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator to manage HomeSec data updates.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + self.hass = hass + self.entry = entry + + self._host = entry.data[CONF_HOST] + self._port = entry.data[CONF_PORT] + self._api_key = entry.data.get(CONF_API_KEY) + self._verify_ssl = entry.data.get(CONF_VERIFY_SSL, True) + self._camera_names = entry.data.get(CONF_CAMERAS, []) + + options = entry.options + scan_interval = options.get("scan_interval", DEFAULT_SCAN_INTERVAL) + if isinstance(scan_interval, (int, float)): + update_interval = timedelta(seconds=int(scan_interval)) + else: + update_interval = DEFAULT_SCAN_INTERVAL + + self._motion_reset_seconds = int( + options.get("motion_reset_seconds", DEFAULT_MOTION_RESET_SECONDS) + ) + + self._connected = False + self._last_poll_data: dict[str, Any] | None = None + self._motion_active: dict[str, bool] = dict.fromkeys(self._camera_names, False) + self._latest_alerts: dict[str, dict[str, Any]] = {} + self._motion_resets: dict[str, Callable[[], None]] = {} + self._event_unsub: Callable[[], None] | None = None + + self._session = async_get_clientsession(hass) + self._timeout = 10.0 + + super().__init__( + hass, + _LOGGER, + name="homesec", + update_interval=update_interval, + ) + + @property + def base_url(self) -> str: + host = self._host.rstrip("/") + if host.startswith("http://") or host.startswith("https://"): + base = host + else: + base = f"http://{host}" + + if self._port and ":" not in base.split("//", 1)[-1]: + return f"{base}:{self._port}" + return base + + @property + def camera_names(self) -> list[str]: + return list(self._camera_names) + + async def _async_update_data(self) -> dict[str, Any]: + try: + health = await self._request_json("GET", "/api/v1/health", auth=False) + cameras = await self._request_json("GET", "/api/v1/cameras") + stats = await self._request_json("GET", "/api/v1/stats") + except HomesecAuthError as exc: + self._connected = False + raise UpdateFailed("Authentication failed") from exc + except HomesecApiError as exc: + self._connected = False + if exc.status == 503: + raise UpdateFailed("HomeSec unavailable") from exc + raise UpdateFailed(f"HomeSec API error: {exc.message}") from exc + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + self._connected = False + raise UpdateFailed("Error communicating with HomeSec") from exc + + self._connected = True + self._last_poll_data = { + "health": health, + "cameras": cameras, + "stats": stats, + } + return self._build_data() + + async def async_subscribe_events(self) -> None: + """Subscribe to HomeSec events fired via HA Events API.""" + if self._event_unsub is not None: + return + + self._event_unsub = self.hass.bus.async_listen(EVENT_ALERT, self._handle_alert_event) + + async def async_unsubscribe_events(self) -> None: + """Unsubscribe from HomeSec events.""" + if self._event_unsub is not None: + self._event_unsub() + self._event_unsub = None + + for cancel in self._motion_resets.values(): + cancel() + self._motion_resets.clear() + + async def async_add_camera( + self, name: str, source_backend: str, source_config: dict[str, Any] + ) -> dict: + payload = { + "name": name, + "enabled": True, + "source_backend": source_backend, + "source_config": source_config, + } + return await self._request_json("POST", "/api/v1/cameras", payload=payload) + + async def async_update_camera( + self, camera_name: str, source_config: dict[str, Any] | None = None + ) -> dict: + payload: dict[str, Any] = {} + if source_config is not None: + payload["source_config"] = source_config + return await self._request_json("PUT", f"/api/v1/cameras/{camera_name}", payload=payload) + + async def async_delete_camera(self, camera_name: str) -> None: + await self._request_json("DELETE", f"/api/v1/cameras/{camera_name}") + + async def async_set_camera_enabled(self, camera_name: str, enabled: bool) -> dict: + payload = {"enabled": enabled} + return await self._request_json("PUT", f"/api/v1/cameras/{camera_name}", payload=payload) + + async def _handle_alert_event(self, event: Any) -> None: + data = event.data or {} + camera = data.get("camera") + if not camera: + return + + self._latest_alerts[camera] = dict(data) + self._motion_active[camera] = True + + cancel = self._motion_resets.pop(camera, None) + if cancel is not None: + cancel() + + self._motion_resets[camera] = async_call_later( + self.hass, self._motion_reset_seconds, self._clear_motion(camera) + ) + + self.async_set_updated_data(self._build_data()) + + def _clear_motion(self, camera: str) -> Callable[[Any], None]: + def _clear(_: Any) -> None: + self._motion_active[camera] = False + self._motion_resets.pop(camera, None) + self.async_set_updated_data(self._build_data()) + + return _clear + + def _build_data(self) -> dict[str, Any]: + base = self._last_poll_data or {"health": {}, "cameras": [], "stats": {}} + return { + "health": base.get("health", {}), + "cameras": base.get("cameras", []), + "stats": base.get("stats", {}), + "connected": self._connected, + "motion_active": dict(self._motion_active), + "latest_alerts": dict(self._latest_alerts), + } + + async def _request_json( + self, + method: str, + path: str, + payload: dict[str, Any] | None = None, + auth: bool = True, + ) -> dict[str, Any] | list[Any]: + url = f"{self.base_url}{path}" + headers = {} + if auth and self._api_key: + headers["Authorization"] = f"Bearer {self._api_key}" + + try: + async with ( + async_timeout.timeout(self._timeout), + self._session.request( + method, + url, + json=payload, + headers=headers, + ssl=self._verify_ssl, + ) as response, + ): + if response.status in (401, 403): + raise HomesecAuthError("Unauthorized") + if response.status >= 400: + message = await response.text() + raise HomesecApiError(response.status, message) + return await response.json() + except asyncio.TimeoutError as exc: + raise HomesecApiError(None, "Request timed out") from exc diff --git a/homeassistant/integration/custom_components/homesec/diagnostics.py b/homeassistant/integration/custom_components/homesec/diagnostics.py new file mode 100644 index 00000000..87f0afd5 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/diagnostics.py @@ -0,0 +1,27 @@ +"""Diagnostics for HomeSec.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import CONF_API_KEY, DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + data = dict(entry.data) + if CONF_API_KEY in data: + data[CONF_API_KEY] = "REDACTED" + + return { + "config_entry": data, + "options": dict(entry.options), + "coordinator_data": coordinator.data, + } diff --git a/homeassistant/integration/custom_components/homesec/entity.py b/homeassistant/integration/custom_components/homesec/entity.py new file mode 100644 index 00000000..c9c1d188 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/entity.py @@ -0,0 +1,59 @@ +"""Entity base classes for HomeSec.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import HomesecCoordinator + + +class HomesecHubEntity(CoordinatorEntity[HomesecCoordinator]): + """Base class for HomeSec hub entities.""" + + _attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo: + return DeviceInfo( + identifiers={(DOMAIN, "homesec_hub")}, + name="HomeSec Hub", + manufacturer="HomeSec", + model="AI Security Hub", + ) + + +class HomesecCameraEntity(CoordinatorEntity[HomesecCoordinator]): + """Base class for HomeSec camera entities.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: HomesecCoordinator, camera_name: str) -> None: + super().__init__(coordinator) + self._camera_name = camera_name + + @property + def device_info(self) -> DeviceInfo: + return DeviceInfo( + identifiers={(DOMAIN, self._camera_name)}, + name=f"HomeSec {self._camera_name}", + manufacturer="HomeSec", + model="Camera", + via_device=(DOMAIN, "homesec_hub"), + ) + + def _get_camera_data(self) -> dict[str, Any] | None: + cameras = self.coordinator.data.get("cameras", []) + for camera in cameras: + if camera.get("name") == self._camera_name: + return camera + return None + + def _is_motion_active(self) -> bool: + return bool(self.coordinator.data.get("motion_active", {}).get(self._camera_name)) + + def _get_latest_alert(self) -> dict[str, Any] | None: + return self.coordinator.data.get("latest_alerts", {}).get(self._camera_name) diff --git a/homeassistant/integration/custom_components/homesec/manifest.json b/homeassistant/integration/custom_components/homesec/manifest.json new file mode 100644 index 00000000..485626aa --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "homesec", + "name": "HomeSec", + "codeowners": ["@lan17"], + "config_flow": true, + "dependencies": [], + "single_config_entry": true, + "documentation": "https://github.com/lan17/homesec", + "integration_type": "hub", + "iot_class": "local_push", + "issue_tracker": "https://github.com/lan17/homesec/issues", + "requirements": ["aiohttp>=3.8.0"], + "version": "1.0.0" +} diff --git a/homeassistant/integration/custom_components/homesec/sensor.py b/homeassistant/integration/custom_components/homesec/sensor.py new file mode 100644 index 00000000..3854465e --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/sensor.py @@ -0,0 +1,154 @@ +"""Sensor platform for HomeSec.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_CAMERAS, DOMAIN +from .coordinator import HomesecCoordinator +from .entity import HomesecCameraEntity, HomesecHubEntity + + +@dataclass(frozen=True) +class HomesecHubSensorDescription(SensorEntityDescription): + """Description for HomeSec hub sensors.""" + + +@dataclass(frozen=True) +class HomesecCameraSensorDescription(SensorEntityDescription): + """Description for HomeSec camera sensors.""" + + +HUB_SENSORS: tuple[HomesecHubSensorDescription, ...] = ( + HomesecHubSensorDescription( + key="cameras_online", + name="Cameras Online", + icon="mdi:cctv", + ), + HomesecHubSensorDescription( + key="alerts_today", + name="Alerts Today", + icon="mdi:bell", + ), + HomesecHubSensorDescription( + key="clips_today", + name="Clips Today", + icon="mdi:video", + ), + HomesecHubSensorDescription( + key="system_health", + name="System Health", + icon="mdi:heart-pulse", + ), +) + +CAMERA_SENSORS: tuple[HomesecCameraSensorDescription, ...] = ( + HomesecCameraSensorDescription( + key="last_activity", + name="Last Activity", + icon="mdi:motion-sensor", + ), + HomesecCameraSensorDescription( + key="risk_level", + name="Risk Level", + icon="mdi:alert", + ), + HomesecCameraSensorDescription( + key="health", + name="Health", + icon="mdi:camera", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up HomeSec sensors from a config entry.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensorEntity] = [ + HomesecHubSensor(coordinator, description) for description in HUB_SENSORS + ] + + camera_names = entry.data.get(CONF_CAMERAS, []) + for camera_name in camera_names: + entities.extend( + HomesecCameraSensor(coordinator, camera_name, description) + for description in CAMERA_SENSORS + ) + + async_add_entities(entities) + + +class HomesecHubSensor(HomesecHubEntity, SensorEntity): + """Hub-level HomeSec sensors.""" + + entity_description: HomesecHubSensorDescription + + def __init__( + self, coordinator: HomesecCoordinator, description: HomesecHubSensorDescription + ) -> None: + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"homesec_hub_{description.key}" + + @property + def native_value(self) -> Any: + key = self.entity_description.key + stats = self.coordinator.data.get("stats", {}) + health = self.coordinator.data.get("health", {}) + + if key == "system_health": + return health.get("status") + if key == "cameras_online": + return stats.get("cameras_online") + if key == "alerts_today": + return stats.get("alerts_today") + if key == "clips_today": + return stats.get("clips_today") + return None + + +class HomesecCameraSensor(HomesecCameraEntity, SensorEntity): + """Camera-level HomeSec sensors.""" + + entity_description: HomesecCameraSensorDescription + + def __init__( + self, + coordinator: HomesecCoordinator, + camera_name: str, + description: HomesecCameraSensorDescription, + ) -> None: + super().__init__(coordinator, camera_name) + self.entity_description = description + self._attr_unique_id = f"homesec_{camera_name}_{description.key}" + + @property + def native_value(self) -> Any: + key = self.entity_description.key + camera = self._get_camera_data() or {} + alert = self._get_latest_alert() or {} + + if key == "health": + healthy = camera.get("healthy") + if healthy is None: + return None + return "healthy" if healthy else "unhealthy" + + if key == "last_activity": + return alert.get("activity_type") + + if key == "risk_level": + return alert.get("risk_level") + + return None diff --git a/homeassistant/integration/custom_components/homesec/strings.json b/homeassistant/integration/custom_components/homesec/strings.json new file mode 100644 index 00000000..cf42f5d0 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/strings.json @@ -0,0 +1,39 @@ +{ + "title": "HomeSec", + "config": { + "step": { + "user": { + "title": "Connect to HomeSec", + "description": "Set up HomeSec for this Home Assistant instance." + }, + "addon": { + "title": "Use HomeSec add-on", + "description": "HomeSec add-on detected. Confirm to connect." + }, + "manual": { + "title": "Manual setup", + "description": "Enter connection details for a HomeSec server." + }, + "cameras": { + "title": "Select cameras", + "description": "Choose which cameras to add to Home Assistant." + } + }, + "error": { + "cannot_connect": "Failed to connect to HomeSec.", + "invalid_auth": "Invalid authentication.", + "single_instance_allowed": "Only one HomeSec instance is allowed." + }, + "abort": { + "single_instance_allowed": "Only one HomeSec instance is allowed." + } + }, + "options": { + "step": { + "init": { + "title": "HomeSec options", + "description": "Adjust polling and motion reset timing." + } + } + } +} diff --git a/homeassistant/integration/custom_components/homesec/switch.py b/homeassistant/integration/custom_components/homesec/switch.py new file mode 100644 index 00000000..951ff631 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/switch.py @@ -0,0 +1,51 @@ +"""Switch platform for HomeSec.""" + +from __future__ import annotations + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_CAMERAS, DOMAIN +from .coordinator import HomesecCoordinator +from .entity import HomesecCameraEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up HomeSec switches from a config entry.""" + coordinator: HomesecCoordinator = hass.data[DOMAIN][entry.entry_id] + + camera_names = entry.data.get(CONF_CAMERAS, []) + entities = [HomesecCameraEnabledSwitch(coordinator, name) for name in camera_names] + async_add_entities(entities) + + +class HomesecCameraEnabledSwitch(HomesecCameraEntity, SwitchEntity): + """Enable/disable a HomeSec camera.""" + + _attr_name = "Enabled" + + def __init__(self, coordinator: HomesecCoordinator, camera_name: str) -> None: + super().__init__(coordinator, camera_name) + self._attr_unique_id = f"homesec_{camera_name}_enabled" + + @property + def is_on(self) -> bool | None: + camera = self._get_camera_data() or {} + enabled = camera.get("enabled") + if enabled is None: + return None + return bool(enabled) + + async def async_turn_on(self, **_: object) -> None: + await self.coordinator.async_set_camera_enabled(self._camera_name, True) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **_: object) -> None: + await self.coordinator.async_set_camera_enabled(self._camera_name, False) + await self.coordinator.async_refresh() diff --git a/homeassistant/integration/custom_components/homesec/translations/en.json b/homeassistant/integration/custom_components/homesec/translations/en.json new file mode 100644 index 00000000..cf42f5d0 --- /dev/null +++ b/homeassistant/integration/custom_components/homesec/translations/en.json @@ -0,0 +1,39 @@ +{ + "title": "HomeSec", + "config": { + "step": { + "user": { + "title": "Connect to HomeSec", + "description": "Set up HomeSec for this Home Assistant instance." + }, + "addon": { + "title": "Use HomeSec add-on", + "description": "HomeSec add-on detected. Confirm to connect." + }, + "manual": { + "title": "Manual setup", + "description": "Enter connection details for a HomeSec server." + }, + "cameras": { + "title": "Select cameras", + "description": "Choose which cameras to add to Home Assistant." + } + }, + "error": { + "cannot_connect": "Failed to connect to HomeSec.", + "invalid_auth": "Invalid authentication.", + "single_instance_allowed": "Only one HomeSec instance is allowed." + }, + "abort": { + "single_instance_allowed": "Only one HomeSec instance is allowed." + } + }, + "options": { + "step": { + "init": { + "title": "HomeSec options", + "description": "Adjust polling and motion reset timing." + } + } + } +} diff --git a/homeassistant/integration/hacs.json b/homeassistant/integration/hacs.json new file mode 100644 index 00000000..167d4719 --- /dev/null +++ b/homeassistant/integration/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "HomeSec", + "render_readme": true, + "domains": ["homesec"] +} diff --git a/homeassistant/integration/tests/conftest.py b/homeassistant/integration/tests/conftest.py new file mode 100644 index 00000000..5c8788a4 --- /dev/null +++ b/homeassistant/integration/tests/conftest.py @@ -0,0 +1,99 @@ +"""Fixtures for HomeSec Home Assistant integration tests.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + +import pytest + +pytest_plugins = "pytest_homeassistant_custom_component" + +ROOT = Path(__file__).resolve().parents[2] +INTEGRATION_ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) +if str(INTEGRATION_ROOT) not in sys.path: + sys.path.insert(0, str(INTEGRATION_ROOT)) + +from homesec.api.routes.cameras import CameraResponse +from homesec.api.routes.health import HealthResponse +from homesec.api.routes.stats import StatsResponse + + +@pytest.fixture(autouse=True) +def _enable_custom_integrations(enable_custom_integrations) -> None: + """Enable custom integrations for HomeSec tests.""" + _ = enable_custom_integrations + + +@pytest.fixture +def homesec_base_url() -> str: + return "http://homesec.local:8080" + + +@pytest.fixture +def health_payload() -> dict[str, Any]: + data = { + "status": "healthy", + "pipeline": "running", + "postgres": "connected", + "cameras_online": 1, + } + HealthResponse.model_validate(data) + return data + + +@pytest.fixture +def stats_payload() -> dict[str, Any]: + data = { + "clips_today": 3, + "alerts_today": 2, + "cameras_total": 2, + "cameras_online": 1, + "uptime_seconds": 120.0, + } + StatsResponse.model_validate(data) + return data + + +@pytest.fixture +def cameras_payload() -> list[dict[str, Any]]: + cameras = [ + { + "name": "front", + "enabled": True, + "source_backend": "rtsp", + "healthy": True, + "last_heartbeat": 1_695_000_000.0, + "source_config": {"url": "rtsp://camera"}, + }, + { + "name": "back", + "enabled": False, + "source_backend": "rtsp", + "healthy": False, + "last_heartbeat": None, + "source_config": {"url": "rtsp://camera2"}, + }, + ] + + for camera in cameras: + CameraResponse.model_validate(camera) + return cameras + + +@pytest.fixture +def alert_payload() -> dict[str, Any]: + return { + "camera": "front", + "clip_id": "clip-123", + "activity_type": "person", + "risk_level": "high", + "summary": "Person at front door", + "view_url": "http://example/clip", + "storage_uri": "local://clip-123", + "timestamp": "2026-02-04T00:00:00+00:00", + "detected_objects": ["person"], + } diff --git a/homeassistant/integration/tests/test_config_flow.py b/homeassistant/integration/tests/test_config_flow.py new file mode 100644 index 00000000..5099955b --- /dev/null +++ b/homeassistant/integration/tests/test_config_flow.py @@ -0,0 +1,142 @@ +"""Tests for HomeSec config flow.""" + +from __future__ import annotations + +import pytest +from custom_components.homesec.const import DOMAIN +from homeassistant import config_entries + + +@pytest.mark.asyncio +async def test_config_flow_manual_success( + hass, + aioclient_mock, + homesec_base_url, + health_payload, + cameras_payload, +) -> None: + """Test manual setup completes with camera selection.""" + # Given: A reachable HomeSec API with cameras + aioclient_mock.get(f"{homesec_base_url}/api/v1/health", json=health_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/cameras", json=cameras_payload) + + # When: User starts manual config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Then: Manual step is shown + assert result["type"] == "form" + assert result["step_id"] == "manual" + + # When: User submits host/port/api key + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "homesec.local", + "port": 8080, + "api_key": "token", + "verify_ssl": True, + }, + ) + + # Then: Camera selection step appears + assert result["type"] == "form" + assert result["step_id"] == "cameras" + + # When: User selects cameras + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"cameras": ["front"]}, + ) + + # Then: Config entry is created + assert result["type"] == "create_entry" + assert result["data"]["cameras"] == ["front"] + + +@pytest.mark.asyncio +async def test_config_flow_manual_invalid_auth( + hass, + aioclient_mock, + homesec_base_url, + health_payload, +) -> None: + """Test manual setup shows invalid auth on 401.""" + # Given: Health endpoint is reachable but cameras endpoint rejects auth + aioclient_mock.get(f"{homesec_base_url}/api/v1/health", json=health_payload) + aioclient_mock.get( + f"{homesec_base_url}/api/v1/cameras", + status=401, + json={"detail": "Unauthorized"}, + ) + + # When: User submits manual config + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "homesec.local", + "port": 8080, + "api_key": "bad", + "verify_ssl": True, + }, + ) + + # Then: Invalid auth error shown + assert result["type"] == "form" + assert result["errors"]["base"] == "invalid_auth" + + +@pytest.mark.asyncio +async def test_config_flow_addon_detected( + hass, + aioclient_mock, + monkeypatch, + health_payload, + cameras_payload, +) -> None: + """Test add-on discovery path.""" + # Given: Supervisor reports HomeSec add-on running + monkeypatch.setenv("SUPERVISOR_TOKEN", "token") + aioclient_mock.get( + "http://supervisor/addons/homesec/info", + json={ + "data": { + "installed": True, + "state": "started", + "hostname": "abc123-homesec", + } + }, + ) + aioclient_mock.get("http://abc123-homesec:8080/api/v1/health", json=health_payload) + aioclient_mock.get("http://abc123-homesec:8080/api/v1/cameras", json=cameras_payload) + + # When: User starts config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Then: Add-on step is shown + assert result["type"] == "form" + assert result["step_id"] == "addon" + + # When: User confirms add-on + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + # Then: Camera selection step is shown + assert result["type"] == "form" + assert result["step_id"] == "cameras" + + # When: User selects cameras + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"cameras": ["front", "back"]}, + ) + + # Then: Config entry is created + assert result["type"] == "create_entry" + assert result["data"]["addon"] is True + assert result["data"]["cameras"] == ["front", "back"] diff --git a/homeassistant/integration/tests/test_coordinator.py b/homeassistant/integration/tests/test_coordinator.py new file mode 100644 index 00000000..57b7310d --- /dev/null +++ b/homeassistant/integration/tests/test_coordinator.py @@ -0,0 +1,130 @@ +"""Tests for HomeSec coordinator.""" + +from __future__ import annotations + +from datetime import timedelta + +import pytest +from custom_components.homesec.const import DOMAIN, EVENT_ALERT +from custom_components.homesec.coordinator import HomesecCoordinator +from homeassistant.util import dt as dt_util +from pytest_homeassistant_custom_component.common import MockConfigEntry, async_fire_time_changed + + +@pytest.mark.asyncio +async def test_coordinator_update_success( + hass, + aioclient_mock, + homesec_base_url, + health_payload, + cameras_payload, + stats_payload, +) -> None: + """Test successful coordinator refresh.""" + # Given: HomeSec API responds successfully + aioclient_mock.get(f"{homesec_base_url}/api/v1/health", json=health_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/cameras", json=cameras_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/stats", json=stats_payload) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "homesec.local", + "port": 8080, + "api_key": "token", + "verify_ssl": True, + "cameras": ["front", "back"], + }, + ) + entry.add_to_hass(hass) + + coordinator = HomesecCoordinator(hass, entry) + + # When: Refreshing coordinator + await coordinator.async_refresh() + + # Then: Data reflects API payloads + assert coordinator.data["health"]["status"] == "healthy" + assert len(coordinator.data["cameras"]) == 2 + assert coordinator.data["stats"]["clips_today"] == 3 + assert coordinator.data["connected"] is True + + +@pytest.mark.asyncio +async def test_coordinator_update_503(hass, aioclient_mock, homesec_base_url) -> None: + """Test coordinator handles 503 errors.""" + # Given: Health endpoint returns 503 + aioclient_mock.get( + f"{homesec_base_url}/api/v1/health", + status=503, + json={"status": "unhealthy"}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "homesec.local", + "port": 8080, + "api_key": "token", + "verify_ssl": True, + "cameras": ["front"], + }, + ) + entry.add_to_hass(hass) + + coordinator = HomesecCoordinator(hass, entry) + + # When: Refresh runs + await coordinator.async_refresh() + + # Then: Coordinator marks update as failed + assert coordinator.last_update_success is False + + +@pytest.mark.asyncio +async def test_coordinator_event_motion_resets( + hass, + aioclient_mock, + homesec_base_url, + health_payload, + cameras_payload, + stats_payload, + alert_payload, +) -> None: + """Test motion state resets after timeout.""" + # Given: Coordinator has initial data and short reset interval + aioclient_mock.get(f"{homesec_base_url}/api/v1/health", json=health_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/cameras", json=cameras_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/stats", json=stats_payload) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "homesec.local", + "port": 8080, + "api_key": "token", + "verify_ssl": True, + "cameras": ["front"], + }, + options={"motion_reset_seconds": 1}, + ) + entry.add_to_hass(hass) + + coordinator = HomesecCoordinator(hass, entry) + await coordinator.async_refresh() + await coordinator.async_subscribe_events() + + # When: Alert event fires + hass.bus.async_fire(EVENT_ALERT, alert_payload) + await hass.async_block_till_done() + + # Then: Motion is active + assert coordinator.data["motion_active"]["front"] is True + + # When: Time advances beyond reset window + future = dt_util.utcnow() + timedelta(seconds=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + # Then: Motion resets to inactive + assert coordinator.data["motion_active"]["front"] is False diff --git a/homeassistant/integration/tests/test_entities.py b/homeassistant/integration/tests/test_entities.py new file mode 100644 index 00000000..b115e160 --- /dev/null +++ b/homeassistant/integration/tests/test_entities.py @@ -0,0 +1,88 @@ +"""Tests for HomeSec entities.""" + +from __future__ import annotations + +import pytest +from custom_components.homesec.const import DOMAIN, EVENT_ALERT +from pytest_homeassistant_custom_component.common import MockConfigEntry + + +@pytest.mark.asyncio +async def test_entities_reflect_api_state( + hass, + aioclient_mock, + homesec_base_url, + health_payload, + cameras_payload, + stats_payload, +) -> None: + """Test entities reflect API data.""" + # Given: HomeSec API data for hub and cameras + aioclient_mock.get(f"{homesec_base_url}/api/v1/health", json=health_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/cameras", json=cameras_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/stats", json=stats_payload) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "homesec.local", + "port": 8080, + "api_key": "token", + "verify_ssl": True, + "cameras": ["front", "back"], + }, + ) + entry.add_to_hass(hass) + + # When: The integration is set up + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Then: Hub and camera entities are created with expected states + assert hass.states.get("sensor.homesec_hub_system_health").state == "healthy" + assert hass.states.get("sensor.homesec_hub_alerts_today").state == "2" + assert hass.states.get("binary_sensor.homesec_front_online").state == "on" + assert hass.states.get("binary_sensor.homesec_back_online").state == "off" + assert hass.states.get("switch.homesec_front_enabled").state == "on" + assert hass.states.get("switch.homesec_back_enabled").state == "off" + + +@pytest.mark.asyncio +async def test_motion_event_updates_entities( + hass, + aioclient_mock, + homesec_base_url, + health_payload, + cameras_payload, + stats_payload, + alert_payload, +) -> None: + """Test motion event updates motion and last activity.""" + # Given: Integration set up and alert event payload + aioclient_mock.get(f"{homesec_base_url}/api/v1/health", json=health_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/cameras", json=cameras_payload) + aioclient_mock.get(f"{homesec_base_url}/api/v1/stats", json=stats_payload) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "homesec.local", + "port": 8080, + "api_key": "token", + "verify_ssl": True, + "cameras": ["front"], + }, + options={"motion_reset_seconds": 30}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # When: An alert event is fired + hass.bus.async_fire(EVENT_ALERT, alert_payload) + await hass.async_block_till_done() + + # Then: Motion and last activity sensors update + assert hass.states.get("binary_sensor.homesec_front_motion").state == "on" + assert hass.states.get("sensor.homesec_front_last_activity").state == "person" diff --git a/pyproject.toml b/pyproject.toml index 338482df..ad776bb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,8 +38,8 @@ dependencies = [ "asyncpg>=0.29.0", "ultralytics>=8.3.226", "uvicorn>=0.30.0", - "pyyaml>=6.0.3", - "aiohttp>=3.13.2", + "pyyaml>=6.0.2", + "aiohttp>=3.11.13", "anyio>=4.0.0", "greenlet>=3.3.0", "pyftpdlib>=2.1.0", @@ -77,9 +77,10 @@ dev = [ "ipykernel>=7.1.0", "mypy>=1.19.1", "nbstripout>=0.8.2", - "pytest>=9.0.2", - "pytest-asyncio>=1.3.0", + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", "pytest-cov>=4.1.0", + "pytest-homeassistant-custom-component>=0.13.300", "ruff>=0.14.10", "types-pyyaml>=6.0.12.20250915", ] @@ -88,6 +89,7 @@ dev = [ pythonpath = ["src"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +addopts = "-p no:pytest_homeassistant_custom_component" [tool.ruff] src = ["src", "tests"] @@ -130,3 +132,6 @@ major_on_zero = false # Don't bump to 1.0.0 on breaking changes while in 0.x # feat = minor, fix/perf/chore/docs/style/refactor = patch # BREAKING CHANGE = major (but blocked by major_on_zero while in 0.x) patch_tags = ["fix", "perf", "chore", "docs", "style", "refactor"] + +[tool.uv] +environments = ["sys_platform != 'win32' and python_full_version >= '3.13.2'"] diff --git a/tests/homesec/conftest.py b/tests/homesec/conftest.py index bde9672f..50d3b61b 100644 --- a/tests/homesec/conftest.py +++ b/tests/homesec/conftest.py @@ -21,6 +21,24 @@ ) +@pytest.fixture(autouse=True) +def _enable_socket(socket_enabled) -> None: + """Allow socket usage for tests that hit local Postgres.""" + _ = socket_enabled + + +@pytest.fixture +def enable_event_loop_debug() -> None: + """Override HA plugin fixture to avoid HA event loop policy in core tests.""" + return None + + +@pytest.fixture +def verify_cleanup() -> None: + """Override HA plugin cleanup verification for core tests.""" + return None + + @pytest.fixture def mock_filter() -> MockFilter: """Return a MockFilter with default config.""" @@ -71,7 +89,7 @@ def postgres_dsn() -> str: """Return test Postgres DSN (requires local DB running).""" import os - return os.getenv("TEST_DB_DSN", "postgresql://homesec:homesec@localhost:5432/homesec") + return os.getenv("TEST_DB_DSN", "postgresql://homesec:homesec@127.0.0.1:5432/homesec") @pytest.fixture diff --git a/tests/homesec/test_state_store.py b/tests/homesec/test_state_store.py index 306b36f6..762d6777 100644 --- a/tests/homesec/test_state_store.py +++ b/tests/homesec/test_state_store.py @@ -11,7 +11,7 @@ from homesec.state.postgres import Base, ClipState, _normalize_async_dsn # Default DSN for local Docker Postgres (matches docker-compose.postgres.yml) -DEFAULT_DSN = "postgresql://homesec:homesec@localhost:5432/homesec" +DEFAULT_DSN = "postgresql://homesec:homesec@127.0.0.1:5432/homesec" def get_test_dsn() -> str: diff --git a/uv.lock b/uv.lock index bc2496b1..ce0b36be 100644 --- a/uv.lock +++ b/uv.lock @@ -2,12 +2,39 @@ version = 1 revision = 2 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.13.2' and python_full_version < '3.14' and sys_platform != 'win32'", +] +supported-markers = [ + "python_full_version >= '3.13.2' and sys_platform != 'win32'", +] + +[[package]] +name = "acme" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "josepy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyopenssl", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyrfc3339", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/f6/897be0abeb0e64f0e6136a8a6369a54d2a603a44cb7a411f6d77dbafb4ac/acme-5.1.0.tar.gz", hash = "sha256:7b97820857d9baffed98bca50ab82bb6a636e447865d7a013a7bdd7972f03cda", size = 89982, upload-time = "2025-10-07T17:30:38.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/0b/4d0421412bb063f4393ae7ebf3a9a6fde621aed187a1140ccf7f9e22b823/acme-5.1.0-py3-none-any.whl", hash = "sha256:80e9c315d82302bb97279f4516ff31230d29195ab9d4a6c9411ceec20481b61e", size = 94151, upload-time = "2025-10-07T17:30:15.994Z" }, +] + +[[package]] +name = "aiodns" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycares", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2f/9d1ee4f937addda60220f47925dac6c6b3782f6851fd578987284a8d2491/aiodns-3.6.1.tar.gz", hash = "sha256:b0e9ce98718a5b8f7ca8cd16fc393163374bc2412236b91f6c851d066e3324b6", size = 15143, upload-time = "2025-12-11T12:53:07.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/e3/9f777774ebe8f664bcd564f9de3936490a16effa82a969372161c9b0fb21/aiodns-3.6.1-py3-none-any.whl", hash = "sha256:46233ccad25f2037903828c5d05b64590eaa756e51d12b4a5616e2defcbc98c7", size = 7975, upload-time = "2025-12-11T12:53:06.387Z" }, ] [[package]] @@ -19,124 +46,173 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] +[[package]] +name = "aiohasupervisor" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "mashumaro", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "orjson", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e0/f8865efa28ce22e44e3526f18654c7a69a6f0d0e8523e2aaf743f2798fd8/aiohasupervisor-0.3.3.tar.gz", hash = "sha256:24e268f58f37f9d8dafadba2ef9d860292ff622bc6e78b1ca4ef5e5095d1bbc8", size = 44696, upload-time = "2025-10-01T14:55:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/97/b811d22148e7227e6f02a1f0f13f60d959bb163c806feab853544da07c3e/aiohasupervisor-0.3.3-py3-none-any.whl", hash = "sha256:bc185dbb81bb8ec6ba91b5512df7fd3bf99db15e648b20aed3f8ce7dc3203f1f", size = 40486, upload-time = "2025-10-01T14:55:56.52Z" }, +] + [[package]] name = "aiohttp" -version = "3.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471, upload-time = "2025-10-28T20:55:27.924Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985, upload-time = "2025-10-28T20:55:29.456Z" }, - { url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274, upload-time = "2025-10-28T20:55:31.134Z" }, - { url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171, upload-time = "2025-10-28T20:55:36.065Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036, upload-time = "2025-10-28T20:55:37.576Z" }, - { url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975, upload-time = "2025-10-28T20:55:39.457Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823, upload-time = "2025-10-28T20:55:40.958Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374, upload-time = "2025-10-28T20:55:42.745Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315, upload-time = "2025-10-28T20:55:44.264Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140, upload-time = "2025-10-28T20:55:46.626Z" }, - { url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496, upload-time = "2025-10-28T20:55:48.933Z" }, - { url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625, upload-time = "2025-10-28T20:55:50.377Z" }, - { url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025, upload-time = "2025-10-28T20:55:51.861Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918, upload-time = "2025-10-28T20:55:53.515Z" }, - { url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113, upload-time = "2025-10-28T20:55:55.438Z" }, - { url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290, upload-time = "2025-10-28T20:55:56.96Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075, upload-time = "2025-10-28T20:55:58.373Z" }, - { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, - { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, - { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, - { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, - { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, - { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, - { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, - { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, - { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, - { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, - { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, - { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, - { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, - { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, - { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, - { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, - { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, - { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, - { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, - { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, - { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, - { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, - { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, - { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, - { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, - { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, - { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiosignal", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "attrs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "frozenlist", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "multidict", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "propcache", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "yarl", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, +] + +[[package]] +name = "aiohttp-asyncmdnsresolver" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiodns", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "zeroconf", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/83/09fb97705e7308f94197a09b486669696ea20f28074c14b5811a38bdedc3/aiohttp_asyncmdnsresolver-0.1.1.tar.gz", hash = "sha256:8c65d4b08b42c8a260717a2766bd5967a1d437cee852a9b21f3928b5171a7c81", size = 36129, upload-time = "2025-02-14T14:46:44.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/d1/4f61508a43de82bb5c60cede3bb89cc57c5e8af7978d93ca03ad60b99368/aiohttp_asyncmdnsresolver-0.1.1-py3-none-any.whl", hash = "sha256:d04ded993e9f0e07c07a1bc687cde447d9d32e05bcf55ecbf94f63b33dcab93e", size = 13582, upload-time = "2025-02-14T14:46:41.985Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/a6/982f3a013b42e914a2420631afcaecb729c49525cc6cc58e15d27ee4cb4b/aiohttp_fast_zlib-0.3.0.tar.gz", hash = "sha256:963a09de571b67fa0ef9cb44c5a32ede5cb1a51bc79fc21181b1cddd56b58b28", size = 8770, upload-time = "2025-06-07T12:41:49.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/11/ea9ecbcd6cf68c5de690fd39b66341405ab091aa0c3598277e687aa65901/aiohttp_fast_zlib-0.3.0-py3-none-any.whl", hash = "sha256:d4cb20760a3e1137c93cb42c13871cbc9cd1fdc069352f2712cd650d6c0e537e", size = 8615, upload-time = "2025-06-07T12:41:47.454Z" }, +] + +[[package]] +name = "aiooui" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b7/ad0f86010bbabc4e556e98dd2921a923677188223cc524432695966f14fa/aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8", size = 369276, upload-time = "2025-01-19T00:12:44.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/32/2a084b27ac45efd62213a29fb93b40a95089760c896c2e36d2f8444e3c1a/aiooui-0.1.9-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:64d904b43f14dd1d8d9fcf1684d9e2f558bc5e0bd68dc10023c93355c9027907", size = 367352, upload-time = "2025-01-19T00:12:39.761Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fa/b1310457adbea7adb84d2c144159f3b41341c40c80df3c10ce6b266874b3/aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5", size = 367404, upload-time = "2025-01-19T00:12:42.57Z" }, ] [[package]] @@ -144,23 +220,33 @@ name = "aiosignal" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "frozenlist", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "aiozoneinfo" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/00/e437a179ab78ed24780ded10bbb5d7e10832c07f62eab1d44ee2f335c95c/aiozoneinfo-0.2.3.tar.gz", hash = "sha256:987ce2a7d5141f3f4c2e9d50606310d0bf60d688ad9f087aa7267433ba85fff3", size = 8381, upload-time = "2025-02-04T19:32:06.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/a4/99e13bb4006999de2a4d63cee7497c3eb7f616b0aefc660c4c316179af3a/aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661", size = 8009, upload-time = "2025-02-04T19:32:04.74Z" }, +] + [[package]] name = "alembic" version = "1.17.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mako" }, - { name = "sqlalchemy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, + { name = "mako", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" } wheels = [ @@ -185,15 +271,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "annotatedyaml" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "propcache", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "voluptuous", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/4b/973067092ee348e331d125acd60c45245f11663373c219650814b43d0025/annotatedyaml-1.0.2.tar.gz", hash = "sha256:f9a49952994ef1952ca17d27bb6478342eb1189d2c28e4c0ddbbb32065471fb0", size = 15366, upload-time = "2025-10-04T14:36:26.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/0f/4482333d679e7174b74655d17b3969ab3754ae4d581752bac1002fe316c0/annotatedyaml-1.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:359a964daf3fccbb4818e6f08478d2e6712a2417a261cbd6472826ce5e8f1503", size = 58944, upload-time = "2025-10-04T14:41:49.516Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/ab5f9c67dd13b54e0100e8a4cdfd371c45ecfea1ba776a971d7b728087fe/annotatedyaml-1.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6d2dcf741bdedf893d04f958f3f1ad0b5b12b1fe27746f9918a24e2f347eac1", size = 60030, upload-time = "2025-10-04T14:41:50.859Z" }, + { url = "https://files.pythonhosted.org/packages/47/3f/785a22acee2fc16049ac00a9f708f11b1354e40578ae4e5076b989dc5f82/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:139533a395301f219bfd4ba2265b7a8c55cb4931aac7f730a8ff204a465e76d3", size = 70701, upload-time = "2025-10-04T14:41:52.091Z" }, + { url = "https://files.pythonhosted.org/packages/e8/93/712a6170903b6dd2a30aa59f76e39569f260fde38e9277d3a40ddbdf53f4/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:623f571e0d3819a3cbadce592a2c691274ccd46b09ad770f9271201d7476ea88", size = 65011, upload-time = "2025-10-04T14:41:53.35Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/5f3e9b72d871d67f89d70166d0e2affdbcf0cf87cd20276c84b6db968a52/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75927ec682f188efe25309e259c115e3976b702900ce1be93a971b328c87a10a", size = 71370, upload-time = "2025-10-04T14:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/a4/32/143d643d9f5d21f1b66666713d24adf68677790fb61700bc727078bdef2c/annotatedyaml-1.0.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:106ac5eaa022df4dfa42e307932aa2a197a19151de3bb41e98840cfc7f1745e1", size = 69434, upload-time = "2025-10-04T14:36:24.962Z" }, + { url = "https://files.pythonhosted.org/packages/83/b0/1ce75b81e42e033914f94159f633b923e57c507690eb0bba966475cab9a1/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:139d2626fe8faccba9cc79b4d8dca25a4d59e4a274508612842d78945bddeebe", size = 71333, upload-time = "2025-10-04T14:41:55.836Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/89451246b115dfc5fcfb3b3ca966f9fcdfc647b10a13eb517fa11f2e3ffd/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6f2fb86c18064f0dcfb01e3d1096f0575cdff509a24b748c2994f97eb0b70156", size = 65905, upload-time = "2025-10-04T14:41:57.121Z" }, + { url = "https://files.pythonhosted.org/packages/70/57/7008f39f1af0e0b36668cd9affe8a68846797ee1119ec36daac428ade742/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56d7235147b58d155b4ee93f2a92920b4c0be6a6852dc3fd810c67f6e56f8c15", size = 72181, upload-time = "2025-10-04T14:41:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/1b/91/0acf5b74926c6964812d9ed752af77531ab4daa06fba1cb668d9006e9e1f/annotatedyaml-1.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c42f385c3f04f425d5948c16afbb94a876da867be276dbf2c2e7436b9a80792d", size = 58962, upload-time = "2025-10-04T14:42:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/71/f6/5dac1ce125984db4cb99d883f234e6a8c0e49358a9136047a490bc2ba51a/annotatedyaml-1.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b5d9d24ba907fd2e905eac69c88e651310c480980a17aa57faf0599ff21f586f", size = 60252, upload-time = "2025-10-04T14:42:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/81dea3e4272927518abb9c96ab299b8c4346c40267740bfb8d6b0cdb317f/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2572b7c3c630dae1dd163d6c6ba847493a7f987437941b32d0ad8354615f358a", size = 71219, upload-time = "2025-10-04T14:42:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/5aa3d7767cb92e8ba34cba582e5b088f42746e6d075f7d387fcdc4e5dd62/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b28fe13eb0014a0dd06c9a292466feed0cd298ab10525ef9a37089df01c7d333", size = 64459, upload-time = "2025-10-04T14:42:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/a0/eb/b29b84eec6d3a1fc3278ff2959388f347e7853a3f82fc5275c591a523835/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:987f73a13f121c775bcdb082214c17f114447fee7dad37db2f86b035893ad58d", size = 71175, upload-time = "2025-10-04T14:42:05.22Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7c/4f4bf854f4b62cade7485a9572773d4440ee535e905f166b441b2d3f19a7/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5cb4ee79c08da2b8f4f24b1775732ca6c497682f3c9b3fd65dee4ea084fc925c", size = 71824, upload-time = "2025-10-04T14:42:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/68/ac/1f903eeccde636723fcf664b372a6ab253b7f13c3de446ff5bce6852d696/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:da065a8c29556219fce1aa81b406e84f73bc2181067658e57428a8b2e662fc1b", size = 65343, upload-time = "2025-10-04T14:42:07.727Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/43e83b50a42ad5c51abf1a335cfc249e182f66542d7c7306ee07397b1956/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ba9937418c1b189b267540b47fa0dc24c148292739d06a6ca31c2ca8482f16", size = 72328, upload-time = "2025-10-04T14:42:08.656Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5d/7e384f4115a7bc113162f7b6eb5d561031e303f840f304b68e3f1b0541a1/annotatedyaml-1.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8698bbbd1d38f8c9ba95a107d7597f5af3f2ba295d1d14227f85b62377998ffc", size = 104776, upload-time = "2025-10-04T14:42:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/77bebdd30118c1e85f11d5a83a3bb5955409bba74d81cfb0f7b551273513/annotatedyaml-1.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cbc661dbc7c5f7ddf69fbf879da6a96745b8cd39ae1338dab3a0aa8eb208367", size = 107716, upload-time = "2025-10-04T14:42:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/bfc798abb154e398d5210304ba3beff9ad9c7b6ec4574ffb705493b8e2d5/annotatedyaml-1.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db1c3ca021bbd390354037ede5c255657afb2a7544b7cfa0e091b62b888aa462", size = 130361, upload-time = "2025-10-04T14:42:15.494Z" }, +] + [[package]] name = "anyio" version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sniffio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } wheels = [ @@ -209,6 +326,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] +[[package]] +name = "astral" +version = "2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/c3/76dfe55a68c48a1a6f3d2eeab2793ebffa9db8adfba82774a7e0f5f43980/astral-2.2.tar.gz", hash = "sha256:e41d9967d5c48be421346552f0f4dedad43ff39a83574f5ff2ad32b6627b6fbe", size = 578223, upload-time = "2020-05-20T14:23:17.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/60/7cc241b9c3710ebadddcb323e77dd422c693183aec92449a1cf1fb59e1ba/astral-2.2-py2.py3-none-any.whl", hash = "sha256:b9ef70faf32e81a8ba174d21e8f29dc0b53b409ef035f27e0749ddc13cb5982a", size = 30775, upload-time = "2020-05-20T14:23:14.866Z" }, +] + [[package]] name = "asttokens" version = "3.0.1" @@ -218,6 +347,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] +[[package]] +name = "async-interrupt" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/79/732a581e3ceb09f938d33ad8ab3419856181d95bb621aa2441a10f281e10/async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7", size = 8484, upload-time = "2025-02-22T17:15:04.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/77/060b972fa7819fa9eea9a70acf8c7c0c58341a1e300ee5ccb063e757a4a7/async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c", size = 8907, upload-time = "2025-02-22T17:15:01.971Z" }, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -231,9 +369,6 @@ wheels = [ name = "asyncpg" version = "0.31.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/d9/507c80bdac2e95e5a525644af94b03fa7f9a44596a84bd48a6e80f854f92/asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61", size = 644865, upload-time = "2025-11-24T23:25:23.527Z" }, @@ -242,48 +377,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/98/1a853f6870ac7ad48383a948c8ff3c85dc278066a4d69fc9af7d3d4b1106/asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1", size = 2867087, upload-time = "2025-11-24T23:25:28.399Z" }, { url = "https://files.pythonhosted.org/packages/11/29/7e76f2a51f2360a7c90d2cf6d0d9b210c8bb0ae342edebd16173611a55c2/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3", size = 2747631, upload-time = "2025-11-24T23:25:30.154Z" }, { url = "https://files.pythonhosted.org/packages/5d/3f/716e10cb57c4f388248db46555e9226901688fbfabd0afb85b5e1d65d5a7/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8", size = 2855107, upload-time = "2025-11-24T23:25:31.888Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ec/3ebae9dfb23a1bd3f68acfd4f795983b65b413291c0e2b0d982d6ae6c920/asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095", size = 521990, upload-time = "2025-11-24T23:25:33.402Z" }, - { url = "https://files.pythonhosted.org/packages/20/b4/9fbb4b0af4e36d96a61d026dd37acab3cf521a70290a09640b215da5ab7c/asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540", size = 581629, upload-time = "2025-11-24T23:25:34.846Z" }, { url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" }, { url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" }, { url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" }, { url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" }, { url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" }, { url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" }, - { url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" }, - { url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, - { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, - { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, - { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, - { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, - { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, - { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] [[package]] @@ -291,14 +414,23 @@ name = "asyncpg-stubs" version = "0.31.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asyncpg" }, - { name = "typing-extensions" }, + { name = "asyncpg", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/e1/a51adefd76533eeff03d442bb4acbc96c2e27e04c85ce4be410b2ea92f33/asyncpg_stubs-0.31.1.tar.gz", hash = "sha256:6d7342417f867365c98b67d5ae40cb57ce6b2a9eb921fff39d9296961fca18be", size = 20591, upload-time = "2025-12-10T16:59:00.99Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/ab/2301aace8c32be52832f3af75aadfd3c8516b8e7764ba8fa82c6008a99aa/asyncpg_stubs-0.31.1-py3-none-any.whl", hash = "sha256:96c0cf3786948f313207b990d26bf3430daf385ca2913ba65d9dd0ede6bf8bf4", size = 27651, upload-time = "2025-12-10T16:58:59.896Z" }, ] +[[package]] +name = "atomicwrites-homeassistant" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/5a/10ff0fd9aa04f78a0b31bb617c8d29796a12bea33f1e48aa54687d635e44/atomicwrites-homeassistant-1.4.1.tar.gz", hash = "sha256:256a672106f16745445228d966240b77b55f46a096d20305901a57aa5d1f4c2f", size = 12223, upload-time = "2022-07-08T20:56:46.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/1b/872dd3b11939edb4c0a27d2569a9b7e77d3b88995a45a331f376e13528c0/atomicwrites_homeassistant-1.4.1-py2.py3-none-any.whl", hash = "sha256:01457de800961db7d5b575f3c92e7fb56e435d88512c366afb0873f4f092bb0d", size = 7128, upload-time = "2022-07-08T20:56:44.186Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -309,12 +441,270 @@ wheels = [ ] [[package]] -name = "backports-asyncio-runner" -version = "1.2.0" +name = "audioop-lts" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/3b/69ff8a885e4c1c42014c2765275c4bd91fe7bc9847e9d8543dbcbb09f820/audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387", size = 30204, upload-time = "2024-08-04T21:14:43.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/91/a219253cc6e92db2ebeaf5cf8197f71d995df6f6b16091d1f3ce62cb169d/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a", size = 46252, upload-time = "2024-08-04T21:13:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f6/3cb21e0accd9e112d27cee3b1477cd04dafe88675c54ad8b0d56226c1e0b/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e", size = 27183, upload-time = "2024-08-04T21:13:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7e/f94c8a6a8b2571694375b4cf94d3e5e0f529e8e6ba280fad4d8c70621f27/audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6", size = 26726, upload-time = "2024-08-04T21:14:00.846Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f8/a0e8e7a033b03fae2b16bc5aa48100b461c4f3a8a38af56d5ad579924a3a/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe", size = 80718, upload-time = "2024-08-04T21:14:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ea/a98ebd4ed631c93b8b8f2368862cd8084d75c77a697248c24437c36a6f7e/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a", size = 88326, upload-time = "2024-08-04T21:14:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/e97a9f9daac0982aa92db1199339bd393594d9a4196ad95ae088635a105f/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300", size = 80539, upload-time = "2024-08-04T21:14:04.679Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d3/1051d80e6f2d6f4773f90c07e73743a1e19fcd31af58ff4e8ef0375d3a80/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059", size = 78577, upload-time = "2024-08-04T21:14:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/54f4c58bae8dc8c64a75071c7e98e105ddaca35449376fcb0180f6e3c9df/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e", size = 82074, upload-time = "2024-08-04T21:14:09.99Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/2e78daa7cebbea57e72c0e1927413be4db675548a537cfba6a19040d52fa/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48", size = 84210, upload-time = "2024-08-04T21:14:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/3ff8a74df2ec2fa6d2ae06ac86e4a27d6412dbb7d0e0d41024222744c7e0/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281", size = 85664, upload-time = "2024-08-04T21:14:12.394Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/21cc4e5878f6edbc8e54be4c108d7cb9cb6202313cfe98e4ece6064580dd/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959", size = 93255, upload-time = "2024-08-04T21:14:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/7f7418c362a899ac3b0bf13b1fde2d4ffccfdeb6a859abd26f2d142a1d58/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47", size = 87760, upload-time = "2024-08-04T21:14:14.74Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/577a8be87dc7dd2ba568895045cee7d32e81d85a7e44a29000fe02c4d9d4/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77", size = 84992, upload-time = "2024-08-04T21:14:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/99/bb664a99561fd4266687e5cb8965e6ec31ba4ff7002c3fce3dc5ef2709db/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455", size = 46827, upload-time = "2024-08-04T21:14:23.034Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/f664171e867e0768ab982715e744430cf323f1282eb2e11ebfb6ee4c4551/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f", size = 27479, upload-time = "2024-08-04T21:14:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0d/2a79231ff54eb20e83b47e7610462ad6a2bea4e113fae5aa91c6547e7764/audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf", size = 27056, upload-time = "2024-08-04T21:14:28.061Z" }, + { url = "https://files.pythonhosted.org/packages/86/46/342471398283bb0634f5a6df947806a423ba74b2e29e250c7ec0e3720e4f/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab", size = 87802, upload-time = "2024-08-04T21:14:29.586Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/7a85b08d4ed55517634ff19ddfbd0af05bf8bfd39a204e4445cd0e6f0cc9/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6", size = 95016, upload-time = "2024-08-04T21:14:30.481Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2a/45edbca97ea9ee9e6bbbdb8d25613a36e16a4d1e14ae01557392f15cc8d3/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543", size = 87394, upload-time = "2024-08-04T21:14:31.883Z" }, + { url = "https://files.pythonhosted.org/packages/14/ae/832bcbbef2c510629593bf46739374174606e25ac7d106b08d396b74c964/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074", size = 84874, upload-time = "2024-08-04T21:14:32.751Z" }, + { url = "https://files.pythonhosted.org/packages/26/1c/8023c3490798ed2f90dfe58ec3b26d7520a243ae9c0fc751ed3c9d8dbb69/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78", size = 88698, upload-time = "2024-08-04T21:14:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/5379d953d4918278b1f04a5a64b2c112bd7aae8f81021009da0dcb77173c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb", size = 90401, upload-time = "2024-08-04T21:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/99/6e/3c45d316705ab1aec2e69543a5b5e458d0d112a93d08994347fafef03d50/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2", size = 91864, upload-time = "2024-08-04T21:14:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/08/58/6a371d8fed4f34debdb532c0b00942a84ebf3e7ad368e5edc26931d0e251/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a", size = 98796, upload-time = "2024-08-04T21:14:37.185Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/d637aa35497e0034ff846fd3330d1db26bc6fd9dd79c406e1341188b06a2/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a", size = 94116, upload-time = "2024-08-04T21:14:38.145Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/7afc2abf46bbcf525a6ebc0305d85ab08dc2d1e2da72c48dbb35eee5b62c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63", size = 91520, upload-time = "2024-08-04T21:14:39.128Z" }, +] + +[[package]] +name = "awesomeversion" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/3a/c97ef69b8209aa9d7209b143345fe49c1e20126f62a775038ab6dcd78fd5/awesomeversion-25.8.0.tar.gz", hash = "sha256:e6cd08c90292a11f30b8de401863dcde7bc66a671d8173f9066ebd15d9310453", size = 70873, upload-time = "2025-08-03T08:54:07.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/b3/c6be343010721bfdd3058b708eb4868fa1a207534a3b6c80de74d35fb568/awesomeversion-25.8.0-py3-none-any.whl", hash = "sha256:1c314683abfcc3e26c62af9e609b585bbcbf2ec19568df2f60ff1034fb1dae28", size = 15919, upload-time = "2025-08-03T08:54:06.265Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, +] + +[[package]] +name = "bleak" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dbus-fast", marker = "python_full_version >= '3.13.2' and sys_platform == 'linux'" }, + { name = "pyobjc-core", marker = "python_full_version >= '3.13.2' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corebluetooth", marker = "python_full_version >= '3.13.2' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libdispatch", marker = "python_full_version >= '3.13.2' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/8a/5acbd4da6a5a301fab56ff6d6e9e6b6945e6e4a2d1d213898c21b1d3a19b/bleak-2.1.1.tar.gz", hash = "sha256:4600cc5852f2392ce886547e127623f188e689489c5946d422172adf80635cf9", size = 120634, upload-time = "2025-12-31T20:43:28.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fe/22aec895f040c1e457d6e6fcc79286fbb17d54602600ab2a58837bec7be1/bleak-2.1.1-py3-none-any.whl", hash = "sha256:61ac1925073b580c896a92a8c404088c5e5ec9dc3c5bd6fc17554a15779d83de", size = 141258, upload-time = "2025-12-31T20:43:27.302Z" }, +] + +[[package]] +name = "bleak-retry-connector" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleak", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bluetooth-adapters", marker = "python_full_version >= '3.13.2' and sys_platform == 'linux'" }, + { name = "dbus-fast", marker = "python_full_version >= '3.13.2' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/e1/28c82e31f4f1c555c029598225b8d895bb3ccdc6c99b0974a1322f9725a9/bleak_retry_connector-4.5.0.tar.gz", hash = "sha256:5db81f8510c63cbea7b85d94bfa2b0fd9a24f0704474a49727a634488623fa17", size = 18648, upload-time = "2026-01-07T18:34:24.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/52/24ef8647f2e0dab293ec68fc58c4f0ad4e00652b631d83835cf32c06d101/bleak_retry_connector-4.5.0-py3-none-any.whl", hash = "sha256:ea63420e8f20117ef04202ea13d5215bffb736720cf865ce9a5aa556ea677afe", size = 18638, upload-time = "2026-01-07T18:34:23.119Z" }, +] + +[[package]] +name = "bluetooth-adapters" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiooui", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bleak", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "dbus-fast", marker = "python_full_version >= '3.13.2' and sys_platform == 'linux'" }, + { name = "uart-devices", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "usb-devices", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/b37a52f5243cf8bd30cc7e25c1128750d129db15a7b2c7ef107ddb7429f9/bluetooth_adapters-2.1.1.tar.gz", hash = "sha256:f289e0f08814f74252a28862f488283680584744430d7eac45820f9c20ba041a", size = 17234, upload-time = "2025-09-12T17:18:48.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/11/8f344d5379df2d31eea73052128136702630f28b5fb55a8d30250c8112e2/bluetooth_adapters-2.1.1-py3-none-any.whl", hash = "sha256:1f93026e530dcb2f4515a92955fa6f85934f928b009a181ee57edc8b4affd25c", size = 20276, upload-time = "2025-09-12T17:18:47.763Z" }, +] + +[[package]] +name = "bluetooth-auto-recovery" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bluetooth-adapters", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "btsocket", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyric", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "usb-devices", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/8b/1d6f338ced9b47965382c8f1325bf3d22be65e6f838b4e465227c52d333c/bluetooth_auto_recovery-1.5.3.tar.gz", hash = "sha256:0b36aa6be84474fff81c1ce328f016a6553272ac47050b1fa60f03e36a8db46d", size = 12798, upload-time = "2025-09-13T17:17:09.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/ab/518f14a3c3e43c34c638485cd29bfa80bd35da5a151a434f7ac3c86e1e83/bluetooth_auto_recovery-1.5.3-py3-none-any.whl", hash = "sha256:5d66b859a54ef20fdf1bd3cf6762f153e86651babe716836770da9d9c47b01c4", size = 11750, upload-time = "2025-09-13T17:17:07.681Z" }, +] + +[[package]] +name = "bluetooth-data-tools" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/90/46dfa84798ca4e5c2f66d9a756bb207ed21d89a32b8ef8d3ea89e079455f/bluetooth_data_tools-1.28.4.tar.gz", hash = "sha256:0617a879c30e0410c3506e263ee9e9bd51b06d64db13b4ad0bfd765f794b756f", size = 16488, upload-time = "2025-10-28T15:23:05.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/c2/a79621d871ef0f4801267b408c0afc8073b70618afde250d1ac3c1e1f69c/bluetooth_data_tools-1.28.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97a82d04306a827fa9d94a51aa6bfee0cc2a0ca8977150d7184f717410a0ee26", size = 115550, upload-time = "2025-10-28T15:35:33.524Z" }, + { url = "https://files.pythonhosted.org/packages/72/ce/0a20794616169494ab92827d751e9c4acb3f5317b0b47548faa415cee0d4/bluetooth_data_tools-1.28.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f01c248081b4e19aa898f5719ee5603b9d1c636d3ba7d40422fe6f43234b0464", size = 116633, upload-time = "2025-10-28T15:35:34.923Z" }, + { url = "https://files.pythonhosted.org/packages/52/e2/2418b496b6e7a1a9d7921ce48ea15b314b416adede7cdacdc1c9b15e862b/bluetooth_data_tools-1.28.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9eef1bb34b8e80b8aa33dfc088fc2cc242e3d7157e271ff4d70453616215602", size = 142637, upload-time = "2025-10-28T15:35:36.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/53/36e9aed670fa0de4f7de559b063ca2ccdb685060cf3e8e3aae85d56c96f4/bluetooth_data_tools-1.28.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5a9b12d3bed1481579d850414584cf5ac2384355004a85bc0a9da2d013878dd0", size = 129004, upload-time = "2025-10-28T15:35:37.361Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/a12c569c92e1901b5db796aa134f040b9c9e1cde218c26034f3fc1068c3e/bluetooth_data_tools-1.28.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2acced8c530f9e39d0c2d76919a5de5b340a1685bc26b7a76107f707ff3f33ff", size = 144818, upload-time = "2025-10-28T15:35:38.476Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/0fa4aecac3f422828c756698c4fa98388a78fe39a8e947082df6c3ae3b4c/bluetooth_data_tools-1.28.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d3e76881a14d34d1e1aa3b07b78e07e52625da5c2944dfc609fd5f6d9d6f8146", size = 142731, upload-time = "2025-10-28T15:35:39.603Z" }, + { url = "https://files.pythonhosted.org/packages/70/33/5fef78dd53f6f7f64bbf805a7899882db9b88a870c5dcee71377ddc90195/bluetooth_data_tools-1.28.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:db13e956cccf5da0a2326bb8e84a3b15be3a5497f6d3ce52b31b13fe27452440", size = 132970, upload-time = "2025-10-28T15:35:41Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/ee55278bdfbfd5f1e4d04cf67f95566095eb252ce2a7837a23fa35767d54/bluetooth_data_tools-1.28.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4695ffe677ff7d217952c8a7ebce2050ed61ce3d24775f4b9d30fa8198960857", size = 146438, upload-time = "2025-10-28T15:35:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/fd5c5df8ff5c72b0ea4779cdd6b8843eb1353846fffccf4f8d7865b1dfb5/bluetooth_data_tools-1.28.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38835f52cbe1a4a2d4639ae0f8fcd3b727b0e9ae19ffb9641b7ea0e91d628e56", size = 385618, upload-time = "2025-10-28T15:35:45.544Z" }, + { url = "https://files.pythonhosted.org/packages/c4/20/f05d11cee39a85ba46a62fee317ad35257dfd18c27b73bc33f27905b6745/bluetooth_data_tools-1.28.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1920020169e2b5c94f432d016b0ba88aae4c1dd76492042eb9069bad7a4cd11e", size = 386585, upload-time = "2025-10-28T15:35:47.079Z" }, + { url = "https://files.pythonhosted.org/packages/4e/10/363c7588a943216befcef37026334c0e8a98d52ac9d53a44694065033978/bluetooth_data_tools-1.28.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:370a81e12fd7a86bd2a54527afe60c27a483fd3c72bea403b15550ae834a3f82", size = 413231, upload-time = "2025-10-28T15:35:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/8f9f26d977c7a02a2a31b51a6689086644e6ae01bbdc379e03ab3704cd86/bluetooth_data_tools-1.28.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ccb0cc3c9458d51ad55b2f45e682429136e9242241378737bbbbf43a400e3e55", size = 129850, upload-time = "2025-10-28T15:35:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/9a7cc7c740070998c45b23d7be80e9ef570f0b0489dfa506e85729b738a2/bluetooth_data_tools-1.28.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc0b4474c2b41a6762660d4206464ec66a0884ed06103f248a9b85243d70b0ce", size = 414999, upload-time = "2025-10-28T15:35:50.453Z" }, + { url = "https://files.pythonhosted.org/packages/40/a2/b32af336d18f4f162267ed70f08e58d5abb4d35038907bfb7fd301478d53/bluetooth_data_tools-1.28.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6abd6d1896e94def35d3d40a79934e9a8b0fb892129b1446d1c7d1fec8b81b3a", size = 413526, upload-time = "2025-10-28T15:35:52.065Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b2/21390825d1b41a7d837a3fb0fc92f6324081939624252244f94ece6cc571/bluetooth_data_tools-1.28.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9c68aad6bc729972bcb03fa7d0fa49c6892660654cd3ef61d0a0872930542528", size = 133974, upload-time = "2025-10-28T15:35:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/70/ce/2fc1c50fc2636c6d8f2592277b17d49cf5a91c2fffb289e739dc42f0ac3a/bluetooth_data_tools-1.28.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bb97f120775e328fcdf9d70e80766340d5882c94d5c4332870e0eb73ed139d31", size = 417205, upload-time = "2025-10-28T15:35:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/93/ed/9bb3a560ff073b5ea6f1fe9ef66d4af0fb8071cd3f569ba58769217114bb/bluetooth_data_tools-1.28.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e3184f43c52ed1e39f9ad412c586c84b4e0841f052608e6ed7ef81daf656fb64", size = 385017, upload-time = "2025-10-28T15:35:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8237718607f1daeec1f4aefb3dfdd7b7b56bc1422fdc4e5f9ef991e3a9b2/bluetooth_data_tools-1.28.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c5e524df9afae40142c3a3dcf128983df99e73158a2bc98f1709024ff185a22", size = 386609, upload-time = "2025-10-28T15:35:59.73Z" }, + { url = "https://files.pythonhosted.org/packages/35/8e/e6136f790c68261610160c0b8dfadd874d38bff8e3da0feb4bf1428b89fd/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bf7eb8b41995466af3401db3387726afda42487b291b94ab90e7d26aadb72ac", size = 414363, upload-time = "2025-10-28T15:36:00.975Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/af729a472e5e9274480b34c8218fda915b3d7add9247d766dee79143eebf/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b3a1e9838f147d6e80b5d9cf7e33c9e736f1f1bda9db00b4ea5ed45fd57d2e8", size = 132276, upload-time = "2025-10-28T15:36:02.622Z" }, + { url = "https://files.pythonhosted.org/packages/70/eb/111c66cc73fd4ec29071641ba6f8b68db033f3d6b9611aa332565c0e3286/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8688f54fd344f17f0c04bca6c2b4351c9fcb211d16becada60f5656305c04238", size = 414494, upload-time = "2025-10-28T15:36:03.709Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/0cb024304121380d374c62cf119647b77c88be3bea435291a71d98e956d4/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8f4233a9d8983ede1d4b319783266b5ae89dbd0f8ac48dcc9b0c2a1d6a60a0ca", size = 414311, upload-time = "2025-10-28T15:36:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/00/e11af70293608c36507f14bf893f20b072d753ab1c929dc8103209cf9555/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:df2948eae3bd32242322d7f1a7f0d74e2d2f79e5e3254c7e06ec2ddbcacabe7b", size = 135039, upload-time = "2025-10-28T15:36:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/a4/24/47344c86c8abef13a7b39240c3ee5789e0c5fde16ea65c97f3a9a669c121/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e98f7bcd491711f5be161a0400721c9ecb782308f0eeb030f3bd450450f53d0", size = 416584, upload-time = "2025-10-28T15:36:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/83/cd/fa868c3bed326976813c04bd89e833cb0032a6a18ffc03f843947caa29d3/bluetooth_data_tools-1.28.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ade5a22f394cee6b428474f5c23f8ce086ebc618b30fa478fc53703b5dc1bf09", size = 383602, upload-time = "2025-10-28T15:36:11.963Z" }, + { url = "https://files.pythonhosted.org/packages/d3/37/ef120dcce334ba8e3d97c06c9d46ab1db3b7474fad1fb867097b7c0a9355/bluetooth_data_tools-1.28.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea8569f42699e94e18a1be32e45c737f2795c7509f09fa27dd5d342a7855473c", size = 385073, upload-time = "2025-10-28T15:36:13.522Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/1ce2f4d2ce6a04a6a1be490cd2b975777fb76f5230818cefe24b7ed7ba9d/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dfaecb4269bc4830a7bd6f823e8a0a4c368d9135ee6805e6db5eecf1211a2e4", size = 412028, upload-time = "2025-10-28T15:36:14.709Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/f525cc4d4da3555f820a6ce79a3877424ba73f69f4d44a4389b19f7aaf15/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ff3d43804f3510bd11a267268c567b7fe5653b10243be48527ac01d8e15b3faa", size = 130572, upload-time = "2025-10-28T15:36:16.184Z" }, + { url = "https://files.pythonhosted.org/packages/6d/01/2c4b89de730e71c94f3552948aa8adf0a0b5a4dc21e642805bc8e014f41d/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e99be62bdcd2b94778eb230c6d73f4da4ad1493ccc33c09efc8432c5a242c071", size = 412805, upload-time = "2025-10-28T15:36:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/6ca04f0225b51ef27e79f369e9b9fff4bf104025a4e51d6fb2d943c38645/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:f85fbc0c540c3e64b5fc925f6b60d8c96d521548c7bfa3b1e8998ea4e5a59054", size = 140133, upload-time = "2025-10-28T15:23:03.49Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3e/675f9037c7b23df43229d95e8627fe10759f8c3c4a1ef6919b8d1683d4df/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c9dd29f39bddbcfa1dcfca13dcfd2a1111d5a0fbba708a8c98feb98bca10b7a", size = 411910, upload-time = "2025-10-28T15:36:19.207Z" }, + { url = "https://files.pythonhosted.org/packages/a1/12/4f2086f879c0595e065e62dd1bfbe8d371336308654e466ca10b6cf61d86/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8bbcd287a1d5b249639fc1ba99c7ab8f0d7257d43104cf349fab2c747b84b3cd", size = 132814, upload-time = "2025-10-28T15:36:20.789Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/56a3b3a15cd6c601c3c22cf8c58db788ea59e669db1af123a4113983302b/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7decde5838ccccf71ec626c3f0421a6265054cb1e5ced121bb6448434a0bb72f", size = 414926, upload-time = "2025-10-28T15:36:21.897Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba322c36376532f3133c7d56bc80dd2859df9c78aa52de19ed7627b9fb/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:81c6c2b7c844d30a0fd1527e38e47cdb0f350c0297fb11516bfa255b37241fbf", size = 383677, upload-time = "2025-10-28T15:36:33.905Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2e/74e7b4857ba10a524cd00177fbd78764c50810fb523020b7d5cbf0fdbac8/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:99896987f48d762694cdea7a8a7091031cdf40dc65e8e934a7422746264865ba", size = 385890, upload-time = "2025-10-28T15:36:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/35bc257ed1935e55ac7bfb56172a290f094f8b982f65f68aadb0f03ceab5/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebac9d60786bd7c403f472fcda871cb74d0aef0d4e713715af2e5e095d15a625", size = 412966, upload-time = "2025-10-28T15:36:36.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/2ed3dff30e85029e631a211d93e11aab7dc4a899d9c96a15eca18541e66e/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:06a2750e49fed2310ddd7b51388b891cbd4457ee7392f3a17c387591cbb74ace", size = 129887, upload-time = "2025-10-28T15:36:38.429Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/824f3b34b0fab4e57efd457ea8b9bdf41d279a44eb19cfde5ede159d90b3/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5dccfe237237463c3d74fa425aaf8a9d78b26a5177e6777b10039699313a335", size = 412909, upload-time = "2025-10-28T15:36:39.552Z" }, + { url = "https://files.pythonhosted.org/packages/eb/88/f2217b88c32b470e5f9dc9fbce38f24b9548c0776be7c5e0db1249c42ae9/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4a071d7af2614af9a00f65063adaacda94f4357cc2dfedda7057c005f437dacd", size = 413005, upload-time = "2025-10-28T15:36:41.572Z" }, + { url = "https://files.pythonhosted.org/packages/6d/da/cde7557972e50cbb8a92291cc34e5de07f0e2bbc28a388151e738e9efe84/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:bd84c4f2d24103ff43044ccd3cf8c0e05ee285bd6f9eddc9772b2069cfb6c271", size = 131426, upload-time = "2025-10-28T15:36:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/925fd28e2695ba810b1f7f02f2d5ab8635a11d6e415ac4039446145f9e48/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e3895dbbdad2a39de5a7b36a4ddb5e2f8ad38029628e3eddfde31a5c56d81b5", size = 414955, upload-time = "2025-10-28T15:36:43.775Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/f2ce46cf82b32d6a62171753a2d6550d633af5b27f0ad2c2ff5fef1980a4/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a44c48bf163606a2915d12ffb3ac1b022548e566c062907f98266e8a19c6173c", size = 488264, upload-time = "2025-10-28T15:36:47.582Z" }, + { url = "https://files.pythonhosted.org/packages/ba/32/c3bbee5b7c66190f0729e71fefe44adb49e7bb94407b110d972d817561a2/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b76a6c8c6d610844c8712cecf207c16373cad3361fb29e6dbcdcb12f2700bcb9", size = 492846, upload-time = "2025-10-28T15:36:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/71/5c/751028e7fab907c0c2fc7749f088d19bf2b938e5cdd7d0e68ddbcacb7b79/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61b827616075ecee12c374b04b14d81575403849435bf915c9a3812138f046b7", size = 548041, upload-time = "2025-10-28T15:36:50.066Z" }, + { url = "https://files.pythonhosted.org/packages/77/02/4d8f4a9cb2a2beaaedda71fb3017f6bb5eb3de08656adfb9a8a773ec7912/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:525646baaf5f741ea071aa4babd8313e4e9bae75b46757c4b0f6aeadfa71b52a", size = 517778, upload-time = "2025-10-28T15:36:51.628Z" }, + { url = "https://files.pythonhosted.org/packages/89/9b/90d65fed47b531b0f0f4c8be012d35c97950c97fb7b74501bfe938c7f7ca/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c06b66ef406c68a95052a87640fa34d402d31120a8b0b62f99080169621697a", size = 546643, upload-time = "2025-10-28T15:36:52.971Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6b/c15363ccfc208a34cd6d627610350c72633e2a6764d37d04a1340fb13844/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:152232c157f2f6d8265c0141e56423bbedd9e84044fb815e69d786a73fb195c7", size = 548872, upload-time = "2025-10-28T15:36:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/b649eeea14e6330da34f42dc1407424cd929af3ae1298b5651459d0c4bb8/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:243163028565955e73f19c0c462b619fd0f56e31875c30f5f3af2a48b43adb67", size = 524783, upload-time = "2025-10-28T15:36:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6e/96c762f8a49f65348748d72c515c5a79c9179c685d3e02694c380bdafa72/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0a1608bca00e24b6ca3b98ed7d797a03988a44285d74286e045446c8161a62ea", size = 551318, upload-time = "2025-10-28T15:36:57.062Z" }, +] + +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jmespath", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "s3transfer", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/b0/f2a371146877b4d60cd2a6f51c440503cbf77a73c85347ad282eebe90269/boto3-1.42.41.tar.gz", hash = "sha256:86dd2bc577e33da5cd9f10e21b22cdda01a24f83f31ca1bb5ac1f5f8b8e9e67e", size = 112806, upload-time = "2026-02-03T20:38:58.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/e8/e671b9aed3e8f6bb5f85aed7fd9dd4e79074798b2a14391fab6b6da04ff7/boto3-1.42.41-py3-none-any.whl", hash = "sha256:32470b1d32208e03b47cf6ce4a7adb337b8a1730aaefb97c336cfd4e2be2577f", size = 140602, upload-time = "2026-02-03T20:38:56.352Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "urllib3", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/1d/5fd43319250a5a484bcbde334da2e2297956cdae40de8eb0808759538a7c/botocore-1.42.41.tar.gz", hash = "sha256:0698967741d873d819134bea1ffe9c35decc00c741f49a42885dbd7ad8198fbc", size = 14923189, upload-time = "2026-02-03T20:38:47.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/84/df765c7942f52eab12540ef1880b9489d45b364b777fad522fa84435ed11/botocore-1.42.41-py3-none-any.whl", hash = "sha256:567530f7d4da668af891c1259fb80c01578ab8d9f1098127cc250451a46fb54f", size = 14597792, upload-time = "2026-02-03T20:38:45.058Z" }, +] + +[[package]] +name = "btsocket" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/b1/0ae262ecf936f5d2472ff7387087ca674e3b88d8c76b3e0e55fbc0c6e956/btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d", size = 19563, upload-time = "2024-06-10T07:05:27.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, + { url = "https://files.pythonhosted.org/packages/67/2b/9bf3481131a24cb29350d69469448349362f6102bed9ae4a0a5bb228d731/btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c", size = 14807, upload-time = "2024-06-10T07:05:26.381Z" }, ] [[package]] @@ -331,7 +721,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "python_full_version >= '3.13.2' and implementation_name != 'PyPy' and sys_platform != 'win32'" }, ] 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 = [ @@ -345,8 +735,6 @@ wheels = [ { 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/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, @@ -357,9 +745,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -369,9 +754,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, @@ -381,9 +763,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, @@ -392,9 +771,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, @@ -403,9 +779,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] @@ -427,9 +800,6 @@ wheels = [ { 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" }, @@ -443,9 +813,6 @@ wheels = [ { 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" }, @@ -459,9 +826,6 @@ wheels = [ { 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" }, @@ -475,9 +839,6 @@ wheels = [ { 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" }, @@ -491,33 +852,65 @@ wheels = [ { 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/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 = "ciso8601" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/8a/075724aea06c98626109bfd670c27c248c87b9ba33e637f069bf46e8c4c3/ciso8601-2.3.3.tar.gz", hash = "sha256:db5d78d9fb0de8686fbad1c1c2d168ed52efb6e8bf8774ae26226e5034a46dae", size = 31909, upload-time = "2025-08-20T16:31:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c1/ebdb2614bb7a7a8ea7b496709bdec4cd0842ef38cde44203f4986df2d8f9/ciso8601-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf67a1d47a52dad19aaffb136de63263910dcab6e50d428f27416733ce81f183", size = 16077, upload-time = "2025-08-20T16:30:18.097Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bb/0d100a3774c8d15b432f693e8897891c3af4536a36b0c8ed7a527f319c8f/ciso8601-2.3.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:67316d2a2d278fad3d569771b032e9bd8484c8aab842e1a2524f6433260cf9ac", size = 24112, upload-time = "2025-08-20T16:30:19.261Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/52af79a935073c4f2a31a3e73ab531dd5f41e8544eafd84ef5cc14b0c198/ciso8601-2.3.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:48e0ac5d411d186865fdf0d30529fb7ae6df7c8d622540d5274b453f0e7b935a", size = 15868, upload-time = "2025-08-20T16:30:20.436Z" }, + { url = "https://files.pythonhosted.org/packages/36/b0/6a9f59dc68dab198df18fcb47999d9d18b67765706f7d9292814def99dac/ciso8601-2.3.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9063aa362b291a72d395980e1b6479366061ec77d98ae7375aa5891abe0c6b9d", size = 40017, upload-time = "2025-08-20T16:30:21.441Z" }, + { url = "https://files.pythonhosted.org/packages/31/1f/662b51464c2873ba345db671048e441267437e1ce802f079e024e9305b5b/ciso8601-2.3.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe7b832298a70ac39ef0b3cd1ce860289a2b45d2fdca2c2acd26551e29273487", size = 40519, upload-time = "2025-08-20T16:30:22.369Z" }, + { url = "https://files.pythonhosted.org/packages/14/ec/8f9ebbc8e3330d3c2374983cfe7553592d53cdeb59a35078ce135c81d83d/ciso8601-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0e81268f84f6ed5a8f07026abed8ffa4fa54953e5763802b259e170f7bd7fb0", size = 39986, upload-time = "2025-08-20T16:30:23.582Z" }, + { url = "https://files.pythonhosted.org/packages/24/c4/cff2f87395514ae70938b71ce4ceba975e71b000fd507ad000a8cd917a0b/ciso8601-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44fdb272acdc59e94282f6155eacbff8cd9687a2a84df0bbbed2b1bd53fa8406", size = 40236, upload-time = "2025-08-20T16:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/30/5744492f9e7dbe60a3c92968cdb8987566f5389b8d0e5c60f6d633da45fe/ciso8601-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f068fb60b801640b4d729a3cf79f5b3075c071f0dad3a08e5bf68b89ca41aef7", size = 16076, upload-time = "2025-08-20T16:30:27.005Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c6/ce97f28a3b936a9a6c0abba9905382cb89022b8e1abb37a2150c1caf71d6/ciso8601-2.3.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:2f347401756cdd552420a4596a0535a4f8193298ff401e41fb31603e182ae302", size = 24110, upload-time = "2025-08-20T16:30:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/1af026c7959d39bdbaa6400b76ffb54437fa52698b801d51ddaa14063f0e/ciso8601-2.3.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:77e8e691ade14dd0e2ae1bcdd98475c25cd76be34b1cf43d9138bbb7ea7a8a37", size = 15871, upload-time = "2025-08-20T16:30:30.059Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1a/9ae630bf75a51755bf701660a65207b8efa2f95590408832b38e58834d57/ciso8601-2.3.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a5839ea7d2edf22e0199587e2ea71bc082b0e7ffce90389c7bdd407c05dbf230", size = 40380, upload-time = "2025-08-20T16:30:31.211Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3c/8671bde2bbf6abb8ceee82db0bc6bcd08066e7104680e3866eda6047adc1/ciso8601-2.3.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de0476ced02b965ef82c20191757f26e14878c76ce8d32a94c1e9ee14658ec6e", size = 40914, upload-time = "2025-08-20T16:30:32.096Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/433f91f19ff553653f340e77dbb12afe46de8a84a407ae01483d22ea8f7a/ciso8601-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe9303131af07e3596583e9d7faebb755d44c52c16f8077beeea1b297541fb61", size = 40154, upload-time = "2025-08-20T16:30:33.325Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/39b905b09f77f2140724707919edea2a3d34b00a9366cd7ad541aefb464e/ciso8601-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c443761b899e4e350a647b3439f8e999d6c925dc4e83887b3063b13c2a9b195", size = 40428, upload-time = "2025-08-20T16:30:34.626Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/b723a6981cfc42bbe992da23179f5dd1556e9054067985108ec6cbe34dd3/ciso8601-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e7ef14610446211c4102bf6c67f32619ab341e56db15bad6884385b43c12b064", size = 16111, upload-time = "2025-08-20T16:30:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e9/e547ec4dd75f28d8d217488130fa07767bc42fd643d61a18870487133c0e/ciso8601-2.3.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:523901aec6b0ccdf255c863ef161f476197f177c5cd33f2fbb35955c5f97fdb4", size = 24193, upload-time = "2025-08-20T16:30:38.067Z" }, + { url = "https://files.pythonhosted.org/packages/14/c8/801b78e30667cb31b4524e9dc26cbc2c03c012f9aa3f5ae21676461dc622/ciso8601-2.3.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:45f8254d1fb0a41e20f98e93075db7b56504adddf65e4c8b397671feba4861ca", size = 15917, upload-time = "2025-08-20T16:30:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/6b/dfc56a2a4e572a2a3f8c88a66dea6a9186a8e10da7c36cc84abc31bf795c/ciso8601-2.3.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:202ca99077577683e6a84d394ff2677ec19d9f406fbf35734f68be85d2bcd3f1", size = 41324, upload-time = "2025-08-20T16:30:40.321Z" }, + { url = "https://files.pythonhosted.org/packages/7c/57/cf66171cb5807fe345b03ce9e32fd91b3a8b6e5bd95710618a9a1b0f3fab/ciso8601-2.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7cec4e31c363e87221f2561e7083ce055a82de041e822e7c3775f8ce6250a7e", size = 41804, upload-time = "2025-08-20T16:30:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/15e8871d7ae2ff0f756128e246348bdede58c08edba13cd886450ceeb304/ciso8601-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:389fef3ccc3065fa21cb6ef7d03aee63ab980591b5d87b9f0bbe349f52b16bdc", size = 41209, upload-time = "2025-08-20T16:30:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/54/7563e20a158a4bdf3e8d13c63e02b71f9b73c662edc83cb4d5ab67171a7d/ciso8601-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4499cfbe4da092dea95ab81aefc78b98e2d7464518e6e80107cf2b9b1f65fa2", size = 41368, upload-time = "2025-08-20T16:30:43.397Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/88154fe8247e4dcfdbaed8c6b8ccf32b1dd4389c6c95b1986bf31649eb00/ciso8601-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8afa073802c926c3244e1e5fcc5818afd3acb90fb7826a90f91ddbda0636ea70", size = 16109, upload-time = "2025-08-20T16:30:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/be/46/8d46372b3802c7201c20c8b316569f27253aaafba0cdd2cd033985e8b77e/ciso8601-2.3.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8a04e518b4adf8e35e030feaecdb4a835d39b9bb44d207e926aea8ce3447ad7c", size = 24189, upload-time = "2025-08-20T16:30:46.958Z" }, + { url = "https://files.pythonhosted.org/packages/13/80/1890e097cb76e41995de82f29c0289ca590d7135e0be3707e5b78f54350d/ciso8601-2.3.3-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:f79ad8372463ba4265981016d1648bc05f4922bc8044c4243fcbaef7a12ee9f7", size = 15925, upload-time = "2025-08-20T16:30:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e9/690a2a6beefd9d982c20adde3f09ff54a23291a699b0df7cf0c59027d9cf/ciso8601-2.3.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5894a33f119b5ac1082df187dc58c74fe13c9c092e19ba36495c2b7cee3540b", size = 41352, upload-time = "2025-08-20T16:30:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/9a498ceb0ebd23f538e6685721c9fc4666701372c651874ed22ec46b1423/ciso8601-2.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09deebf3e326ec59d80019b4ad35175c90b99cde789c644b1496811fe3340587", size = 41866, upload-time = "2025-08-20T16:30:50.262Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0a/ee0981502aa1c9f28f7e89cf6cee08bdff2c6ed9d4289b00cceb8a1c500e/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3aa43ed59b2117baccc5bb760e5e53dad77cacba671d757c1e82e0a367b1f42a", size = 41271, upload-time = "2025-08-20T16:30:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/fb/65/24a888240324188d8350bc24fb58a6d759c0ca43adfa77210f3d60370b56/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:289515aa3a3b86a9c3450bf482f634138b98788332d136751507bfdfe46e6031", size = 41411, upload-time = "2025-08-20T16:30:52.439Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/54ad0ae2257870076b4990545a8f16221470fecea0aa7a4e1f39506db8c5/ciso8601-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82db4047d74d8b1d129e7a8da578518729912c3bd19cb71541b147e41f426381", size = 16115, upload-time = "2025-08-20T16:30:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/9fe767d44520691e2b706769466852fbdeb44a82dc294c2766bce1049d22/ciso8601-2.3.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a553f3fc03a2ed5ca6f5716de0b314fa166461df01b45d8b36043ccac3a5e79f", size = 24214, upload-time = "2025-08-20T16:30:56.359Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ac/984fd3948f372c46c436a2b48da43f4fb7bc6f156a6f4bc858adaab79d42/ciso8601-2.3.3-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:ff59c26083b7bef6df4f0d96e4b649b484806d3d7bcc2de14ad43147c3aafb04", size = 15929, upload-time = "2025-08-20T16:30:58.352Z" }, + { url = "https://files.pythonhosted.org/packages/de/3a/5572917d4e0bec2c1ef0eda8652f9dc8d1850d29d3eef9e5e82ffe5d6791/ciso8601-2.3.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99a1fa5a730790431d0bfcd1f3a6387f60cddc6853d8dcc5c2e140cd4d67a928", size = 41578, upload-time = "2025-08-20T16:30:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cf/07321ce5cf099b98de0c02cd4bab4818610da69743003e94c8fb6e8a59cb/ciso8601-2.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c35265c1b0bd2ac30ed29b49818dd38b0d1dfda43086af605d8b91722727dec0", size = 42085, upload-time = "2025-08-20T16:31:00.338Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c7/3c521d6779ee433d9596eb3fcded79549bbe371843f25e62006c04f74dc9/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa9df2f84ab25454f14df92b2dd4f9aae03dbfa581565a716b3e89b8e2110c03", size = 41313, upload-time = "2025-08-20T16:31:01.313Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/efd40db0d6b512be1cbe4e7e750882c2e88f580e17f35b3e9cc9c23004b5/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32e06a35eb251cfc4bbe01a858c598da0a160e4ad7f42ff52477157ceaf48061", size = 41443, upload-time = "2025-08-20T16:31:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/83/e5/eee65bc8c91e5981ed3440dbd4e546ff14b67deba07f6f346de1a61f28c0/ciso8601-2.3.3-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d88ab28ecb3626e3417c564e8aec9d0245b4eb75e773d2e7f3f095ea9897ded", size = 16956, upload-time = "2025-08-20T16:31:24.869Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/809cba0f1928d1d45a4e5c9d789b06fd092a145702d32a41394f4b9665ca/ciso8601-2.3.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d5a37798bf0cab6144daa2b6d07657ab1a63df540de24c23a809fb2bdf36149", size = 18285, upload-time = "2025-08-20T16:31:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1d/025db546af38ab5236086f462292c50a1f9a4b248a309129a85bb1113996/ciso8601-2.3.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5b18c75c66499ef22cb47b429e3b5a137db5a68674365b9ca3cd0e4488d229f", size = 16957, upload-time = "2025-08-20T16:31:27.503Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/976d9c4b79e28cbda95b1acf574b00f811d9aec0fce55b63d573d6fa446b/ciso8601-2.3.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58799673ffdf621fe138fb8af6a89daf4ddefdf7ca4a10777ad8d55f3f171b6e", size = 18284, upload-time = "2025-08-20T16:31:28.43Z" }, +] + [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] -[[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 = "comm" version = "0.2.3" @@ -527,89 +920,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, -] - [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -621,9 +937,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, @@ -632,9 +945,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, @@ -643,9 +953,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, - { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, @@ -654,9 +961,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, - { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, - { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, @@ -665,9 +969,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, - { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, @@ -676,118 +977,136 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -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.11'" }, +version = "7.10.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, + { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, + { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, + { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, + { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +] + +[[package]] +name = "cronsim" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/1a/02f105147f7f2e06ed4f734ff5a6439590bb275a53dd91fc73df6312298a/cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e", size = 14213, upload-time = "2025-10-21T16:38:20.431Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.13.2' and platform_python_implementation != 'PyPy' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" }, + { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" }, + { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" }, + { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/47fc6223a341f26d103cb6da2216805e08a37d3b52bee7f3b2aee8066f95/cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8", size = 7198626, upload-time = "2025-10-01T00:27:54.8Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" }, + { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" }, + { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" }, + { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" }, + { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/25/b2/067a7db693488f19777ecf73f925bcb6a3efa2eae42355bafaafa37a6588/cryptography-46.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f25a41f5b34b371a06dad3f01799706631331adc7d6c05253f5bca22068c7a34", size = 3701860, upload-time = "2025-10-01T00:28:53.003Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/1aabe338149a7d0f52c3e30f2880b20027ca2a485316756ed6f000462db3/cryptography-46.0.2-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1d3b3edd145953832e09607986f2bd86f85d1dc9c48ced41808b18009d9f30e5", size = 3714495, upload-time = "2025-10-01T00:28:57.222Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0a/0d10eb970fe3e57da9e9ddcfd9464c76f42baf7b3d0db4a782d6746f788f/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fe245cf4a73c20592f0f48da39748b3513db114465be78f0a36da847221bd1b4", size = 4243379, upload-time = "2025-10-01T00:28:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/7d/60/e274b4d41a9eb82538b39950a74ef06e9e4d723cb998044635d9deb1b435/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2b9cad9cf71d0c45566624ff76654e9bae5f8a25970c250a26ccfc73f8553e2d", size = 4409533, upload-time = "2025-10-01T00:29:00.785Z" }, + { url = "https://files.pythonhosted.org/packages/19/9a/fb8548f762b4749aebd13b57b8f865de80258083fe814957f9b0619cfc56/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9bd26f2f75a925fdf5e0a446c0de2714f17819bf560b44b7480e4dd632ad6c46", size = 4243120, upload-time = "2025-10-01T00:29:02.515Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/883f24147fd4a0c5cab74ac7e36a1ff3094a54ba5c3a6253d2ff4b19255b/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:7282d8f092b5be7172d6472f29b0631f39f18512a3642aefe52c3c0e0ccfad5a", size = 4408940, upload-time = "2025-10-01T00:29:04.42Z" }, ] [[package]] @@ -799,6 +1118,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "dbus-fast" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/f7/36515d10e85ab6d6193edbabbcae974c25d6fbabb8ead84cfd2b4ee8eaf6/dbus_fast-4.0.0.tar.gz", hash = "sha256:e1d3ee49a4a81524d7caaa2d5a31fc71075a1c977b661df958cee24bef86b8fe", size = 75082, upload-time = "2026-02-01T20:56:27.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/f3/55d3bb231ca5c6c7eaee736d0df47fad1da87d79c3192277072e7edcf239/dbus_fast-4.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a29ad81e59b328c840c9020daa855971d8f345d2c2472e9d5b200b3c82fc734", size = 832489, upload-time = "2026-02-01T21:05:00.327Z" }, + { url = "https://files.pythonhosted.org/packages/c5/37/f02d0a7ffa513e007d8b2ef159aaef42396172424cab9c1233183ea16795/dbus_fast-4.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e3d62b7a0e392a80f61227c6f314e969dd5bec36e693723728908f8e8a172885", size = 875549, upload-time = "2026-02-01T21:05:01.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/31/c418db2977cd17df67813fa6423ca1558e95697a45ebd3817f24db3bbf97/dbus_fast-4.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:35bbeb692e60ff2a0eb3f97dc4b048e92fc7ddc8468ed7bd173bc5513d4690cc", size = 837531, upload-time = "2026-02-01T21:05:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/9d/95/eb8e8045de0b8c97336f2c6fef86582260c6c11d10ffd4ba20b19f2f0f67/dbus_fast-4.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dfa3cb3137c727ea50d89e9e4e4ce5042e28baf36fcc8b1e3c84dff50eee70aa", size = 881970, upload-time = "2026-02-01T21:05:04.637Z" }, + { url = "https://files.pythonhosted.org/packages/a6/43/c46a4aaeb4bdc4ce7e20c3204eb7f455aa9ddf065c9cbe5fdd8c03a72752/dbus_fast-4.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:512f25a0705903047e9b55d2bc3724f06dcbfb77e0b13f10a7eb835679d3705c", size = 829838, upload-time = "2026-02-01T21:05:07.337Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/8ba9bf4027cc29c1eeb1e90828f151df6691012a67a0c3dabf4955e68f83/dbus_fast-4.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28209c72c36f8e2bb2152c02598d353e9442d53d751efbf49870bc37ac3afcad", size = 875354, upload-time = "2026-02-01T21:05:09.516Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fd/b7fbb9546d575dc73b7540a908a38ccba46239d8d838d3c41454e6a56a70/dbus_fast-4.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:618931126219f23285b33b5825dc40cfb166c8e6554f800f7c53dfb5f368289b", size = 835403, upload-time = "2026-02-01T21:05:11.176Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3d/f9d17947917817ba88791e66169dbe807218d38a5bee4320c215ee3a187e/dbus_fast-4.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0615063551e8d4b34bee778885ab56be3ef168df38f9bfc4364d8c80687e2df4", size = 881686, upload-time = "2026-02-01T21:05:12.602Z" }, + { url = "https://files.pythonhosted.org/packages/64/2b/92d24e61a4774d752d25e81485fcdf23a540c3e0a9f06939463b8b22fd0d/dbus_fast-4.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfb269a9ed3b3ab29932b2948de52d7ea2eebfcad0c641ad6b25024b048d0b68", size = 791936, upload-time = "2026-02-01T21:05:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/584e55aa3afa108c3dc34a7a307bfbb628954ad0a393a3a3dc8e76315724/dbus_fast-4.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa367aaad3a868dfb9373eca8868a2a0810bac6cbe35b67460682127834c2460", size = 843295, upload-time = "2026-02-01T21:05:17.671Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1e/6633e0395ea429c0f95ddab2889baebb11741f6531318342a1778a2a0c89/dbus_fast-4.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2283e9c22411b1307fa3e3586fd4b42b44cae90e8a39f4fb4942a97a885d437b", size = 799113, upload-time = "2026-02-01T21:05:19.934Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/6394951e1400760f9c95045a0f11d8019e38cdd2900935b9048b6e6953c3/dbus_fast-4.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a91ec3707b743c2e211fa9ecd08ee483c3af19a2028ad90d2911a7e17d20737", size = 851197, upload-time = "2026-02-01T21:05:21.277Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f6/1aaba04040b763c1baa3e395fb4e73b9b51a2213d356f924e5574e1d7d61/dbus_fast-4.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b83681987b2986af050b728ecea5e230252c09db3c9593cead5b073f6391f41", size = 790986, upload-time = "2026-02-01T21:05:24.553Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/744ca46c1b9566907aa01affa2623970cd721f6a5c5f82d5eb852356914c/dbus_fast-4.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:191c9053c9d54356f0c5c202e2fab9ad2508b27b8b224a184cf367591a2586cb", size = 840552, upload-time = "2026-02-01T21:05:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/c40beb615adde9b00352f5ed3bad827a17d1a505c4d064cdf8dcb795d816/dbus_fast-4.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c34c748b71c6fc71e47ffe901ccfcd4a01e98d5fa80f98c732945da45d9fc614", size = 797107, upload-time = "2026-02-01T21:05:28.391Z" }, + { url = "https://files.pythonhosted.org/packages/58/5f/97c1f07b460577bf9a19016dca1351298c142cb3791fed49f050acea26e9/dbus_fast-4.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:39ac2e639833320678c2c4e64931b28a3e10c57111c8c24967f1a16de69b92b0", size = 849752, upload-time = "2026-02-01T21:05:30.369Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c5/5fee1e5d59b2856db9da8372c67ed7699b262108a4540d5858f34a67699f/dbus_fast-4.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e53d7e19d2433f2ca1d811856e4b80a3b3126f361703e5caf6e7f086a03b994", size = 804142, upload-time = "2026-02-01T21:05:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/3e/91a9339278ccee8be93df337c69703dd9d3f5b8fc97dadb2f8a3ff06f6c0/dbus_fast-4.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b430760c925e0b695b6f1a3f21f6e57954807cab4704a3bc4bc5f311261016b", size = 846011, upload-time = "2026-02-01T21:05:34.875Z" }, + { url = "https://files.pythonhosted.org/packages/34/bf/bab415e523fc67a3b1d246a677dcac1198b5cf4d89ae594b2b25b71c02c7/dbus_fast-4.0.0-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:2818d76da8291202779fe8cb23edc62488786eee791f332c2c40350552288d8b", size = 844116, upload-time = "2026-02-01T20:56:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/5cc517508d102242656c06acb3980decd243e56470f9cb51dc736a9197ef/dbus_fast-4.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0b2aaf80991734e2bbff60b0f57b70322668acccb8bb15a0380ca80b8f8c5d72", size = 810621, upload-time = "2026-02-01T21:05:36.208Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/686bd523c9966bbd9c0705984782fcb33d3a2aae75a2ebbb34b37aca1f3b/dbus_fast-4.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93a864c9e39ab03988c95e2cd9368a4b6560887d53a197037dfc73e7d966b690", size = 853111, upload-time = "2026-02-01T21:05:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/26a2a2120c32bf6a61b81a19d7d20cd440c79f1c4679b04af85af93bc0e4/dbus_fast-4.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c71b369f8fd743c0d03e5fd566ff5d886cb5ad7f3d187f36185a372096a2a096", size = 1534384, upload-time = "2026-02-01T21:05:41.636Z" }, + { url = "https://files.pythonhosted.org/packages/d0/53/916c2bbb6601108f694b7c37c71c650ef8d06c2ed282a704b5c8cca67edf/dbus_fast-4.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffc16ee344e68a907a40327074bca736086897f2e783541086eedb5e6855f3f0", size = 1610347, upload-time = "2026-02-01T21:05:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f6/05eeb374a02f63b0e29b1ee2073569e8cf42f655970a651f938bcdbe7eae/dbus_fast-4.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f8f4b0f8af730c39bbb83de1e299e706fbd7f7f3955764471213b013fa59516", size = 1549395, upload-time = "2026-02-01T21:05:45.159Z" }, + { url = "https://files.pythonhosted.org/packages/a4/87/d03a718e7bfdbbebaa4b6a66ba5bb069bc00a84e5ad176d8198cc785cd42/dbus_fast-4.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6af190d8306f1bd506740c39701f5c211aa31ac660a3fcb401ebb97d33166c7", size = 1627620, upload-time = "2026-02-01T21:05:46.878Z" }, +] + [[package]] name = "debugpy" version = "1.8.17" @@ -807,24 +1159,14 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c42 wheels = [ { url = "https://files.pythonhosted.org/packages/38/36/b57c6e818d909f6e59c0182252921cf435e0951126a97e11de37e72ab5e1/debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542", size = 2098021, upload-time = "2025-09-17T16:33:22.556Z" }, { url = "https://files.pythonhosted.org/packages/be/01/0363c7efdd1e9febd090bb13cee4fb1057215b157b2979a4ca5ccb678217/debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3", size = 3087399, upload-time = "2025-09-17T16:33:24.292Z" }, - { url = "https://files.pythonhosted.org/packages/79/bc/4a984729674aa9a84856650438b9665f9a1d5a748804ac6f37932ce0d4aa/debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4", size = 5230292, upload-time = "2025-09-17T16:33:26.137Z" }, - { url = "https://files.pythonhosted.org/packages/5d/19/2b9b3092d0cf81a5aa10c86271999453030af354d1a5a7d6e34c574515d7/debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a", size = 5261885, upload-time = "2025-09-17T16:33:27.592Z" }, { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, - { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, - { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" }, { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" }, { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" }, { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" }, - { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" }, { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, ] @@ -837,14 +1179,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "dropbox" version = "12.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, - { name = "six" }, - { name = "stone" }, + { name = "requests", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "six", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "stone", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/56/ac085f58e8e0d0bcafdf98c2605e454ac946e3d0c72679669ae112dc30be/dropbox-12.0.2.tar.gz", hash = "sha256:50057fd5ad5fcf047f542dfc6747a896e7ef982f1b5f8500daf51f3abd609962", size = 560236, upload-time = "2024-06-03T16:45:30.448Z" } wheels = [ @@ -852,15 +1203,21 @@ wheels = [ ] [[package]] -name = "exceptiongroup" -version = "1.3.1" +name = "envs" +version = "1.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +sdist = { url = "https://files.pythonhosted.org/packages/3c/7f/2098df91ff1499860935b4276ea0c27d3234170b03f803a8b9c97e42f0e9/envs-1.4.tar.gz", hash = "sha256:9d8435c6985d1cdd68299e04c58e2bdb8ae6cf66b2596a8079e6f9a93f2a0398", size = 9230, upload-time = "2021-12-09T22:16:52.616Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/bc/f8c625a084b6074c2295f7eab967f868d424bb8ca30c7a656024b26fe04e/envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1", size = 10988, upload-time = "2021-12-09T22:16:51.127Z" }, ] -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" } + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] @@ -877,10 +1234,10 @@ name = "fastapi" version = "0.128.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, + { name = "annotated-doc", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "starlette", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } wheels = [ @@ -896,6 +1253,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/b2/731a6696e37cd20eed353f69a09f37a984a43c9713764ee3f7ad5f57f7f9/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a", size = 516760, upload-time = "2025-10-19T22:25:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/c5/79/c73c47be2a3b8734d16e628982653517f80bbe0570e27185d91af6096507/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00", size = 264748, upload-time = "2025-10-19T22:41:52.873Z" }, + { url = "https://files.pythonhosted.org/packages/24/c5/84c1eea05977c8ba5173555b0133e3558dc628bcf868d6bf1689ff14aedc/fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470", size = 254537, upload-time = "2025-10-19T22:33:55.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/4e362367b7fa17dbed646922f216b9921efb486e7abe02147e4b917359f8/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d", size = 278994, upload-time = "2025-10-19T22:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/3985be633b5a428e9eaec4287ed4b873b7c4c53a9639a8b416637223c4cd/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8", size = 280003, upload-time = "2025-10-19T22:23:45.415Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/6ef192a6df34e2266d5c9deb39cd3eea986df650cbcfeaf171aa52a059c3/fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219", size = 303583, upload-time = "2025-10-19T22:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/9d/11/8a2ea753c68d4fece29d5d7c6f3f903948cc6e82d1823bc9f7f7c0355db3/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6", size = 460955, upload-time = "2025-10-19T22:36:25.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/42/7a32c93b6ce12642d9a152ee4753a078f372c9ebb893bc489d838dd4afd5/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe", size = 480763, upload-time = "2025-10-19T22:24:28.451Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e9/a5f6f686b46e3ed4ed3b93770111c233baac87dd6586a411b4988018ef1d/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d", size = 452613, upload-time = "2025-10-19T22:25:06.827Z" }, + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, +] + [[package]] name = "filelock" version = "3.20.0" @@ -910,13 +1320,70 @@ name = "fire" version = "0.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "termcolor" }, + { name = "termcolor", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/00/f8d10588d2019d6d6452653def1ee807353b21983db48550318424b5ff18/fire-0.7.1.tar.gz", hash = "sha256:3b208f05c736de98fb343310d090dcc4d8c78b2a89ea4f32b837c586270a9cbf", size = 88720, upload-time = "2025-08-16T20:20:24.175Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/4c/93d0f85318da65923e4b91c1c2ff03d8a458cbefebe3bc612a6693c7906d/fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882", size = 115945, upload-time = "2025-08-16T20:20:22.87Z" }, ] +[[package]] +name = "fnv-hash-fast" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fnvhash", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/19/10f4e1b4bbfe7cf162d20bb4d54bd62935d652e2ea107ddb0b5a6c4e8b75/fnv_hash_fast-1.6.0.tar.gz", hash = "sha256:a09feefad2c827192dc4306826df3ffb7c6288f25ab7976d4588fdae9cbb7661", size = 5675, upload-time = "2025-10-04T19:35:00.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/c1/9e28c04db2c7554c04e2b1bf9acbeef5c202eb02ff930a6dcaf12988daea/fnv_hash_fast-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ee5d5fcdf1f24c5adb5c2e009f3f930c992bc94649bba06123cce6bbf1653e6", size = 13030, upload-time = "2025-10-04T19:44:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d8/229e45a76775472e305f2f69dc56982111ada69bea90cc6938f3b8c5daeb/fnv_hash_fast-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1af1b4280df65626c96d83253c7266f850d77c82247c4934158f94df9f2019e", size = 13611, upload-time = "2025-10-04T19:44:59.136Z" }, + { url = "https://files.pythonhosted.org/packages/47/de/bd02dc21dc6d6ff67288ac5d3b77b4a1b9503579d16c4358cb11dcc08404/fnv_hash_fast-1.6.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:56b74fe49032c4fc677a535693e2bd2e8758e98b8e9f9e718488eaa52ab45d78", size = 14655, upload-time = "2025-10-04T19:45:00.161Z" }, + { url = "https://files.pythonhosted.org/packages/45/96/41191876659756453e12a1354ee0e911d2dfb88a8647e6a2435af089f162/fnv_hash_fast-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:332fd17375357d6a83278d5cd264de3ece1063defd9af342d4f29178fba10cc9", size = 15814, upload-time = "2025-10-04T19:45:01.142Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/9e5983af23c12429a58121d32cf038152e199c018e51321305f270dc2a84/fnv_hash_fast-1.6.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6fad7a0ab855fc3c15899622ed0230b16a8ddf2a2d7bec23705fa08e2d3d2d1e", size = 13850, upload-time = "2025-10-04T19:45:02.01Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/5d05f166f2fefae734bce7be60485677680820f86b58a6785fd1f3c29ce1/fnv_hash_fast-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6feab601350657ea6b4a96c0efd6b4ce25d6599df131cbd418dd4b66bf48a183", size = 15752, upload-time = "2025-10-04T19:45:02.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/cb15a8518ce77aff19d4af6d922a331b833970ff655b498987372c49e246/fnv_hash_fast-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1f9d46414a7c856dc57002eda9bcf13427deb18ef285f371fc3319a7ac7a3e4f", size = 14205, upload-time = "2025-10-04T19:45:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a4/5d4c9550e3588dfe2041fa489a8d6adec594d398e12977382349ea890fc5/fnv_hash_fast-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e3c978f8bf86a470d077b3087afc7b736a7a02f724377f14db45843dd3b3f701", size = 15001, upload-time = "2025-10-04T19:45:05.506Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/a250c545faccf828772830ae3db234ad8741ed57832d96b3aa9d6ebe8616/fnv_hash_fast-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8054c711283a4d598d516c17445d6c12304c93e48f7e97aba970549e0d5b413b", size = 13373, upload-time = "2025-10-04T19:45:09.032Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/fb0f0e516a46c2288f6dd4afea0ba1c7c3065ac0565fc12d9c662c7d2434/fnv_hash_fast-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:135bc62ec5d61a9a38222afce83d3d0c9d4d52fa8a2670f6acc86ee6818d2bfa", size = 13901, upload-time = "2025-10-04T19:45:10.25Z" }, + { url = "https://files.pythonhosted.org/packages/22/76/f4c7c8784b836da32fa84b1ef78455b3d26950c46da99cfeffa9dfdfbb95/fnv_hash_fast-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34cd7e776fd515dfcab4cf84bdb63d879653cc3f77f6f086822ed9c50645f75c", size = 15210, upload-time = "2025-10-04T19:45:11.198Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/a8c154b9b1fae5ab2ded2a389efef18c7332d93a1ab260f5cae02e34b3d8/fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9189a9a1b820658c791728b4558b4aadad6b35429b1784bcaf57183788d3f489", size = 16299, upload-time = "2025-10-04T19:45:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/674c4d4ac7bcc9145f351707d2d944c76c7bdc955700707468854e6cf00b/fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893306251f69bf9591b51e75dfac2bac9703c4156b9c7e78dd2930e33b184f1f", size = 14208, upload-time = "2025-10-04T19:45:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/73/90/54752bc1cfcc281aef379c5926c00b127006e6a4197c701796dd382f81c6/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7107073d69dcf68b602bddf37a6ab2af6e9afcfc31239e215b75dcc5049667ae", size = 16197, upload-time = "2025-10-04T19:45:14.876Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4f/971d08ab32e41d7cc4255dc0d2e94be8b907def305b45fbe9c62f1c75db6/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f0b9cbb8538e981d4e0895f01af6510bae5dd6bcb1a8bf3db8cecd9877079e66", size = 14634, upload-time = "2025-10-04T19:45:15.822Z" }, + { url = "https://files.pythonhosted.org/packages/16/bf/ff169383b41176a15828aec88a39a8b1397041e6d3948273aec010688712/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11b93131512147038d54c92106a4035b1ae71488abe730952bdeb55a9d7dcb18", size = 15542, upload-time = "2025-10-04T19:45:17.064Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/c73abc05dd01434442dbd38a2e50166e9ba59f8db41cdf82649410c37d12/fnv_hash_fast-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34e4f2acc41aacd97877d396948b38efc7197a2dd91c15e818c049c4d48b0a0", size = 13350, upload-time = "2025-10-04T19:45:20.184Z" }, + { url = "https://files.pythonhosted.org/packages/75/f8/a79d5a29dcf3b0e41635056ee37fff9e2bc46e3625d44b163a4ac2b9160c/fnv_hash_fast-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1a1fe55163d38052ec90aaf16f190bb807342aa09f9680185b9772ce0407b62", size = 13864, upload-time = "2025-10-04T19:45:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/c1/41/fabca5bf0c5b36405517908974e93f1832780d692271295efaf8dba40afc/fnv_hash_fast-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d7c3a18e7aa483d18ff569554b07b5238403775f8e401245ab8b3c27bcb34cf", size = 15206, upload-time = "2025-10-04T19:45:22.242Z" }, + { url = "https://files.pythonhosted.org/packages/04/7f/1c5c4e451c0213b44235b39737cecf3e58f4195332b173f45e2c95a9b0d8/fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8fdeee59431cc03afdb8a04c3c46b452dc2ded85973953b7077715e897a85b", size = 16301, upload-time = "2025-10-04T19:45:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7c/095bb6f7ed9bbb85d7451312388fd61dfcde194aad5a2e3902e8fd908a78/fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6d8284c7ad0339def03252905f3456195ec9d77d8329225e5b09b226e3eb79ec", size = 14175, upload-time = "2025-10-04T19:45:24.397Z" }, + { url = "https://files.pythonhosted.org/packages/73/52/ced7073eaee3479b4e09fed73c1db5a51a9bc72f7546324126b76b4c2f9b/fnv_hash_fast-1.6.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:03642803cc4567dada952d7b1490d6eedd97cd960a83ebbb4a4b7c545629f33f", size = 14581, upload-time = "2025-10-04T19:34:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a3/877d7f9bce7efccb70607307b25abec35f1206f5dcb3b5a898ad67d61dbf/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3a540bae99086d3942a2976c16480916cb86d9f06a632023176fe4fa56d298b5", size = 16214, upload-time = "2025-10-04T19:45:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e5/f26eb6e262a8d2329aad6d618b102c238009a11e00d6c5914cb510d1d968/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9d6a447404ddfc0035a52de80747c36dce8ba0cc24c27610ca4be9c0ba46d783", size = 14649, upload-time = "2025-10-04T19:45:26.568Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/6791aa693e9400d00ef56be40586bc9de7b826756f0156a3f4e5b3b6d40b/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d016ee85cd9faccb2f958e5017eb60c8c6410b1700f85052f5dbf2b34084c7ef", size = 15554, upload-time = "2025-10-04T19:45:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/9c724ac795f53578dd6be61d6a0466c4cd51550485b301764ddfc6ed5ad1/fnv_hash_fast-1.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:07bb79eaa44f91db2aab3b641194f68dc4ddd15701756f687c1a7a294bfa9c06", size = 13296, upload-time = "2025-10-04T19:45:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/0c/11/a2eb0a7fbfb5d5cb5d27df7f6d4e395ce2f328da16d32702909af00ffe82/fnv_hash_fast-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4176315430f9fcf5346a0339b0f55982e1715452345d70c2887755bfd5aa2b64", size = 13879, upload-time = "2025-10-04T19:45:32.063Z" }, + { url = "https://files.pythonhosted.org/packages/0e/85/3a297faae2416916f7a5cb858b08b500296bbc7d7136faf2cfbadde61e33/fnv_hash_fast-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31db9d944c91d286475870855b9203f4fb4794cb0674de5458e9d1231e07f37", size = 15222, upload-time = "2025-10-04T19:45:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/9c81426e4a22d15dc9c1a73536c6a7e2aeb8a71ac0b398d841ebd287e8e5/fnv_hash_fast-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fc1bbec5871060c6efa6a444a554496f372f1f4a7e83b99989be5ea6b97435f", size = 16379, upload-time = "2025-10-04T19:45:34.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/7f1454ebc9dee224d6ee5360111e3855802ce79f48f1808117998771ffaa/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:91ed6df63ab2082b5b48a6b8f5d7eb7b51d39c2eeffd64821301bf6d9662ff11", size = 16252, upload-time = "2025-10-04T19:45:35.243Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/cedd70c2e09ba09f5834c7e50f8fed4a37bba38c0c2471849bb4dac91148/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6d34541e15bbc3877da7541f059fb1eadf53031abe7fc4318b28421e02eff383", size = 15570, upload-time = "2025-10-04T19:45:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/da/7a/b5bd2b9a06269098af059e79e05ceff320a405b1c49b9f3d29708324179b/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:83aa2d791193e3b3f4132741c4dc09eed4f7df8000d76ad77fb9d24db8e59a88", size = 21338, upload-time = "2025-10-04T19:45:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/21/07/1688d543a7688529857cd43bcff3ac324c69fd2923a9b40a1adc120cef20/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b8d33f002bb336f9f0949a32d7da07cc9d340a9d07e4f16cc9ece982842eb4e0", size = 22455, upload-time = "2025-10-04T19:45:40.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/588f43d8dd122fc884c3556f993a3e3db953afecc62fa812d439f69ec067/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0042af2a1cb7ffae412ec3cb6ae8c581a73610fd523f7e17ed58a5359505ffec", size = 25053, upload-time = "2025-10-04T19:45:41.74Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/6209457f59e0ff43b066ca8cbfeb800bc0af478e221e74beadaf0b58effa/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73308e11c0e5a2dba433fc5645672de4756a52b323de1dab20e45d4fe5e83994", size = 27875, upload-time = "2025-10-04T19:45:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/c4796f6b1ee6cb778620663d00eadb970a8271fb537ce75774d5acfeecdb/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96282ecb75bec190af0111e82ddd38afc98e9cb867a1689e873ab6802af951b7", size = 27443, upload-time = "2025-10-04T19:45:44.274Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5b/846b8f977dda4f0e7f1ec4ffff6707b9e666dabb9eb203c4c2bfc4b0b6fe/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cae16753c1d85ed358df13824bd8a474bfa9da34daddc1a90c72b25ff4177f51", size = 25765, upload-time = "2025-10-04T19:45:45.565Z" }, +] + +[[package]] +name = "fnvhash" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/43/30d2dd2b14621b2004f658ba5335e5a6f5a9c1338ed37678d7fd247b7a9c/fnvhash-0.2.1.tar.gz", hash = "sha256:0c7e885f44c8f06de07f442befebc590ee9ca0cc88846681f608496284ce9cd5", size = 19057, upload-time = "2025-05-05T16:59:10.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/92/7c8abc21a1de7159013c0b0bd2ecf06530959bb14fd5c3bf0045e788c6d9/fnvhash-0.2.1-py3-none-any.whl", hash = "sha256:00fab14bec841e4cb29b4fd2ed9358f8bf9f4600d9d8149cde27a191193a33e8", size = 18115, upload-time = "2025-05-05T16:59:09.269Z" }, +] + [[package]] name = "fonttools" version = "4.60.1" @@ -929,51 +1396,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/20/9b2b4051b6ec6689480787d506b5003f72648f50972a92d04527a456192c/fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", size = 4794635, upload-time = "2025-09-29T21:11:08.651Z" }, { url = "https://files.pythonhosted.org/packages/10/52/c791f57347c1be98f8345e3dca4ac483eb97666dd7c47f3059aeffab8b59/fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", size = 4843878, upload-time = "2025-09-29T21:11:10.893Z" }, { url = "https://files.pythonhosted.org/packages/69/e9/35c24a8d01644cee8c090a22fad34d5b61d1e0a8ecbc9945ad785ebf2e9e/fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", size = 4954555, upload-time = "2025-09-29T21:11:13.24Z" }, - { url = "https://files.pythonhosted.org/packages/f7/86/fb1e994971be4bdfe3a307de6373ef69a9df83fb66e3faa9c8114893d4cc/fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", size = 2232019, upload-time = "2025-09-29T21:11:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/40/84/62a19e2bd56f0e9fb347486a5b26376bade4bf6bbba64dda2c103bd08c94/fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", size = 2276803, upload-time = "2025-09-29T21:11:18.152Z" }, { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, - { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" }, { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" }, { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" }, - { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" }, { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, - { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" }, { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" }, { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" }, { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" }, { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" }, { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" }, - { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" }, { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" }, { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" }, { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" }, { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" }, { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" }, { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" }, - { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" }, { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, ] +[[package]] +name = "freezegun" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/75/0455fa5029507a2150da59db4f165fbc458ff8bb1c4f4d7e8037a14ad421/freezegun-1.5.2.tar.gz", hash = "sha256:a54ae1d2f9c02dbf42e02c18a3ab95ab4295818b549a34dac55592d72a905181", size = 34855, upload-time = "2025-05-24T12:38:47.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/b2/68d4c9b6431121b6b6aa5e04a153cac41dcacc79600ed6e2e7c3382156f5/freezegun-1.5.2-py3-none-any.whl", hash = "sha256:5aaf3ba229cda57afab5bd311f0108d86b6fb119ae89d2cd9c43ec8c1733c85b", size = 18715, upload-time = "2025-05-24T12:38:45.274Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -993,9 +1460,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, - { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, - { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, @@ -1009,9 +1473,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, @@ -1025,9 +1486,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, @@ -1041,9 +1499,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, @@ -1057,9 +1512,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, @@ -1073,9 +1525,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, @@ -1089,9 +1538,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] @@ -1117,7 +1563,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, @@ -1125,7 +1570,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, @@ -1133,7 +1577,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, @@ -1141,7 +1584,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, @@ -1149,7 +1591,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, @@ -1159,6 +1600,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, ] +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1168,47 +1660,234 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "habluetooth" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-interrupt", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bleak", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bleak-retry-connector", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bluetooth-adapters", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bluetooth-auto-recovery", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bluetooth-data-tools", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "btsocket", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "dbus-fast", marker = "python_full_version >= '3.13.2' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/89/0da109c6ed1704c991f9d466a3dbee99f8a921d006d6f44eac84fff95da7/habluetooth-5.8.0.tar.gz", hash = "sha256:7ecbe1ad6a4d3610f918dbe573bb9bee16064e7a4a61c95c37ef22b0c4533493", size = 49177, upload-time = "2025-12-02T19:30:53.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/de/3333b072726da4ae0f03ef68f273b79365e4d891521e9abfdc5edf7f8433/habluetooth-5.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a3e7e4b7555ab833f7fcf64f92601af18449ef1672820aba9d6c5d296cb89ea", size = 575229, upload-time = "2025-12-02T19:47:12.497Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f5/69c662a208791c2b08d27c3008dfe29179b6242185a7b41a608c7a3d1e86/habluetooth-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cebe57227316b4d8e2b4af511ed85d3cabba93313aa640e91d390d6b4f0393af", size = 563861, upload-time = "2025-12-02T19:47:14.01Z" }, + { url = "https://files.pythonhosted.org/packages/33/6e/5d577236914b1dfd1c687880090636d0e233ec853aba14de163df10dd6a1/habluetooth-5.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe5a5a80fb4abe3f884a91e18d5f271e72bfa384e063b225e87a97e3f0c7f6bb", size = 692032, upload-time = "2025-12-02T19:47:15.604Z" }, + { url = "https://files.pythonhosted.org/packages/b9/97/946b362a5647fd8422e3a4dba981f848a362b47b63a87ebb6808ca8557aa/habluetooth-5.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d7da1d1fc383479e35c780d6359a5efb69a702aee2a4677bf05a9a5422f506cf", size = 666908, upload-time = "2025-12-02T19:47:17.209Z" }, + { url = "https://files.pythonhosted.org/packages/ed/10/6754a394d3de6777482ebde6b1ca11e1c2e09feed819dd0559bffbdacf77/habluetooth-5.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e87ace66ae45e8c1488adb24bbfe8a2fa857a3ee9c7ea5218e08224ed999c791", size = 725704, upload-time = "2025-12-02T19:47:18.506Z" }, + { url = "https://files.pythonhosted.org/packages/b7/49/3d0ea82678222f0f7f8aab925cc19f497f82c6bb9b79bf1410e51d74f85a/habluetooth-5.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:363b5c51099532def49765500fd0d85ad2bf0427de66542d405a0d79a366f444", size = 700767, upload-time = "2025-12-02T19:47:20.114Z" }, + { url = "https://files.pythonhosted.org/packages/40/89/3558e8d7045e52b241a17c7427c0d55852b5cfe1c71c111f2301f6cbaf39/habluetooth-5.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dc53fdcf16f7803f6094693e949542f834cb338cbac8d8962db7fa154aa0fa2", size = 668409, upload-time = "2025-12-02T19:47:21.432Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/f967277d736856869e50021826ab862cc44fb72961db50e4dca27443a7c9/habluetooth-5.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce46a6520c346037e9332a5ec2cf0c8b2fbe6075101006491b8bd4a2d76df197", size = 733310, upload-time = "2025-12-02T19:47:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/5f41bb8367db0f704bf27b7a346dda3e0e4965b1a2107c33af1078ca5271/habluetooth-5.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c5bd5ac723d2b38f5d56506408d56fa1de0f9a063410b6ce8061961849a1b63", size = 570041, upload-time = "2025-12-02T19:47:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/4f/20/22dea6dd139f6addf78d02bbd840ba565353aee29ca812a2488b14b6b4ff/habluetooth-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e920fe6dd4fbb601f22043c205d3baaae5ac9804206d16a424cd19e00493d88d", size = 563353, upload-time = "2025-12-02T19:47:28.371Z" }, + { url = "https://files.pythonhosted.org/packages/39/e0/e2ddf93dbe2cbd536c9d66c7189ce394ca40133ab84420748e94afda7be0/habluetooth-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1086a8b76364e8c008b3ddc54e420f0aa74187231edfa987d362912cae339350", size = 656230, upload-time = "2025-12-02T19:47:29.712Z" }, + { url = "https://files.pythonhosted.org/packages/0b/af/be98ead575eced913fa396fd985ed47786db20ae11f9e1a3cc954161be40/habluetooth-5.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1702528676be2ebe17a2bd357fe7dc09f2855b70fd83de64392f040b18f404c5", size = 643444, upload-time = "2025-12-02T19:47:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6a/cd096b7c405b867a1864b5326c4a250942c8fa734ae319ba0e0ba485aab4/habluetooth-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b875a4de30861d3a9359c154fa21532c235d337ca48d203ea0dc74df3e60885c", size = 700091, upload-time = "2025-12-02T19:47:32.281Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3c/aebb70fdad26c7f547773cd95d381610606e0bd36c45bfc7652f593f08ac/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c184a80d342f01087478ba643e5e35e04c57a364e55b7f3092742d98a18707b3", size = 664128, upload-time = "2025-12-02T19:47:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5b/22ecdabd000c0cd1e9984f0dec257549b96e011a978b973797e024cc9063/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e431d2c833c83e748490b3d2bb37a4625d36c89e7d3faac03286b916fca910e3", size = 647860, upload-time = "2025-12-02T19:47:35.671Z" }, + { url = "https://files.pythonhosted.org/packages/88/ac/7e87cbd3f5a34361d37b23c8ec8ed48bea7c81246f01db88e2b41ad7f752/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbbe296186c7e66ef79dca12e4e69326f8748db405e7203451119c9d7c1e5a2e", size = 705598, upload-time = "2025-12-02T19:47:37.102Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/6bb983149530899647ee0807e5fc46124b56f0b326c6a952df48ed5c2b21/habluetooth-5.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a88e3f586427642355927d43ad80190964589ae99d0d579ac4a39395d60f9a7", size = 566214, upload-time = "2025-12-02T19:47:41.767Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/18ff974967494402f827dab3d69d9a7d51d7d62153d8872206c0d4c4b058/habluetooth-5.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bb028a5d46fdead2ad687182db10667da201f54cc0f8eeba54870a3c2d1797f", size = 560005, upload-time = "2025-12-02T19:47:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d1/77ca3c8b3213f1f8429902329cad1b2e2a637cd32df70748ff5e187aad3b/habluetooth-5.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c425044ee873d5730571601097bb55020a7ff293cf96972e8924cf1719aa5345", size = 656376, upload-time = "2025-12-02T19:47:45.138Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/b06804e511552f1e5553b5abfb48f7bec795ffbe6c3290d95abfe35e51b4/habluetooth-5.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dc8baf5eee8835cfe436b14a6b4dd087f8134a8fd5d6ec4072ea992ab5aaae41", size = 639827, upload-time = "2025-12-02T19:47:46.447Z" }, + { url = "https://files.pythonhosted.org/packages/ab/34/88fde875eb1df89f870aa11aa1b0ab8e1cfca104cd8e9af802dd831c4068/habluetooth-5.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d56f3c73f5c74bfc3817a56343f5ef38ef4e584b825cf1d0d45e148b8fc2ce20", size = 699639, upload-time = "2025-12-02T19:47:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/74c352a234cf9faca007e1d43af79493de8ce19416b9bdddb9d2894d801b/habluetooth-5.8.0-cp313-cp313-manylinux_2_41_x86_64.whl", hash = "sha256:b651fa1d34a4086bd4bab27e528a0ea11dc310e806e86bd877c1b77a8b58ff7c", size = 698468, upload-time = "2025-12-02T19:30:51.699Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fe/8a28d94411c0cc19409091692ec893a603d71e66ada35c2c5db6c716f265/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:468e04e601a457d0097f57a2b16f65debfe4ed8b219270d52ebfba5cf0051b5c", size = 663619, upload-time = "2025-12-02T19:47:49.452Z" }, + { url = "https://files.pythonhosted.org/packages/7a/be/fb3d81a1cad478120f6ea7d5085eff98e1336331fbf1a7ceca11ca0a6982/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:91d34f82e36a2292aac5465db2c5b268d31df25303870af6687ad824275648bd", size = 647900, upload-time = "2025-12-02T19:47:50.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a0/1a738c6675d41193e80defe425f630c5a4db6da4ef5ee4cbce60fb998a28/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b25442a1ab5ba73c7da0726491a8a96637a546433f45d39f195415219bf11bef", size = 705834, upload-time = "2025-12-02T19:47:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/00/9c/a219114293d6d0b83d2c2b3cb505125de96edb73037b9f4f71c203e0b231/habluetooth-5.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12148fb6c41a4464c1336cd5bc127ed1c97cbfd0f2bedb2dbb16fecadd54e1c0", size = 568992, upload-time = "2025-12-02T19:47:56.921Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/5783e448e310f235550ef5b682d0518ea196e19f82484815943fee69640e/habluetooth-5.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7962af2e456676df1c09eb0e7a95b5c27696b76a82c2971a0dff25f017168453", size = 566035, upload-time = "2025-12-02T19:47:58.294Z" }, + { url = "https://files.pythonhosted.org/packages/97/60/80ea750a3af4d84455315eb038100e49419d8c5b8b14bb966a46ba8a8971/habluetooth-5.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84abc0819e462e6210620778ff9cf6ace8df04b6e9c04f41dd56abdaf9dade72", size = 666209, upload-time = "2025-12-02T19:47:59.656Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/0b45dfffbabfa5982e9721946e43f2fc7ff982c8d6598cb148c527797983/habluetooth-5.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a74652fe7ae5cb571138832cdf16945d17a9dc564e7c48e0c2173ca9796a7e90", size = 632911, upload-time = "2025-12-02T19:48:01.028Z" }, + { url = "https://files.pythonhosted.org/packages/25/0c/e3c9158eb988917f00fc93001a86bfccdf59c383ad95d90a5f897121bf98/habluetooth-5.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ddbe3e7c03d5df6d6064cd622d2235f1db2cb19282705d48393535ed1f3547b", size = 703092, upload-time = "2025-12-02T19:48:02.358Z" }, + { url = "https://files.pythonhosted.org/packages/1a/13/79858645697ebd3b87819d8b08a168d49532dccf38a0d8a66def0c6744b2/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c5f650be8b36e47016f418c73b36011754c742c0e4b8975b178a99fb40f94900", size = 674337, upload-time = "2025-12-02T19:48:03.643Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d3/55793994bd8500e459e5b5ecd1290b77c87d978df4a93a08abe031c353fd/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6c0ffe030bef47c268c658c2e0a1dcf8db3841727761f617953caee2831af6c", size = 637932, upload-time = "2025-12-02T19:48:05.126Z" }, + { url = "https://files.pythonhosted.org/packages/60/bf/118c76a8fda261ae42d2d15696c4277ec1a1c4da47c5ab030f7b50cce53b/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2248235680aa591bcba3fd626f859cb0fd89d84e03f1a596cf281d6802ba3d1b", size = 709054, upload-time = "2025-12-02T19:48:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/46/2a/e8d31c5a53c1aa9c2af9bbe1760ee1976b9642828d31b9b8b5aa4a01245b/habluetooth-5.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aded66250091ca6f05740317b295197d9387ca88bcec138e01f7fbd145d3406c", size = 1124956, upload-time = "2025-12-02T19:48:11.74Z" }, + { url = "https://files.pythonhosted.org/packages/39/a0/41ebef6913efcfe1ebe22c62423a013d2978c0a890bb849f4702ab8fbee5/habluetooth-5.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c3a5dc4f2e01931fce07018ccc46584483c44fc24369098ec8af253def6421db", size = 1123851, upload-time = "2025-12-02T19:48:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cf/68ab568ac01c054d32ecbd4790fceafd8587d22dd3382c441a905d993e29/habluetooth-5.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289af0d5a67bef760203d3cbdfa9e9aa9cc1f7da29112dbc50474a16f3d66735", size = 1275887, upload-time = "2025-12-02T19:48:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/3e68a905858e9945027b49f872bd6d6c92a916fd26f1c9c16e0b1056609b/habluetooth-5.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d96008b6c09d3617016801aa4dba74d065e118d9a4cb84e913e16fa3f379e597", size = 1192706, upload-time = "2025-12-02T19:48:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/d7483c7e57f0c34acb6bfb83fd32bac2c31ffbfc111b7511dffd1610c752/habluetooth-5.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e569435c561e79267dd6b2e0718d5411c1face47a810bb1d8fe42a7a9a9d8ee4", size = 1335960, upload-time = "2025-12-02T19:48:17.911Z" }, + { url = "https://files.pythonhosted.org/packages/27/6d/03fda3288621b9ba3221f37b1e2c877b2e73fd0f82591f751a058e83b2ef/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:60b5eed41f69ce6ca6c98df7c5f3ba1185737f2008225cd19e43bef0dab55074", size = 1294245, upload-time = "2025-12-02T19:48:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/d3/df/8d2df48c24c175f2dafa7026f8fb0f24793e69cc4e73f01f51ac5c5480c7/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:63b6d82223d64d19cd2feb23fafa7246b42574c628014ce21b010bb31fced5f1", size = 1215522, upload-time = "2025-12-02T19:48:21.197Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/5c0f86a9f35593102ca2a0a7fef6943e1400a0ea97fa9f296aec945b5992/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a106c1809516c86c7ff10a78ac68017dc23f73e5613735295eac628ca95a97b", size = 1350369, upload-time = "2025-12-02T19:48:23.095Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "acme", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "async-timeout", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "attrs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ciso8601", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "grpcio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "josepy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "litellm", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pycognito", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sentence-stream", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "snitun", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "voluptuous", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "yarl", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/63/132c981f9615db0681d2a08a6af3294b6568af833df7d5075368f41b9518/hass_nabucasa-1.7.0.tar.gz", hash = "sha256:a6d25a02a538e316625ea48c44e70def10687def7b05e08da91da72004dd3ea4", size = 106571, upload-time = "2025-12-03T12:24:40.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/9a/c3295290c3f616cd2271157971357e085a50d858a1bed6862ecf13a967b9/hass_nabucasa-1.7.0-py3-none-any.whl", hash = "sha256:2256974ce3f9b4f170ed19e902427557d08ea2ac2b88c5feafd6a572d3e552ed", size = 82666, upload-time = "2025-12-03T12:24:38.71Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, +] + +[[package]] +name = "home-assistant-bluetooth" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "habluetooth", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/0e/c05ee603cab1adb847a305bc8f1034cbdbc0a5d15169fcf68c0d6d21e33f/home_assistant_bluetooth-1.13.1.tar.gz", hash = "sha256:0ae0e2a8491cc762ee9e694b8bc7665f1e2b4618926f63969a23a2e3a48ce55e", size = 7607, upload-time = "2025-02-04T16:11:15.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/9b/9904cec885cc32c45e8c22cd7e19d9c342e30074fdb7c58f3d5b33ea1adb/home_assistant_bluetooth-1.13.1-py3-none-any.whl", hash = "sha256:cdf13b5b45f7744165677831e309ee78fbaf0c2866c6b5931e14d1e4e7dae5d7", size = 7915, upload-time = "2025-02-04T16:11:13.163Z" }, +] + +[[package]] +name = "homeassistant" +version = "2026.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiodns", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiohasupervisor", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiohttp-asyncmdnsresolver", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiohttp-cors", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiohttp-fast-zlib", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "aiozoneinfo", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "annotatedyaml", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "astral", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "async-interrupt", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "attrs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "awesomeversion", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "bcrypt", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "certifi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ciso8601", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "cronsim", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "fnv-hash-fast", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "hass-nabucasa", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "home-assistant-bluetooth", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ifaddr", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jinja2", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "lru-dict", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "orjson", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pillow", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "propcache", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "psutil-home-assistant", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyopenssl", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "python-slugify", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "securetar", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "standard-telnetlib", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ulid-transform", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "urllib3", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "uv", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "voluptuous", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "voluptuous-openapi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "voluptuous-serialize", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "yarl", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "zeroconf", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/45/abe9b04f358b056fc09bf4633a582171e145b98367ff206b28d36d888252/homeassistant-2026.1.3.tar.gz", hash = "sha256:82ce58c91d4ca1f48c57c4c37811a9f6478844472ccd919c2efb671ce611eee7", size = 30227545, upload-time = "2026-01-23T20:24:08.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/84/215bb2bbc4eca231d76e91f28198ce1d184e1f604f63b729bd4ab68aa4d7/homeassistant-2026.1.3-py3-none-any.whl", hash = "sha256:64f0c3e65749e1bc46705d2821f53f61469a1aee7a3934e22800970ddef46a9c", size = 50201024, upload-time = "2026-01-23T20:24:03.42Z" }, +] + [[package]] name = "homesec" version = "1.2.2" source = { editable = "." } dependencies = [ - { name = "aiohttp" }, - { name = "alembic" }, - { name = "anyio" }, - { name = "asyncpg" }, - { name = "dropbox" }, - { name = "fastapi" }, - { name = "fire" }, - { name = "greenlet" }, - { name = "opencv-python" }, - { name = "paho-mqtt" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyftpdlib" }, - { name = "pyyaml" }, - { name = "sqlalchemy" }, - { name = "ultralytics" }, - { name = "uvicorn" }, + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "alembic", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "anyio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "asyncpg", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "dropbox", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "fastapi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "fire", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "greenlet", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "opencv-python", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "paho-mqtt", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyftpdlib", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ultralytics", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "uvicorn", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] [package.dev-dependencies] dev = [ - { name = "asyncpg-stubs" }, - { name = "httpx" }, - { name = "ipykernel" }, - { name = "mypy" }, - { name = "nbstripout" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, - { name = "types-pyyaml" }, + { name = "asyncpg-stubs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ipykernel", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "mypy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "nbstripout", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-asyncio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-cov", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-homeassistant-custom-component", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ruff", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "types-pyyaml", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = ">=3.13.2" }, + { name = "aiohttp", specifier = ">=3.11.13" }, { name = "alembic", specifier = ">=1.13.0" }, { name = "anyio", specifier = ">=4.0.0" }, { name = "asyncpg", specifier = ">=0.29.0" }, @@ -1221,7 +1900,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.0.0" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pyftpdlib", specifier = ">=2.1.0" }, - { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "pyyaml", specifier = ">=6.0.2" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "ultralytics", specifier = ">=8.3.226" }, { name = "uvicorn", specifier = ">=0.30.0" }, @@ -1234,9 +1913,10 @@ dev = [ { name = "ipykernel", specifier = ">=7.1.0" }, { name = "mypy", specifier = ">=1.19.1" }, { name = "nbstripout", specifier = ">=0.8.2" }, - { name = "pytest", specifier = ">=9.0.2" }, - { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "pytest-homeassistant-custom-component", specifier = ">=0.13.300" }, { name = "ruff", specifier = ">=0.14.10" }, { name = "types-pyyaml", specifier = ">=6.0.12.20250915" }, ] @@ -1246,8 +1926,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "certifi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "h11", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -1259,16 +1939,37 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, + { name = "anyio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "certifi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "httpcore", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "idna", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "huggingface-hub" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "fsspec", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "hf-xet", marker = "(python_full_version >= '3.13.2' and platform_machine == 'AMD64' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and platform_machine == 'aarch64' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and platform_machine == 'amd64' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and platform_machine == 'arm64' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform != 'win32')" }, + { name = "httpx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "shellingham", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "tqdm", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typer-slim", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/3f/352efd52136bfd8aa9280c6d4a445869226ae2ccd49ddad4f62e90cfd168/huggingface_hub-1.3.7.tar.gz", hash = "sha256:5f86cd48f27131cdbf2882699cbdf7a67dd4cbe89a81edfdc31211f42e4a5fd1", size = 627537, upload-time = "2026-02-02T10:40:10.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/89/bfbfde252d649fae8d5f09b14a2870e5672ed160c1a6629301b3e5302621/huggingface_hub-1.3.7-py3-none-any.whl", hash = "sha256:8155ce937038fa3d0cb4347d752708079bc85e6d9eb441afb44c84bcf48620d2", size = 536728, upload-time = "2026-02-02T10:40:08.274Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1279,87 +1980,73 @@ wheels = [ ] [[package]] -name = "iniconfig" -version = "2.3.0" +name = "ifaddr" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, ] [[package]] -name = "ipykernel" -version = "7.1.0" +name = "importlib-metadata" +version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appnope", marker = "sys_platform == 'darwin'" }, - { name = "comm" }, - { name = "debugpy" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "matplotlib-inline" }, - { name = "nest-asyncio" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, + { name = "zipp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +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/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, + { 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 = "ipython" -version = "8.38.0" +name = "iniconfig" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", +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 = "ipykernel" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "appnope", marker = "python_full_version >= '3.13.2' and sys_platform == 'darwin'" }, + { name = "comm", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "debugpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ipython", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jupyter-client", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jupyter-core", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "nest-asyncio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "psutil", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyzmq", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "tornado", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "traitlets", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, ] [[package]] name = "ipython" version = "9.7.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.11'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, - { name = "jedi", marker = "python_full_version >= '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, - { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "stack-data", marker = "python_full_version >= '3.11'" }, - { name = "traitlets", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +dependencies = [ + { name = "decorator", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jedi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pexpect", marker = "python_full_version >= '3.13.2' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pygments", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "stack-data", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "traitlets", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } wheels = [ @@ -1371,7 +2058,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1383,7 +2070,7 @@ name = "jedi" version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "parso" }, + { name = "parso", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ @@ -1395,22 +2082,121 @@ name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe" }, + { name = "markupsafe", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "josepy" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ad/6f520aee9cc9618d33430380741e9ef859b2c560b1e7915e755c084f6bc0/josepy-2.2.0.tar.gz", hash = "sha256:74c033151337c854f83efe5305a291686cef723b4b970c43cfe7270cf4a677a9", size = 56500, upload-time = "2025-10-14T14:54:42.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/b2/b5caed897fbb1cc286c62c01feca977e08d99a17230ff3055b9a98eccf1d/josepy-2.2.0-py3-none-any.whl", hash = "sha256:63e9dd116d4078778c25ca88f880cc5d95f1cab0099bebe3a34c2e299f65d10b", size = 29211, upload-time = "2025-10-14T14:54:41.144Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, + { name = "attrs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "referencing", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "rpds-py", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ @@ -1422,7 +2208,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing" }, + { name = "referencing", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -1434,11 +2220,11 @@ name = "jupyter-client" version = "8.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-core" }, - { name = "python-dateutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, + { name = "jupyter-core", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyzmq", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "tornado", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "traitlets", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } wheels = [ @@ -1450,8 +2236,8 @@ name = "jupyter-core" version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs" }, - { name = "traitlets" }, + { name = "platformdirs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "traitlets", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ @@ -1475,8 +2261,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, - { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, @@ -1488,8 +2272,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, @@ -1501,8 +2283,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, - { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, @@ -1514,8 +2294,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, - { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, @@ -1527,7 +2305,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, @@ -1539,8 +2316,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, @@ -1552,18 +2327,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, - { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, - { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] @@ -1580,8 +2351,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/19/ab2f217e8ec509fca4ea9e2e5022b9f72c1a7b7195f5a5770d299df807ea/librt-0.7.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7766b57aeebaf3f1dac14fdd4a75c9a61f2ed56d8ebeefe4189db1cb9d2a3783", size = 179038, upload-time = "2025-12-15T16:51:10.538Z" }, { url = "https://files.pythonhosted.org/packages/10/1c/d40851d187662cf50312ebbc0b277c7478dd78dbaaf5ee94056f1d7f2f83/librt-0.7.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1c4c89fb01157dd0a3bfe9e75cd6253b0a1678922befcd664eca0772a4c6c979", size = 173502, upload-time = "2025-12-15T16:51:11.888Z" }, { url = "https://files.pythonhosted.org/packages/07/52/d5880835c772b22c38db18660420fa6901fd9e9a433b65f0ba9b0f4da764/librt-0.7.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7fa8beef580091c02b4fd26542de046b2abfe0aaefa02e8bcf68acb7618f2b3", size = 193570, upload-time = "2025-12-15T16:51:13.168Z" }, - { url = "https://files.pythonhosted.org/packages/f1/35/22d3c424b82f86ce019c0addadf001d459dfac8036aecc07fadc5c541053/librt-0.7.4-cp310-cp310-win32.whl", hash = "sha256:543c42fa242faae0466fe72d297976f3c710a357a219b1efde3a0539a68a6997", size = 42596, upload-time = "2025-12-15T16:51:14.422Z" }, - { url = "https://files.pythonhosted.org/packages/95/b1/e7c316ac5fe60ac1fdfe515198087205220803c4cf923ee63e1cb8380b17/librt-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:25cc40d8eb63f0a7ea4c8f49f524989b9df901969cb860a2bc0e4bad4b8cb8a8", size = 48972, upload-time = "2025-12-15T16:51:15.516Z" }, { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, @@ -1590,9 +2359,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" }, { url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" }, { url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" }, - { url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" }, - { url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" }, { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, @@ -1601,9 +2367,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, @@ -1612,9 +2375,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, - { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, @@ -1623,9 +2383,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, - { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, - { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, @@ -1634,17 +2391,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, - { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, - { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, - { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, ] [[package]] -name = "mako" +name = "license-expression" +version = "30.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/79/efb4637d56dcd265cb9329ab502be0e01f4daed80caffdc5065b4b7956df/license_expression-30.4.3.tar.gz", hash = "sha256:49f439fea91c4d1a642f9f2902b58db1d42396c5e331045f41ce50df9b40b1f2", size = 183031, upload-time = "2025-06-25T13:02:25.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ba/f6f6573bb21e51b838f1e7b0e8ef831d50db6d0530a5afaba700a34d9e12/license_expression-30.4.3-py3-none-any.whl", hash = "sha256:fd3db53418133e0eef917606623bc125fbad3d1225ba8d23950999ee87c99280", size = 117085, upload-time = "2025-06-25T13:02:24.503Z" }, +] + +[[package]] +name = "litellm" +version = "1.81.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "click", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "fastuuid", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "importlib-metadata", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jinja2", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jsonschema", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "openai", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "tiktoken", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "tokenizers", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/69/cfa8a1d68cd10223a9d9741c411e131aece85c60c29c1102d762738b3e5c/litellm-1.81.7.tar.gz", hash = "sha256:442ff38708383ebee21357b3d936e58938172bae892f03bc5be4019ed4ff4a17", size = 14039864, upload-time = "2026-02-03T19:43:10.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/95/8cecc7e6377171e4ac96f23d65236af8706d99c1b7b71a94c72206672810/litellm-1.81.7-py3-none-any.whl", hash = "sha256:58466c88c3289c6a3830d88768cf8f307581d9e6c87861de874d1128bb2de90d", size = 12254178, upload-time = "2026-02-03T19:43:08.035Z" }, +] + +[[package]] +name = "lru-dict" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/fc/d0de12343c9f132b10c7efe40951dfb6c3cfba328941ecf4c198e6bfdd78/lru_dict-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4073333894db9840f066226d50e6f914a2240711c87d60885d8c940b69a6673f", size = 17708, upload-time = "2023-11-06T01:38:35.233Z" }, + { url = "https://files.pythonhosted.org/packages/75/56/af1cae207a5c4f1ada20a9bde92d7d953404274f499dd8fe3f4ece91eefe/lru_dict-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0ad6361e4dd63b47b2fc8eab344198f37387e1da3dcfacfee19bafac3ec9f1eb", size = 11017, upload-time = "2023-11-06T01:38:36.948Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/1dcaf052b4d039b85af8a8df9090c10923acc4bed448051ce791376313f3/lru_dict-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c637ab54b8cd9802fe19b260261e38820d748adf7606e34045d3c799b6dde813", size = 11322, upload-time = "2023-11-06T01:38:38.208Z" }, + { url = "https://files.pythonhosted.org/packages/14/d4/77553cb43a2e50c3a5bb6338fe4ba3415638a99a5c8404a4ec13ab7cec52/lru_dict-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fce5f95489ca1fc158cc9fe0f4866db9cec82c2be0470926a9080570392beaf", size = 30597, upload-time = "2023-11-06T01:38:39.647Z" }, + { url = "https://files.pythonhosted.org/packages/14/28/184d94fcd121a0dc775fa423bf05b886ae42fc081cbd693540068cf06ece/lru_dict-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2bf2e24cf5f19c3ff69bf639306e83dced273e6fa775b04e190d7f5cd16f794", size = 31871, upload-time = "2023-11-06T01:38:41.01Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/6b49fa5fccc7b2d28fe254c48c64323741c98334e4fe41e4694fa049c208/lru_dict-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e90059f7701bef3c4da073d6e0434a9c7dc551d5adce30e6b99ef86b186f4b4a", size = 28651, upload-time = "2023-11-06T01:38:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/c3a4922421c8e5eb6fa1fdf5f56a7e01270a141a4f5f645d5ed6931b490f/lru_dict-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ecb7ae557239c64077e9b26a142eb88e63cddb104111a5122de7bebbbd00098", size = 30277, upload-time = "2023-11-06T01:38:43.567Z" }, + { url = "https://files.pythonhosted.org/packages/f9/10/a15f70c5c36d46adba72850e64b075c6a118d2a9ee1ce7f2af2f4a419401/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6af36166d22dba851e06a13e35bbf33845d3dd88872e6aebbc8e3e7db70f4682", size = 34793, upload-time = "2023-11-06T01:38:44.938Z" }, + { url = "https://files.pythonhosted.org/packages/56/e3/9901f9165a8c2d650bb84ae6ba371fa635e35e8b1dfb1aff2bd7be4cfd3a/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ee38d420c77eed548df47b7d74b5169a98e71c9e975596e31ab808e76d11f09", size = 33414, upload-time = "2023-11-06T01:38:46.366Z" }, + { url = "https://files.pythonhosted.org/packages/be/27/6323b27dd42914c3ee511631d976d49247699ef0ec6fd468a5d4eef3930e/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0e1845024c31e6ff246c9eb5e6f6f1a8bb564c06f8a7d6d031220044c081090b", size = 36345, upload-time = "2023-11-06T01:38:47.908Z" }, + { url = "https://files.pythonhosted.org/packages/76/14/b7d9009acf698e6f5d656e35776cedd3fd09755db5b09ff372d4e2667c4e/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ca5474b1649555d014be1104e5558a92497509021a5ba5ea6e9b492303eb66b", size = 34619, upload-time = "2023-11-06T01:38:49.387Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c9/6fac0cb67160f0efa3cc76a6a7d04d5e21a516eeb991ebba08f4f8f01ec5/lru_dict-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20c595764695d20bdc3ab9b582e0cc99814da183544afb83783a36d6741a0dac", size = 17750, upload-time = "2023-11-06T01:38:52.667Z" }, + { url = "https://files.pythonhosted.org/packages/61/14/f90dee4bc547ae266dbeffd4e11611234bb6af511dea48f3bc8dac1de478/lru_dict-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9b30a8f50c3fa72a494eca6be5810a1b5c89e4f0fda89374f0d1c5ad8d37d51", size = 11055, upload-time = "2023-11-06T01:38:53.798Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/a0ae20525f9d52f62ac0def47935f8a2b3b6fcd2c145218b9a27fc1fb910/lru_dict-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9710737584650a4251b9a566cbb1a86f83437adb209c9ba43a4e756d12faf0d7", size = 11330, upload-time = "2023-11-06T01:38:54.847Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c6/8c2b81b61e5206910c81b712500736227289aefe4ccfb36137aa21807003/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b84c321ae34f2f40aae80e18b6fa08b31c90095792ab64bb99d2e385143effaa", size = 31793, upload-time = "2023-11-06T01:38:56.163Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d7/af9733f94df67a2e9e31ef47d4c41aff1836024f135cdbda4743eb628452/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eed24272b4121b7c22f234daed99899817d81d671b3ed030c876ac88bc9dc890", size = 33090, upload-time = "2023-11-06T01:38:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6e/5b09b069a70028bcf05dbdc57a301fbe8b3bafecf916f2ed5a3065c79a71/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd13af06dab7c6ee92284fd02ed9a5613a07d5c1b41948dc8886e7207f86dfd", size = 29795, upload-time = "2023-11-06T01:38:58.278Z" }, + { url = "https://files.pythonhosted.org/packages/21/92/4690daefc2602f7c3429ecf54572d37a9e3c372d370344d2185daa4d5ecc/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1efc59bfba6aac33684d87b9e02813b0e2445b2f1c444dae2a0b396ad0ed60c", size = 31586, upload-time = "2023-11-06T01:38:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/0a29a91087196b02f278d8765120ee4e7486f1f72a4c505fd1cd3109e627/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfaf75ac574447afcf8ad998789071af11d2bcf6f947643231f692948839bd98", size = 36662, upload-time = "2023-11-06T01:39:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/8d56c514cd2333b652bd44c8f1962ab986cbe68e8ad7258c9e0f360cddb6/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c95f8751e2abd6f778da0399c8e0239321d560dbc58cb063827123137d213242", size = 35118, upload-time = "2023-11-06T01:39:01.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9a/c7a175d10d503b86974cb07141ca175947145dd1c7370fcda86fbbcaf326/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:abd0c284b26b5c4ee806ca4f33ab5e16b4bf4d5ec9e093e75a6f6287acdde78e", size = 38198, upload-time = "2023-11-06T01:39:03.306Z" }, + { url = "https://files.pythonhosted.org/packages/fd/59/2e5086c8e8a05a7282a824a2a37e3c45cd5714e7b83d8bc0267cb3bb5b4f/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a47740652b25900ac5ce52667b2eade28d8b5fdca0ccd3323459df710e8210a", size = 36542, upload-time = "2023-11-06T01:39:04.751Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174, upload-time = "2023-11-06T01:39:07.923Z" }, + { url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742, upload-time = "2023-11-06T01:39:08.871Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079, upload-time = "2023-11-06T01:39:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469, upload-time = "2023-11-06T01:39:11.091Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496, upload-time = "2023-11-06T01:39:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914, upload-time = "2023-11-06T01:39:13.395Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241, upload-time = "2023-11-06T01:39:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320, upload-time = "2023-11-06T01:39:15.875Z" }, + { url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054, upload-time = "2023-11-06T01:39:17.063Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613, upload-time = "2023-11-06T01:39:18.136Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143, upload-time = "2023-11-06T01:39:19.571Z" }, + { url = "https://files.pythonhosted.org/packages/78/8b/4b7af0793512af8b0d814b3b08ccecb08f313594866cfe9aabf77f642934/lru_dict-1.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f8f7824db5a64581180ab9d09842e6dd9fcdc46aac9cb592a0807cd37ea55680", size = 10060, upload-time = "2023-11-06T01:39:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/47/04/e310269b8bbb5718025d0375d8189551f10f1ef057df2b21e4bc5714fb56/lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acd04b7e7b0c0c192d738df9c317093335e7282c64c9d1bb6b7ebb54674b4e24", size = 13299, upload-time = "2023-11-06T01:39:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d2/246d375c89a71637fe193f260c500537e5dc11cf3a2b5144669bfef69295/lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5c20f236f27551e3f0adbf1a987673fb1e9c38d6d284502cd38f5a3845ef681", size = 13142, upload-time = "2023-11-06T01:39:55.631Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/56fead7639a41d507eac5163a81f18c7f47a8c1feb3046d20a9c8bb56e56/lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca3703ff03b03a1848c563bc2663d0ad813c1cd42c4d9cf75b623716d4415d9a", size = 12839, upload-time = "2023-11-06T01:39:57.003Z" }, +] + +[[package]] +name = "mako" version = "1.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe" }, + { name = "markupsafe", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } wheels = [ @@ -1665,9 +2499,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, @@ -1676,9 +2507,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -1687,9 +2515,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, @@ -1698,9 +2523,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, @@ -1709,9 +2531,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, @@ -1720,9 +2539,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, @@ -1731,9 +2547,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mashumaro" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/7e/654ae97e00c450a5482eee88a47ad7c4a3749a231accb60d6c6756847251/mashumaro-3.19.tar.gz", hash = "sha256:3ef88c23d7a4477092184dace6309da212f115eba9029c89fb93b80ae424692f", size = 191291, upload-time = "2026-02-03T19:17:48.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/1a7a8e7b5e7f12e44d9ca8873a7d510541e9b97d0966a51455ab5dfe6fb4/mashumaro-3.19-py3-none-any.whl", hash = "sha256:14e5fea73ee92479c2fddf634eadd6502a8bd7afe57b7912fa11b5fb51e554c7", size = 94766, upload-time = "2026-02-03T19:17:46.228Z" }, ] [[package]] @@ -1741,16 +2566,15 @@ name = "matplotlib" version = "3.10.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, + { name = "contourpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "cycler", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "fonttools", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "kiwisolver", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pillow", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyparsing", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } wheels = [ @@ -1759,49 +2583,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/3c/5692a2d9a5ba848fda3f48d2b607037df96460b941a59ef236404b39776b/matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297", size = 8680503, upload-time = "2025-10-09T00:26:10.607Z" }, { url = "https://files.pythonhosted.org/packages/ab/a0/86ace53c48b05d0e6e9c127b2ace097434901f3e7b93f050791c8243201a/matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42", size = 9514982, upload-time = "2025-10-09T00:26:12.594Z" }, { url = "https://files.pythonhosted.org/packages/a6/81/ead71e2824da8f72640a64166d10e62300df4ae4db01a0bac56c5b39fa51/matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7", size = 9566429, upload-time = "2025-10-09T00:26:14.758Z" }, - { url = "https://files.pythonhosted.org/packages/65/7d/954b3067120456f472cce8fdcacaf4a5fcd522478db0c37bb243c7cb59dd/matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3", size = 8108174, upload-time = "2025-10-09T00:26:17.015Z" }, { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, - { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, - { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, - { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, - { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, - { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, - { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" }, { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" }, { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" }, { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" }, { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" }, - { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" }, { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" }, { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" }, { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" }, { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" }, { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, - { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, { url = "https://files.pythonhosted.org/packages/1e/6c/a9bcf03e9afb2a873e0a5855f79bce476d1023f26f8212969f2b7504756c/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537", size = 8241204, upload-time = "2025-10-09T00:27:48.806Z" }, { url = "https://files.pythonhosted.org/packages/5b/fd/0e6f5aa762ed689d9fa8750b08f1932628ffa7ed30e76423c399d19407d2/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657", size = 8104607, upload-time = "2025-10-09T00:27:50.876Z" }, { url = "https://files.pythonhosted.org/packages/b9/a9/21c9439d698fac5f0de8fc68b2405b738ed1f00e1279c76f2d9aa5521ead/matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b", size = 8682257, upload-time = "2025-10-09T00:27:52.597Z" }, @@ -1815,13 +2626,19 @@ name = "matplotlib-inline" version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "traitlets" }, + { name = "traitlets", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] +[[package]] +name = "mock-open" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/02/cef85a80ff6d3092a458448c46816656d1c532afd45aeeeb8f50a84aed35/mock-open-1.4.0.tar.gz", hash = "sha256:c3ecb6b8c32a5899a4f5bf4495083b598b520c698bba00e1ce2ace6e9c239100", size = 12127, upload-time = "2020-04-15T15:26:51.234Z" } + [[package]] name = "mpmath" version = "1.3.0" @@ -1835,9 +2652,6 @@ wheels = [ name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, @@ -1855,9 +2669,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, - { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, - { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, @@ -1873,9 +2684,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, @@ -1891,9 +2699,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, @@ -1909,9 +2714,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, @@ -1927,9 +2729,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, @@ -1945,9 +2744,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, @@ -1963,9 +2759,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, ] @@ -1974,11 +2767,10 @@ 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" }, + { name = "librt", marker = "python_full_version >= '3.13.2' and platform_python_implementation != 'PyPy' and sys_platform != 'win32'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pathspec", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] 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 = [ @@ -1987,31 +2779,26 @@ wheels = [ { 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/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] @@ -2029,10 +2816,10 @@ name = "nbformat" version = "5.10.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastjsonschema" }, - { name = "jsonschema" }, - { name = "jupyter-core" }, - { name = "traitlets" }, + { name = "fastjsonschema", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jsonschema", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jupyter-core", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "traitlets", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } wheels = [ @@ -2044,7 +2831,7 @@ name = "nbstripout" version = "0.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nbformat" }, + { name = "nbformat", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/09/cea5360a0788f94337d1b65c313ff0a448fb6a444b65cab89378eb8f7d5c/nbstripout-0.8.2.tar.gz", hash = "sha256:2876530eb684bf93a5b48fe6d92b2163f78d040721c76b37d5b9e1514d38fc69", size = 27747, upload-time = "2025-11-16T17:38:55.6Z" } wheels = [ @@ -2060,29 +2847,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, -] - [[package]] name = "networkx" version = "3.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", -] sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, @@ -2090,64 +2858,64 @@ wheels = [ [[package]] name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158, upload-time = "2025-07-24T20:24:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817, upload-time = "2025-07-24T20:25:07.746Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606, upload-time = "2025-07-24T20:25:18.84Z" }, + { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652, upload-time = "2025-07-24T20:25:40.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816, upload-time = "2025-07-24T20:26:05.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512, upload-time = "2025-07-24T20:26:30.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947, upload-time = "2025-07-24T20:26:58.24Z" }, + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338, upload-time = "2025-07-24T20:57:54.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776, upload-time = "2025-07-24T20:58:16.303Z" }, + { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882, upload-time = "2025-07-24T20:58:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405, upload-time = "2025-07-24T20:58:37.341Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651, upload-time = "2025-07-24T20:58:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166, upload-time = "2025-07-24T21:28:56.38Z" }, ] [[package]] @@ -2187,7 +2955,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -2198,7 +2966,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -2225,9 +2993,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" }, - { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -2238,7 +3006,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -2285,20 +3053,101 @@ wheels = [ ] [[package]] -name = "opencv-python" -version = "4.12.0.88" +name = "openai" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "anyio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "distro", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jiter", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sniffio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "tqdm", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/71/25c98e634b6bdeca4727c7f6d6927b056080668c5008ad3c8fc9e7f8f6ec/opencv-python-4.12.0.88.tar.gz", hash = "sha256:8b738389cede219405f6f3880b851efa3415ccd674752219377353f017d2994d", size = 95373294, upload-time = "2025-07-07T09:20:52.389Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/68/3da40142e7c21e9b1d4e7ddd6c58738feb013203e6e4b803d62cdd9eb96b/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:f9a1f08883257b95a5764bf517a32d75aec325319c8ed0f89739a57fae9e92a5", size = 37877727, upload-time = "2025-07-07T09:13:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/33/7c/042abe49f58d6ee7e1028eefc3334d98ca69b030e3b567fe245a2b28ea6f/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:812eb116ad2b4de43ee116fcd8991c3a687f099ada0b04e68f64899c09448e81", size = 57326471, upload-time = "2025-07-07T09:13:41.26Z" }, - { url = "https://files.pythonhosted.org/packages/62/3a/440bd64736cf8116f01f3b7f9f2e111afb2e02beb2ccc08a6458114a6b5d/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:51fd981c7df6af3e8f70b1556696b05224c4e6b6777bdd2a46b3d4fb09de1a92", size = 45887139, upload-time = "2025-07-07T09:13:50.761Z" }, - { url = "https://files.pythonhosted.org/packages/68/1f/795e7f4aa2eacc59afa4fb61a2e35e510d06414dd5a802b51a012d691b37/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:092c16da4c5a163a818f120c22c5e4a2f96e0db4f24e659c701f1fe629a690f9", size = 67041680, upload-time = "2025-07-07T09:14:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/02/96/213fea371d3cb2f1d537612a105792aa0a6659fb2665b22cad709a75bd94/opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357", size = 30284131, upload-time = "2025-07-07T09:14:08.819Z" }, - { url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" }, + { url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.13.0.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d7/133d5756aef78090f4d8dd4895793aed24942dec6064a15375cfac9175fc/opencv_python-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:58803f8b05b51d8a785e2306d83b44173b32536f980342f3bc76d8c122b5938d", size = 46020278, upload-time = "2026-01-18T08:57:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/7b/65/3b8cdbe13fa2436695d00e1d8c1ddf5edb4050a93436f34ed867233d1960/opencv_python-4.13.0.90-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:a5354e8b161409fce7710ba4c1cfe88b7bb460d97f705dc4e714a1636616f87d", size = 32568376, upload-time = "2026-01-18T08:58:47.19Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/e4d7c165e678563f49505d3d2811fcc16011e929cd00bc4b0070c7ee82b0/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d557cbf0c7818081c9acf56585b68e781af4f00638971f75eaa3de70904a6314", size = 47685110, upload-time = "2026-01-18T08:59:58.045Z" }, + { url = "https://files.pythonhosted.org/packages/cf/02/d9b73dbce28712204e85ae4c1e179505e9a771f95b33743a97e170caedde/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9911581e37b24169e4842069ff01d6645ea2bc4af7e10a022d9ebe340fd035ec", size = 70460479, upload-time = "2026-01-18T09:01:16.377Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1c/87fa71968beb71481ed359e21772061ceff7c9b45a61b3e7daa71e5b0b66/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1150b8f1947761b848bbfa9c96ceba8877743ffef157c08a04af6f7717ddd709", size = 46707819, upload-time = "2026-01-18T09:02:48.049Z" }, + { url = "https://files.pythonhosted.org/packages/af/16/915a94e5b537c328fa3e96b769c7d4eed3b67d1be978e0af658a3d3faed8/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:d6716f16149b04eea52f953b8ca983d60dd9cd4872c1fd5113f6e2fcebb90e93", size = 72926629, upload-time = "2026-01-18T09:04:29.23Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/64/4a3cef001c6cd9c64256348d4c13a7b09b857e3e1cbb5185917df67d8ced/orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7", size = 238600, upload-time = "2025-08-26T17:44:36.875Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/0c8c87f54f79d051485903dc46226c4d3220b691a151769156054df4562b/orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120", size = 123526, upload-time = "2025-08-26T17:44:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/249497e861f2d438f45b3ab7b7b361484237414945169aa285608f9f7019/orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467", size = 128075, upload-time = "2025-08-26T17:44:40.672Z" }, + { url = "https://files.pythonhosted.org/packages/e5/64/00485702f640a0fd56144042a1ea196469f4a3ae93681871564bf74fa996/orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873", size = 130483, upload-time = "2025-08-26T17:44:41.788Z" }, + { url = "https://files.pythonhosted.org/packages/64/81/110d68dba3909171bf3f05619ad0cf187b430e64045ae4e0aa7ccfe25b15/orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a", size = 132539, upload-time = "2025-08-26T17:44:43.12Z" }, + { url = "https://files.pythonhosted.org/packages/79/92/dba25c22b0ddfafa1e6516a780a00abac28d49f49e7202eb433a53c3e94e/orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b", size = 135390, upload-time = "2025-08-26T17:44:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/1d/ca2230fd55edbd87b58a43a19032d63a4b180389a97520cc62c535b726f9/orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf", size = 132966, upload-time = "2025-08-26T17:44:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b9/96bbc8ed3e47e52b487d504bd6861798977445fbc410da6e87e302dc632d/orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4", size = 131349, upload-time = "2025-08-26T17:44:46.862Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3c/418fbd93d94b0df71cddf96b7fe5894d64a5d890b453ac365120daec30f7/orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc", size = 404087, upload-time = "2025-08-26T17:44:48.079Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a9/2bfd58817d736c2f63608dec0c34857339d423eeed30099b126562822191/orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569", size = 146067, upload-time = "2025-08-26T17:44:49.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/ba/29023771f334096f564e48d82ed855a0ed3320389d6748a9c949e25be734/orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6", size = 135506, upload-time = "2025-08-26T17:44:50.558Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/360674cd817faef32e49276187922a946468579fcaf37afdfb6c07046e92/orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f", size = 238238, upload-time = "2025-08-26T17:44:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/05/3d/5fa9ea4b34c1a13be7d9046ba98d06e6feb1d8853718992954ab59d16625/orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91", size = 127713, upload-time = "2025-08-26T17:44:55.596Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/e18367823925e00b1feec867ff5f040055892fc474bf5f7875649ecfa586/orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904", size = 123241, upload-time = "2025-08-26T17:44:57.185Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bd/3c66b91c4564759cf9f473251ac1650e446c7ba92a7c0f9f56ed54f9f0e6/orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6", size = 127895, upload-time = "2025-08-26T17:44:58.349Z" }, + { url = "https://files.pythonhosted.org/packages/82/b5/dc8dcd609db4766e2967a85f63296c59d4722b39503e5b0bf7fd340d387f/orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d", size = 130303, upload-time = "2025-08-26T17:44:59.491Z" }, + { url = "https://files.pythonhosted.org/packages/48/c2/d58ec5fd1270b2aa44c862171891adc2e1241bd7dab26c8f46eb97c6c6f1/orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038", size = 132366, upload-time = "2025-08-26T17:45:00.654Z" }, + { url = "https://files.pythonhosted.org/packages/73/87/0ef7e22eb8dd1ef940bfe3b9e441db519e692d62ed1aae365406a16d23d0/orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb", size = 135180, upload-time = "2025-08-26T17:45:02.424Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/e5bf7b70883f374710ad74faf99bacfc4b5b5a7797c1d5e130350e0e28a3/orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2", size = 132741, upload-time = "2025-08-26T17:45:03.663Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0c/4577fd860b6386ffaa56440e792af01c7882b56d2766f55384b5b0e9d39b/orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55", size = 131104, upload-time = "2025-08-26T17:45:04.939Z" }, + { url = "https://files.pythonhosted.org/packages/66/4b/83e92b2d67e86d1c33f2ea9411742a714a26de63641b082bdbf3d8e481af/orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1", size = 403887, upload-time = "2025-08-26T17:45:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e5/9eea6a14e9b5ceb4a271a1fd2e1dec5f2f686755c0fab6673dc6ff3433f4/orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824", size = 145855, upload-time = "2025-08-26T17:45:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/8d4f5ad0c80ba9bf8ac4d0fc71f93a7d0dc0844989e645e2074af376c307/orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f", size = 135361, upload-time = "2025-08-26T17:45:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" }, + { url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" }, + { url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" }, + { url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, + { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, + { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" }, + { url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" }, + { url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" }, ] [[package]] @@ -2342,7 +3191,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'win32'" }, + { name = "ptyprocess", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2363,9 +3212,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/1c/880921e98f525b9b44ce747ad1ea8f73fd7e992bafe3ca5e5644bf433dea/pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", size = 7026074, upload-time = "2025-10-15T18:21:37.219Z" }, { url = "https://files.pythonhosted.org/packages/28/03/96f718331b19b355610ef4ebdbbde3557c726513030665071fd025745671/pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", size = 6448852, upload-time = "2025-10-15T18:21:39.168Z" }, { url = "https://files.pythonhosted.org/packages/3a/a0/6a193b3f0cc9437b122978d2c5cbce59510ccf9a5b48825096ed7472da2f/pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", size = 7117058, upload-time = "2025-10-15T18:21:40.997Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c4/043192375eaa4463254e8e61f0e2ec9a846b983929a8d0a7122e0a6d6fff/pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", size = 6295431, upload-time = "2025-10-15T18:21:42.518Z" }, - { url = "https://files.pythonhosted.org/packages/92/c6/c2f2fc7e56301c21827e689bb8b0b465f1b52878b57471a070678c0c33cd/pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", size = 7000412, upload-time = "2025-10-15T18:21:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d2/5f675067ba82da7a1c238a73b32e3fd78d67f9d9f80fbadd33a40b9c0481/pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", size = 2435903, upload-time = "2025-10-15T18:21:46.29Z" }, { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, @@ -2374,9 +3220,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, - { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, @@ -2385,12 +3228,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, - { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, @@ -2399,9 +3236,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, - { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, - { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, @@ -2410,12 +3244,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, - { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, - { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, - { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, - { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, @@ -2424,9 +3252,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, - { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, @@ -2435,16 +3260,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "pip" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/c2/65686a7783a7c27a329706207147e82f23c41221ee9ae33128fc331670a0/pip-26.0.tar.gz", hash = "sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa", size = 1812654, upload-time = "2026-01-31T01:40:54.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/00/5ac7aa77688ec4d34148b423d34dc0c9bc4febe0d872a9a1ad9860b2f6f1/pip-26.0-py3-none-any.whl", hash = "sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754", size = 1787564, upload-time = "2026-01-31T01:40:52.252Z" }, +] + +[[package]] +name = "pipdeptree" +version = "2.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pip", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/ef/9158ee3b28274667986d39191760c988a2de22c6321be1262e21c8a19ccf/pipdeptree-2.26.1.tar.gz", hash = "sha256:92a8f37ab79235dacb46af107e691a1309ca4a429315ba2a1df97d1cd56e27ac", size = 41024, upload-time = "2025-04-20T03:27:42.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/a5/f9f143b420e53a296869636d1c3bdc144be498ca3136a113f52b53ea2b02/pipdeptree-2.26.1-py3-none-any.whl", hash = "sha256:3849d62a2ed641256afac3058c4f9b85ac4a47e9d8c991ee17a8f3d230c5cffb", size = 32802, upload-time = "2025-04-20T03:27:40.413Z" }, ] [[package]] @@ -2479,7 +3322,7 @@ name = "polars" version = "1.35.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "polars-runtime-32" }, + { name = "polars-runtime-32", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9b/5b/3caad788d93304026cbf0ab4c37f8402058b64a2f153b9c62f8b30f5d2ee/polars-1.35.1.tar.gz", hash = "sha256:06548e6d554580151d6ca7452d74bceeec4640b5b9261836889b8e68cfd7a62e", size = 694881, upload-time = "2025-10-30T12:12:52.294Z" } wheels = [ @@ -2496,8 +3339,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/70/a0733568b3533481924d2ce68b279ab3d7334e5fa6ed259f671f650b7c5e/polars_runtime_32-1.35.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c2232f9cf05ba59efc72d940b86c033d41fd2d70bf2742e8115ed7112a766aa9", size = 36701908, upload-time = "2025-10-30T12:12:02.166Z" }, { url = "https://files.pythonhosted.org/packages/46/54/6c09137bef9da72fd891ba58c2962cc7c6c5cad4649c0e668d6b344a9d7b/polars_runtime_32-1.35.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42f9837348557fd674477ea40a6ac8a7e839674f6dd0a199df24be91b026024c", size = 41317692, upload-time = "2025-10-30T12:12:04.928Z" }, { url = "https://files.pythonhosted.org/packages/22/55/81c5b266a947c339edd7fbaa9e1d9614012d02418453f48b76cc177d3dd9/polars_runtime_32-1.35.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:c873aeb36fed182d5ebc35ca17c7eb193fe83ae2ea551ee8523ec34776731390", size = 37853058, upload-time = "2025-10-30T12:12:08.342Z" }, - { url = "https://files.pythonhosted.org/packages/6c/58/be8b034d559eac515f52408fd6537be9bea095bc0388946a4e38910d3d50/polars_runtime_32-1.35.1-cp39-abi3-win_amd64.whl", hash = "sha256:35cde9453ca7032933f0e58e9ed4388f5a1e415dd0db2dd1e442c81d815e630c", size = 41289554, upload-time = "2025-10-30T12:12:11.104Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7f/e0111b9e2a1169ea82cde3ded9c92683e93c26dfccd72aee727996a1ac5b/polars_runtime_32-1.35.1-cp39-abi3-win_arm64.whl", hash = "sha256:fd77757a6c9eb9865c4bfb7b07e22225207c6b7da382bd0b9bd47732f637105d", size = 36958878, upload-time = "2025-10-30T12:12:15.206Z" }, ] [[package]] @@ -2505,7 +3346,7 @@ name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wcwidth" }, + { name = "wcwidth", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ @@ -2530,9 +3371,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, @@ -2545,9 +3383,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, @@ -2560,9 +3395,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, @@ -2575,9 +3407,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, @@ -2590,9 +3419,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, @@ -2605,9 +3431,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, @@ -2620,9 +3443,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] @@ -2636,20 +3456,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, - { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, - { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, - { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +] + +[[package]] +name = "psutil-home-assistant" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/4f/32a51f53d645044740d0513a6a029d782b35bdc51a55ea171ce85034f5b7/psutil-home-assistant-0.0.1.tar.gz", hash = "sha256:ebe4f3a98d76d93a3140da2823e9ef59ca50a59761fdc453b30b4407c4c1bdb8", size = 6045, upload-time = "2022-08-25T14:28:39.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/48/8a0acb683d1fee78b966b15e78143b673154abb921061515254fb573aacd/psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41", size = 6300, upload-time = "2022-08-25T14:28:38.083Z" }, ] [[package]] @@ -2675,7 +3501,7 @@ name = "pyasynchat" version = "1.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyasyncore", marker = "python_full_version >= '3.12'" }, + { name = "pyasyncore", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/fd/aacc6309abcc5a388c66915829cd8175daccac583828fde40a1eea5768e4/pyasynchat-1.0.4.tar.gz", hash = "sha256:3f5333df649e46c56d48c57e6a4b7163fd07f626bfd884e22ef373ab3c3a4670", size = 9747, upload-time = "2024-02-28T08:55:35.794Z" } wheels = [ @@ -2691,6 +3517,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/46/aaa0999302d7a584a033ec23b6ca21a452cf9c7f6d8dce8d174ac407eb3f/pyasyncore-1.0.4-py3-none-any.whl", hash = "sha256:9e5f6dc9dc057c56370b7a5cdb4c4670fd4b0556de2913ed1f428cd6a5366895", size = 10032, upload-time = "2024-02-28T08:49:45.696Z" }, ] +[[package]] +name = "pycares" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/ad/9d1e96486d2eb5a2672c4d9a2dd372d015b8d7a332c6ac2722c4c8e6bbbf/pycares-4.11.0.tar.gz", hash = "sha256:c863d9003ca0ce7df26429007859afd2a621d3276ed9fef154a9123db9252557", size = 654473, upload-time = "2025-09-09T15:18:21.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/54ea6c4dbfa7a225124672c7892ef005f4836238c7342af341eeedeb316b/pycares-4.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87dab618fe116f1936f8461df5970fcf0befeba7531a36b0a86321332ff9c20b", size = 145874, upload-time = "2025-09-09T15:16:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/be/b2/7fd2d2e9bb58a0ab2813ae4fb83679bc00855f4763c12dd40da88801a137/pycares-4.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3db6b6439e378115572fa317053f3ee6eecb39097baafe9292320ff1a9df73e3", size = 141821, upload-time = "2025-09-09T15:16:14.994Z" }, + { url = "https://files.pythonhosted.org/packages/00/88/ec6e72982d9c51867e35570d970ee83c223d9b5df0128c9f995bf3270e2a/pycares-4.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:742fbaa44b418237dbd6bf8cdab205c98b3edb334436a972ad341b0ea296fb47", size = 642642, upload-time = "2025-09-09T15:16:16.64Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b3/cb38188b232f6e41f14d9c157fe4f92a45b8cd3e0d117d70df281dcf4d5a/pycares-4.11.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d2a3526dbf6cb01b355e8867079c9356a8df48706b4b099ac0bf59d4656e610d", size = 690268, upload-time = "2025-09-09T15:16:17.864Z" }, + { url = "https://files.pythonhosted.org/packages/3b/18/b7f967d745f401f7d1b0f964a9102b3dfd1a86b4ac385d41a2032ab23015/pycares-4.11.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:3d5300a598ad48bbf169fba1f2b2e4cf7ab229e7c1a48d8c1166f9ccf1755cb3", size = 682138, upload-time = "2025-09-09T15:16:19.474Z" }, + { url = "https://files.pythonhosted.org/packages/27/30/291338fa5e745dba3cee06969343ae55334fa1692afce3851901f2025712/pycares-4.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:066f3caa07c85e1a094aebd9e7a7bb3f3b2d97cff2276665693dd5c0cc81cf84", size = 643965, upload-time = "2025-09-09T15:16:20.789Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c0/de6d9bb2fb917f669c2834d7e00ae593c1b8d84c850cb4a6fa5096ea0612/pycares-4.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dcd4a7761fdfb5aaac88adad0a734dd065c038f5982a8c4b0dd28efa0bd9cc7c", size = 627041, upload-time = "2025-09-09T15:16:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f1/c7c0f9d23621bb192c29481e10d4f76b7d1194943bc220ae9584d14d9f49/pycares-4.11.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:83a7401d7520fa14b00d85d68bcca47a0676c69996e8515d53733972286f9739", size = 673289, upload-time = "2025-09-09T15:16:24.017Z" }, + { url = "https://files.pythonhosted.org/packages/33/51/902a965d771ccb899fd7ed8ae4bde5cd4ba2cac8e4a7e41ee131837416f0/pycares-4.11.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:66c310773abe42479302abf064832f4a37c8d7f788f4d5ee0d43cbad35cf5ff4", size = 656639, upload-time = "2025-09-09T15:16:25.64Z" }, + { url = "https://files.pythonhosted.org/packages/97/1d/3d961dacbf7b132fbbfa76fdbb19e5b9a109b3b53761d87c09dde9f9a832/pycares-4.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95bc81f83fadb67f7f87914f216a0e141555ee17fd7f56e25aa0cc165e99e53b", size = 631945, upload-time = "2025-09-09T15:16:26.877Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0f/2e68eb38244b5bbd68cd8d21e82d5f937353b563fd2f1aae28987e38a93d/pycares-4.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c2971af3a4094280f7c24293ff4d361689c175c1ebcbea6b3c1560eaff7cb240", size = 145863, upload-time = "2025-09-09T15:16:31.253Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3c/3c0ddeed957667438dd6151e9c41f21b54b49a3c16159807ca5d52eff621/pycares-4.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d69e2034160e1219665decb8140e439afc7a7afcfd4adff08eb0f6142405c3e", size = 141825, upload-time = "2025-09-09T15:16:32.408Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/f285b4944e69f611d1f4fadae63675edfb4380a980e6b6e99acca9d7e731/pycares-4.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3bd81ad69f607803f531ff5cfa1262391fa06e78488c13495cee0f70d02e0287", size = 642673, upload-time = "2025-09-09T15:16:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/61550e684035e71c894752e074b3722e5f1d40739840ca8b0b295209def7/pycares-4.11.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:0aed0974eab3131d832e7e84a73ddb0dddbc57393cd8c0788d68a759a78c4a7b", size = 690263, upload-time = "2025-09-09T15:16:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e6/e5e5e96821bb98106222fb8f617ba3e0c8828e75e74c67685f0044c77907/pycares-4.11.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:30d197180af626bb56f17e1fa54640838d7d12ed0f74665a3014f7155435b199", size = 682092, upload-time = "2025-09-09T15:16:36.119Z" }, + { url = "https://files.pythonhosted.org/packages/51/37/3c065239229e5ca57f2f46bac2cedaf32b26a22dae5d728751e8623efb4d/pycares-4.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cb711a66246561f1cae51244deef700eef75481a70d99611fd3c8ab5bd69ab49", size = 643995, upload-time = "2025-09-09T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/a3a24b205a725e51eebf3d766e512ccca07462da60211a238d906535105c/pycares-4.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7aba9a312a620052133437f2363aae90ae4695ee61cb2ee07cbb9951d4c69ddd", size = 627004, upload-time = "2025-09-09T15:16:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/d9d2d4b15fcb6bd703306fa5ad426df22d5c7076e689b62bfbcb884b8a87/pycares-4.11.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c2af7a9d3afb63da31df1456d38b91555a6c147710a116d5cc70ab1e9f457a4f", size = 673235, upload-time = "2025-09-09T15:16:45.449Z" }, + { url = "https://files.pythonhosted.org/packages/1c/51/bc12de8ab3b36c0352a2b157d556dbdae942652d88f6db83034fa3b5cdaf/pycares-4.11.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d5fe089be67bc5927f0c0bd60c082c79f22cf299635ee3ddd370ae2a6e8b4ae0", size = 656624, upload-time = "2025-09-09T15:16:46.905Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ab/dd42b95634edcb26bdf0abde579f78d5ede3377fb46e3947ec223b2fbba5/pycares-4.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35ff1ec260372c97ed688efd5b3c6e5481f2274dea08f6c4ea864c195a9673c6", size = 631904, upload-time = "2025-09-09T15:16:48.587Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4e/4821b66feefaaa8ec03494c1a11614c430983572e54ff062b4589441e199/pycares-4.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b93d624560ba52287873bacff70b42c99943821ecbc810b959b0953560f53c36", size = 145906, upload-time = "2025-09-09T15:16:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/e8/81/93a505dcbb7533254b0ce1da519591dcda889d6a66dcdfa5737e3280e18a/pycares-4.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:775d99966e28c8abd9910ddef2de0f1e173afc5a11cea9f184613c747373ab80", size = 141972, upload-time = "2025-09-09T15:16:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d6/76994c8b21316e48ea6c3ce3298574c28f90c9c41428a3349a57104621c9/pycares-4.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:84fde689557361764f052850a2d68916050adbfd9321f6105aca1d8f1a9bd49b", size = 637832, upload-time = "2025-09-09T15:16:55.523Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/5ca7e316d0edb714d78974cb34f4883f63fe9f580644c2db99fb62b05f56/pycares-4.11.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:30ceed06f3bf5eff865a34d21562c25a7f3dad0ed336b9dd415330e03a6c50c4", size = 687751, upload-time = "2025-09-09T15:16:57.55Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/c5c578fdd335d7b1dcaea88fae3497390095b5b05a1ba34a29f62d037abb/pycares-4.11.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:97d971b3a88a803bb95ff8a40ea4d68da59319eb8b59e924e318e2560af8c16d", size = 678362, upload-time = "2025-09-09T15:16:58.859Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/9be4d838a9348dd2e72a90c34d186b918b66d499af5be79afa18a6ba2808/pycares-4.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2d5cac829da91ade70ce1af97dad448c6cd4778b48facbce1b015e16ced93642", size = 641069, upload-time = "2025-09-09T15:17:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/39/d6/8ea9b5dcef6b566cde034aa2b68743f7b0a19fa0fba9ea01a4f98b8a57fb/pycares-4.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee1ea367835eb441d246164c09d1f9703197af4425fc6865cefcde9e2ca81f85", size = 622357, upload-time = "2025-09-09T15:17:01.205Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/3401e89b5d2970e30e02f9beb29ad59e2a8f19ef2c68c978de2b764cacb0/pycares-4.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3139ec1f4450a4b253386035c5ecd2722582ae3320a456df5021ffe3f174260a", size = 670290, upload-time = "2025-09-09T15:17:02.413Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c4/ff6a166e1d1d1987339548a19d0b1d52ec3ead8b3a8a2247a0d96e56013c/pycares-4.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5d70324ca1d82c6c4b00aa678347f7560d1ef2ce1d181978903459a97751543a", size = 652958, upload-time = "2025-09-09T15:17:04.203Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/fc084b395921c9b862d31a83f809fe649c24314b51b527ad0ab0df33edd4/pycares-4.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2f8d9cfe0eb3a2997fde5df99b1aaea5a46dabfcfcac97b2d05f027c2cd5e28", size = 629239, upload-time = "2025-09-09T15:17:05.477Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a9/62fea7ad72ac1fed2ac9dd8e9a7379b7eb0288bf2b3ea5731642c3a6f7de/pycares-4.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c296ab94d1974f8d2f76c499755a9ce31ffd4986e8898ef19b90e32525f7d84", size = 145909, upload-time = "2025-09-09T15:17:10.491Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/0317d6d0d3bd7599c53b8f1db09ad04260647d2f6842018e322584791fd5/pycares-4.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0fcd3a8bac57a0987d9b09953ba0f8703eb9dca7c77f7051d8c2ed001185be8", size = 141974, upload-time = "2025-09-09T15:17:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/63/11/731b565ae1e81c43dac247a248ee204628186f6df97c9927bd06c62237f8/pycares-4.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bac55842047567ddae177fb8189b89a60633ac956d5d37260f7f71b517fd8b87", size = 637796, upload-time = "2025-09-09T15:17:12.815Z" }, + { url = "https://files.pythonhosted.org/packages/f5/30/a2631fe2ffaa85475cdbff7df1d9376bc0b2a6ae77ca55d53233c937a5da/pycares-4.11.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:4da2e805ed8c789b9444ef4053f6ef8040cd13b0c1ca6d3c4fe6f9369c458cb4", size = 687734, upload-time = "2025-09-09T15:17:14.015Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b7/b3a5f99d4ab776662e71d5a56e8f6ea10741230ff988d1f502a8d429236b/pycares-4.11.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:ea785d1f232b42b325578f0c8a2fa348192e182cc84a1e862896076a4a2ba2a7", size = 678320, upload-time = "2025-09-09T15:17:15.442Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/a00d962b90432993afbf3bd05da8fe42117e0d9037cd7fd428dc41094d7b/pycares-4.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aa160dc9e785212c49c12bb891e242c949758b99542946cc8e2098ef391f93b0", size = 641012, upload-time = "2025-09-09T15:17:16.728Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fb/9266979ba59d37deee1fd74452b2ae32a7395acafe1bee510ac023c6c9a5/pycares-4.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7830709c23bbc43fbaefbb3dde57bdd295dc86732504b9d2e65044df8fd5e9fb", size = 622363, upload-time = "2025-09-09T15:17:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/91/c2/16dbc3dc33781a3c79cbdd76dd1cda808d98ba078d9a63a725d6a1fad181/pycares-4.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ef1ab7abbd238bb2dbbe871c3ea39f5a7fc63547c015820c1e24d0d494a1689", size = 670294, upload-time = "2025-09-09T15:17:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/ff/75/f003905e55298a6dd5e0673a2dc11e31518a5141393b925dc05fcaba9fb4/pycares-4.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a4060d8556c908660512d42df1f4a874e4e91b81f79e3a9090afedc7690ea5ba", size = 652973, upload-time = "2025-09-09T15:17:20.388Z" }, + { url = "https://files.pythonhosted.org/packages/55/2a/eafb235c371979e11f8998d686cbaa91df6a84a34ffe4d997dfe57c45445/pycares-4.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a98fac4a3d4f780817016b6f00a8a2c2f41df5d25dfa8e5b1aa0d783645a6566", size = 629235, upload-time = "2025-09-09T15:17:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/a723bc79bdcac60361b40184b649282ac0ab433b90e9cc0975370c2ff9c9/pycares-4.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:df0a17f4e677d57bca3624752bbb515316522ad1ce0de07ed9d920e6c4ee5d35", size = 145910, upload-time = "2025-09-09T15:17:26.774Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/46311ef5a384b5f0bb206851135dde8f86b3def38fdbee9e3c03475d35ae/pycares-4.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b44e54cad31d3c3be5e8149ac36bc1c163ec86e0664293402f6f846fb22ad00", size = 142053, upload-time = "2025-09-09T15:17:27.956Z" }, + { url = "https://files.pythonhosted.org/packages/74/23/d236fc4f134d6311e4ad6445571e8285e84a3e155be36422ff20c0fbe471/pycares-4.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:80752133442dc7e6dd9410cec227c49f69283c038c316a8585cca05ec32c2766", size = 637878, upload-time = "2025-09-09T15:17:29.173Z" }, + { url = "https://files.pythonhosted.org/packages/f7/92/6edd41282b3f0e3d9defaba7b05c39730d51c37c165d9d3b319349c975aa/pycares-4.11.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:84b0b402dd333403fdce0e204aef1ef834d839c439c0c1aa143dc7d1237bb197", size = 687865, upload-time = "2025-09-09T15:17:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a9/4d7cf4d72600fd47d9518f9ce99703a3e8711fb08d2ef63d198056cdc9a9/pycares-4.11.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c0eec184df42fc82e43197e073f9cc8f93b25ad2f11f230c64c2dc1c80dbc078", size = 678396, upload-time = "2025-09-09T15:17:32.304Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/e546eeb1d8ff6559e2e3bef31a6ea0c6e57ec826191941f83a3ce900ca89/pycares-4.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ee751409322ff10709ee867d5aea1dc8431eec7f34835f0f67afd016178da134", size = 640786, upload-time = "2025-09-09T15:17:33.602Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f5/b4572d9ee9c26de1f8d1dc80730df756276b9243a6794fa3101bbe56613d/pycares-4.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1732db81e348bfce19c9bf9448ba660aea03042eeeea282824da1604a5bd4dcf", size = 621857, upload-time = "2025-09-09T15:17:34.74Z" }, + { url = "https://files.pythonhosted.org/packages/17/f2/639090376198bcaeff86562b25e1bce05a481cfb1e605f82ce62285230cd/pycares-4.11.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:702d21823996f139874aba5aa9bb786d69e93bde6e3915b99832eb4e335d31ae", size = 670130, upload-time = "2025-09-09T15:17:35.982Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c4/cf40773cd9c36a12cebbe1e9b6fb120f9160dc9bfe0398d81a20b6c69972/pycares-4.11.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:218619b912cef7c64a339ab0e231daea10c994a05699740714dff8c428b9694a", size = 653133, upload-time = "2025-09-09T15:17:37.179Z" }, + { url = "https://files.pythonhosted.org/packages/32/6b/06054d977b0a9643821043b59f523f3db5e7684c4b1b4f5821994d5fa780/pycares-4.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:719f7ddff024fdacde97b926b4b26d0cc25901d5ef68bb994a581c420069936d", size = 629344, upload-time = "2025-09-09T15:17:38.308Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/2f3558d298ff8db31d5c83369001ab72af3b86a0374d9b0d40dc63314187/pycares-4.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c9d839b5700542b27c1a0d359cbfad6496341e7c819c7fea63db9588857065ed", size = 146408, upload-time = "2025-09-09T15:17:43.74Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c8/516901e46a1a73b3a75e87a35f3a3a4fe085f1214f37d954c9d7e782bd6d/pycares-4.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:31b85ad00422b38f426e5733a71dfb7ee7eb65a99ea328c508d4f552b1760dc8", size = 142371, upload-time = "2025-09-09T15:17:45.186Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/c3fba0aa575f331ebed91f87ba960ffbe0849211cdf103ab275bc0107ac6/pycares-4.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cdac992206756b024b371760c55719eb5cd9d6b2cb25a8d5a04ae1b0ff426232", size = 647504, upload-time = "2025-09-09T15:17:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e4/1cdc3ec9c92f8069ec18c58b016b2df7c44a088e2849f37ed457554961aa/pycares-4.11.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:ffb22cee640bc12ee0e654eba74ecfb59e2e0aebc5bccc3cc7ef92f487008af7", size = 697122, upload-time = "2025-09-09T15:17:47.772Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d5/bd8f370b97bb73e5bdd55dc2a78e18d6f49181cf77e88af0599d16f5c073/pycares-4.11.0-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:00538826d2eaf4a0e4becb0753b0ac8d652334603c445c9566c9eb273657eb4c", size = 687543, upload-time = "2025-09-09T15:17:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/33/38/49b77b9cf5dffc0b1fdd86656975c3bc1a58b79bdc883a9ef749b17a013c/pycares-4.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:29daa36548c04cdcd1a78ae187a4b7b003f0b357a2f4f1f98f9863373eedc759", size = 649565, upload-time = "2025-09-09T15:17:51.03Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/f6d57bfb99d00a6a7363f95c8d3a930fe82a868d9de24c64c8048d66f16a/pycares-4.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cf306f3951740d7bed36149a6d8d656a7d5432dd4bbc6af3bb6554361fc87401", size = 631242, upload-time = "2025-09-09T15:17:52.298Z" }, + { url = "https://files.pythonhosted.org/packages/33/a2/7b9121c71cfe06a8474e221593f83a78176fae3b79e5853d2dfd13ab01cc/pycares-4.11.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:386da2581db4ea2832629e275c061103b0be32f9391c5dfaea7f6040951950ad", size = 680304, upload-time = "2025-09-09T15:17:53.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/07/dfe76807f637d8b80e1a59dfc4a1bceabdd0205a45b2ebf78b415ae72af3/pycares-4.11.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:45d3254a694459fdb0640ef08724ca9d4b4f6ff6d7161c9b526d7d2e2111379e", size = 661039, upload-time = "2025-09-09T15:17:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9b/55d50c5acd46cbe95d0da27740a83e721d89c0ce7e42bff9891a9f29a855/pycares-4.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eddf5e520bb88b23b04ac1f28f5e9a7c77c718b8b4af3a4a7a2cc4a600f34502", size = 637560, upload-time = "2025-09-09T15:17:56.492Z" }, +] + +[[package]] +name = "pycognito" +version = "2024.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "envs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/67/3975cf257fcc04903686ef87d39be386d894a0d8182f43d37e9cbfc9609f/pycognito-2024.5.1.tar.gz", hash = "sha256:e211c66698c2c3dc8680e95107c2b4a922f504c3f7c179c27b8ee1ab0fc23ae4", size = 31182, upload-time = "2024-05-16T10:02:28.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/7a/f38dd351f47596b22ddbde1b8906e7f43d14be391dcdbd0c2daba886f26c/pycognito-2024.5.1-py3-none-any.whl", hash = "sha256:c821895dc62b7aea410fdccae4f96d8be7cab374182339f50a03de0fcb93f9ea", size = 26607, upload-time = "2024-05-16T10:02:27.3Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -2702,135 +3614,111 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd", size = 816358, upload-time = "2025-10-14T15:02:21.842Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae", size = 460628, upload-time = "2025-10-14T15:02:19.623Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197, upload-time = "2025-10-14T10:19:43.303Z" }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909, upload-time = "2025-10-14T10:19:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905, upload-time = "2025-10-14T10:19:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938, upload-time = "2025-10-14T10:19:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710, upload-time = "2025-10-14T10:19:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445, upload-time = "2025-10-14T10:19:51.269Z" }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875, upload-time = "2025-10-14T10:19:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329, upload-time = "2025-10-14T10:19:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658, upload-time = "2025-10-14T10:19:55.843Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777, upload-time = "2025-10-14T10:19:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705, upload-time = "2025-10-14T10:19:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739, upload-time = "2025-10-14T10:23:06.934Z" }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549, upload-time = "2025-10-14T10:23:09.24Z" }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093, upload-time = "2025-10-14T10:23:11.626Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971, upload-time = "2025-10-14T10:23:14.437Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939, upload-time = "2025-10-14T10:23:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400, upload-time = "2025-10-14T10:23:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840, upload-time = "2025-10-14T10:23:21.738Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, ] [[package]] @@ -2838,9 +3726,9 @@ name = "pydantic-settings" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } wheels = [ @@ -2852,8 +3740,8 @@ name = "pyftpdlib" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyasynchat", marker = "python_full_version >= '3.12'" }, - { name = "pyasyncore", marker = "python_full_version >= '3.12'" }, + { name = "pyasynchat", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyasyncore", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/67/3299ce20585601d21e05153eb9275cb799ae408fe15ab93e48e4582ea9fe/pyftpdlib-2.1.0.tar.gz", hash = "sha256:5e92e7ba37c3e458ec458e5c201e2deb992cb6011c963e6a8512a634d8d80116", size = 205767, upload-time = "2025-09-25T13:03:19.419Z" } @@ -2866,6 +3754,112 @@ 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 = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] + +[[package]] +name = "pylint-per-file-ignores" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/3d/21bec2f2f432519616c34a64ba0766ef972fdfb6234a86bb1b8baf4b0c7c/pylint_per_file_ignores-1.4.0.tar.gz", hash = "sha256:c0de7b3d0169571aefaa1ac3a82a265641b8825b54a0b6f5ef27c3b76b988609", size = 4419, upload-time = "2025-01-17T21:35:02.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/0e/bf3473d86648a17e6dd6ee9e6abce526b077169031177f4f2031368f864a/pylint_per_file_ignores-1.4.0-py3-none-any.whl", hash = "sha256:0cd82d22551738b4e63a0aa1dab2a1fc4016e8f27f1429159616483711e122fd", size = 4888, upload-time = "2025-01-17T21:35:00.371Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/bf/3dbb1783388da54e650f8a6b88bde03c101d9ba93dfe8ab1b1873f1cd999/pyobjc_core-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93418e79c1655f66b4352168f8c85c942707cb1d3ea13a1da3e6f6a143bacda7", size = 676748, upload-time = "2025-11-14T09:30:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/aa/2b2d7ec3ac4b112a605e9bd5c5e5e4fd31d60a8a4b610ab19cc4838aa92a/pyobjc_framework_cocoa-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9b880d3bdcd102809d704b6d8e14e31611443aa892d9f60e8491e457182fdd48", size = 383825, upload-time = "2025-11-14T09:40:28.354Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-cocoa", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/25/d21d6cb3fd249c2c2aa96ee54279f40876a0c93e7161b3304bf21cbd0bfe/pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0", size = 33157, upload-time = "2025-11-14T10:13:28.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1b/06914f4eb1bd8ce598fdd210e1a7411556286910fc8d8919ab7dbaebe629/pyobjc_framework_corebluetooth-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:937849f4d40a33afbcc56cbe90c8d1fbf30fb27a962575b9fb7e8e2c61d3c551", size = 13187, upload-time = "2025-11-14T09:44:04.098Z" }, + { url = "https://files.pythonhosted.org/packages/57/7a/26ae106beb97e9c4745065edb3ce3c2bdd91d81f5b52b8224f82ce9d5fb9/pyobjc_framework_corebluetooth-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:37e6456c8a076bd5a2bdd781d0324edd5e7397ef9ac9234a97433b522efb13cf", size = 13189, upload-time = "2025-11-14T09:44:06.229Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/01fef62a479cdd6ff9ee40b6e062a205408ff386ce5ba56d7e14a71fcf73/pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213", size = 13209, upload-time = "2025-11-14T09:44:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6c/831139ebf6a811aed36abfdfad846bc380dcdf4e6fb751a310ce719ddcfd/pyobjc_framework_corebluetooth-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a894f695e6c672f0260327103a31ad8b98f8d4fb9516a0383db79a82a7e58dc", size = 13229, upload-time = "2025-11-14T09:44:10.463Z" }, + { url = "https://files.pythonhosted.org/packages/09/3c/3a6fe259a9e0745aa4612dee86b61b4fd7041c44b62642814e146b654463/pyobjc_framework_corebluetooth-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1daf07a0047c3ed89fab84ad5f6769537306733b6a6e92e631581a0f419e3f32", size = 13409, upload-time = "2025-11-14T09:44:12.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/41/90640a4db62f0bf0611cf8a161129c798242116e2a6a44995668b017b106/pyobjc_framework_corebluetooth-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:15ba5207ca626dffe57ccb7c1beaf01f93930159564211cb97d744eaf0d812aa", size = 13222, upload-time = "2025-11-14T09:44:14.345Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/8ed2f0ca02b9abe204966142bd8c4501cf6da94234cc320c4c0562c467e8/pyobjc_framework_corebluetooth-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e5385195bd365a49ce70e2fb29953681eefbe68a7b15ecc2493981d2fb4a02b1", size = 13408, upload-time = "2025-11-14T09:44:16.558Z" }, +] + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-cocoa", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/e8/75b6b9b3c88b37723c237e5a7600384ea2d84874548671139db02e76652b/pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41", size = 38277, upload-time = "2025-11-14T10:16:46.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/76/9936d97586dbae4d7d10f3958d899ee7a763930af69b5ad03d4516178c7c/pyobjc_framework_libdispatch-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50a81a29506f0e35b4dc313f97a9d469f7b668dae3ba597bb67bbab94de446bd", size = 20471, upload-time = "2025-11-14T09:52:53.134Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/c4aeab6ce7268373d4ceabbc5c406c4bbf557038649784384910932985f8/pyobjc_framework_libdispatch-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:954cc2d817b71383bd267cc5cd27d83536c5f879539122353ca59f1c945ac706", size = 20463, upload-time = "2025-11-14T09:52:55.703Z" }, + { url = "https://files.pythonhosted.org/packages/83/6f/96e15c7b2f7b51fc53252216cd0bed0c3541bc0f0aeb32756fefd31bed7d/pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789", size = 15650, upload-time = "2025-11-14T09:52:59.284Z" }, + { url = "https://files.pythonhosted.org/packages/38/3a/d85a74606c89b6b293782adfb18711026ff79159db20fc543740f2ac0bc7/pyobjc_framework_libdispatch-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:58ffce5e6bcd7456b4311009480b195b9f22107b7682fb0835d4908af5a68ad0", size = 15668, upload-time = "2025-11-14T09:53:01.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/40/49b1c1702114ee972678597393320d7b33f477e9d24f2a62f93d77f23dfb/pyobjc_framework_libdispatch-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e9f49517e253716e40a0009412151f527005eec0b9a2311ac63ecac1bdf02332", size = 15938, upload-time = "2025-11-14T09:53:03.461Z" }, + { url = "https://files.pythonhosted.org/packages/59/d8/7d60a70fc1a546c6cb482fe0595cb4bd1368d75c48d49e76d0bc6c0a2d0f/pyobjc_framework_libdispatch-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0ebfd9e4446ab6528126bff25cfb09e4213ddf992b3208978911cfd3152e45f5", size = 15693, upload-time = "2025-11-14T09:53:05.531Z" }, + { url = "https://files.pythonhosted.org/packages/99/32/15e08a0c4bb536303e1568e2ba5cae1ce39a2e026a03aea46173af4c7a2d/pyobjc_framework_libdispatch-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:23fc9915cba328216b6a736c7a48438a16213f16dfb467f69506300b95938cc7", size = 15976, upload-time = "2025-11-14T09:53:07.936Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, +] + [[package]] name = "pyparsing" version = "3.2.5" @@ -2875,22 +3869,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] +[[package]] +name = "pyrfc3339" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/7f/3c194647ecb80ada6937c38a162ab3edba85a8b6a58fa2919405f4de2509/pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f", size = 12589, upload-time = "2025-08-23T16:40:31.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", size = 6758, upload-time = "2025-08-23T16:40:30.49Z" }, +] + +[[package]] +name = "pyric" +version = "0.1.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/64/a99f27d3b4347486c7bfc0aa516016c46dc4c0f380ffccbd742a61af1eda/PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9", size = 870401, upload-time = "2016-12-04T07:54:48.374Z" } + [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pluggy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pygments", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, +] + +[[package]] +name = "pytest-aiohttp" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-asyncio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/4b/d326890c153f2c4ce1bf45d07683c08c10a1766058a22934620bc6ac6592/pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc", size = 12842, upload-time = "2025-01-23T12:44:04.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/0f/e6af71c02e0f1098eaf7d2dbf3ffdf0a69fc1e0ef174f96af05cef161f1b/pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d", size = 8932, upload-time = "2025-01-23T12:44:03.27Z" }, ] [[package]] @@ -2898,9 +3918,7 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, - { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ @@ -2912,21 +3930,159 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, + { name = "coverage", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pluggy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] 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 = "pytest-freezer" +version = "0.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "freezegun", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/f0/98dcbc5324064360b19850b14c84cea9ca50785d921741dbfc442346e925/pytest_freezer-0.4.9.tar.gz", hash = "sha256:21bf16bc9cc46bf98f94382c4b5c3c389be7056ff0be33029111ae11b3f1c82a", size = 3177, upload-time = "2024-12-12T08:53:08.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e9/30252bc05bcf67200a17f4f0b4cc7598f0a68df4fa9fa356193aa899f145/pytest_freezer-0.4.9-py3-none-any.whl", hash = "sha256:8b6c50523b7d4aec4590b52bfa5ff766d772ce506e2bf4846c88041ea9ccae59", size = 3192, upload-time = "2024-12-12T08:53:07.641Z" }, +] + +[[package]] +name = "pytest-github-actions-annotate-failures" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/d4/c54ee6a871eee4a7468e3a8c0dead28e634c0bc2110c694309dcb7563a66/pytest_github_actions_annotate_failures-0.3.0.tar.gz", hash = "sha256:d4c3177c98046c3900a7f8ddebb22ea54b9f6822201b5d3ab8fcdea51e010db7", size = 11248, upload-time = "2025-01-17T22:39:32.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/73/7b0b15cb8605ee967b34aa1d949737ab664f94e6b0f1534e8339d9e64ab2/pytest_github_actions_annotate_failures-0.3.0-py3-none-any.whl", hash = "sha256:41ea558ba10c332c0bfc053daeee0c85187507b2034e990f21e4f7e5fef044cf", size = 6030, upload-time = "2025-01-17T22:39:31.701Z" }, +] + +[[package]] +name = "pytest-homeassistant-custom-component" +version = "0.13.308" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "freezegun", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "homeassistant", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "license-expression", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "mock-open", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "paho-mqtt", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pipdeptree", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pylint-per-file-ignores", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-asyncio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-cov", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-freezer", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-github-actions-annotate-failures", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-picked", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-socket", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-sugar", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-timeout", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-unordered", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest-xdist", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "requests-mock", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "respx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "syrupy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "tqdm", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/fa/2c9071b1cb6533990a059b5610616722cf0025b3ab39713ae03ff851757d/pytest_homeassistant_custom_component-0.13.308.tar.gz", hash = "sha256:49d2fddff373f24ac24e06efaf2251fe3d86eacbc93508f416c36c3cefe11efa", size = 63913, upload-time = "2026-01-24T05:06:51.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ea/3c18002800c4435194213f9d032de4c87b4f13b0cb753b9e3bf895ee19c6/pytest_homeassistant_custom_component-0.13.308-py3-none-any.whl", hash = "sha256:733f49e08959fc66130b45b96824a807f5cf2e20cbc345335567ce3cfe16f573", size = 69776, upload-time = "2026-01-24T05:06:49.861Z" }, +] + +[[package]] +name = "pytest-picked" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/e4/51a54dd6638fd4a7c45bb20a737235fd92cbb4d24b5ff681d64ace5d02e9/pytest_picked-0.5.1.tar.gz", hash = "sha256:6634c4356a560a5dc3dba35471865e6eb06bbd356b56b69c540593e9d5620ded", size = 8401, upload-time = "2024-11-06T23:19:52.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/81/450c017746caab376c4b6700439de9f1cc7d8e1f22dec3c1eb235cd9ad3e/pytest_picked-0.5.1-py3-none-any.whl", hash = "sha256:af65c4763b51dc095ae4bc5073a962406902422ad9629c26d8b01122b677d998", size = 6608, upload-time = "2024-11-06T23:19:51.284Z" }, +] + +[[package]] +name = "pytest-socket" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/ff/90c7e1e746baf3d62ce864c479fd53410b534818b9437413903596f81580/pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3", size = 12389, upload-time = "2024-01-28T20:17:23.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754, upload-time = "2024-01-28T20:17:22.105Z" }, +] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "termcolor", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-unordered" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/3e/6ec9ec74551804c9e005d5b3cbe1fd663f03ed3bd4bdb1ce764c3d334d8e/pytest_unordered-0.7.0.tar.gz", hash = "sha256:0f953a438db00a9f6f99a0f4727f2d75e72dd93319b3d548a97ec9db4903a44f", size = 7930, upload-time = "2025-06-03T12:56:04.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/95/ae2875e19472797e9672b65412858ab6639d8e55defd9859241e5ff80d02/pytest_unordered-0.7.0-py3-none-any.whl", hash = "sha256:486b26d24a2d3b879a275c3d16d14eda1bd9c32aafddbb17b98ac755daba7584", size = 6210, upload-time = "2025-06-03T12:36:06.66Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "six", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -2942,6 +4098,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -2955,8 +4132,6 @@ wheels = [ { 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" }, @@ -2964,8 +4139,6 @@ wheels = [ { 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" }, @@ -2973,9 +4146,6 @@ wheels = [ { 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" }, @@ -2983,9 +4153,6 @@ wheels = [ { 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" }, @@ -2993,8 +4160,6 @@ wheels = [ { 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" }, @@ -3002,8 +4167,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -3011,7 +4174,7 @@ name = "pyzmq" version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, + { name = "cffi", marker = "python_full_version >= '3.13.2' and implementation_name == 'pypy' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ @@ -3022,9 +4185,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, @@ -3032,9 +4192,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, @@ -3042,10 +4199,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, @@ -3054,9 +4207,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, @@ -3064,19 +4214,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, - { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, - { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] @@ -3084,30 +4229,153 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "attrs", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "rpds-py", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, +] + [[package]] name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, + { name = "certifi", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "idna", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "urllib3", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] 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-mock" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + [[package]] name = "rpds-py" version = "0.29.0" @@ -3126,8 +4394,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/e9/c435ddb602ced19a80b8277a41371734f33ad3f91cc4ceb4d82596800a3c/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0248b19405422573621172ab8e3a1f29141362d13d9f72bafa2e28ea0cdca5a2", size = 574153, upload-time = "2025-11-16T14:47:50.435Z" }, { url = "https://files.pythonhosted.org/packages/84/82/dc3c32e1f89ecba8a59600d4cd65fe0ad81b6c636ccdbf6cd177fd6a7bac/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f9f436aee28d13b9ad2c764fc273e0457e37c2e61529a07b928346b219fcde3b", size = 600304, upload-time = "2025-11-16T14:47:51.599Z" }, { url = "https://files.pythonhosted.org/packages/35/98/785290e0b7142470735dc1b1f68fb33aae29e5296f062c88396eedf796c8/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24a16cb7163933906c62c272de20ea3c228e4542c8c45c1d7dc2b9913e17369a", size = 562211, upload-time = "2025-11-16T14:47:53.094Z" }, - { url = "https://files.pythonhosted.org/packages/30/58/4eeddcb0737c6875f3e30c65dc9d7e7a10dfd5779646a990fa602c6d56c5/rpds_py-0.29.0-cp310-cp310-win32.whl", hash = "sha256:1a409b0310a566bfd1be82119891fefbdce615ccc8aa558aff7835c27988cbef", size = 221803, upload-time = "2025-11-16T14:47:54.404Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/b35a8dbdcbeb32505500547cdafaa9f8863e85f8faac50ef34464ec5a256/rpds_py-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5523b0009e7c3c1263471b69d8da1c7d41b3ecb4cb62ef72be206b92040a950", size = 235530, upload-time = "2025-11-16T14:47:56.061Z" }, { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, @@ -3140,9 +4406,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, - { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, - { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, @@ -3155,9 +4418,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, - { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, - { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, - { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" }, { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" }, { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" }, @@ -3170,9 +4430,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" }, { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" }, { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" }, - { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" }, - { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" }, { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" }, { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" }, { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" }, @@ -3185,8 +4442,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" }, { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" }, { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" }, - { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" }, { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, @@ -3199,9 +4454,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, @@ -3214,8 +4466,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, - { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, @@ -3251,83 +4501,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, - { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] [[package]] -name = "scipy" -version = "1.15.3" +name = "s3transfer" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +dependencies = [ + { name = "botocore", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] [[package]] name = "scipy" version = "1.16.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } wheels = [ @@ -3339,8 +4532,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, @@ -3349,8 +4540,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, - { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, @@ -3359,8 +4548,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, - { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, @@ -3369,8 +4556,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, @@ -3379,8 +4564,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, - { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, @@ -3389,8 +4572,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, - { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "securetar" +version = "2025.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/5b/da5f56ad39cbb1ca49bd0d4cccde7e97ea7d01fa724fa953746fa2b32ee6/securetar-2025.2.1.tar.gz", hash = "sha256:59536a73fe5cecbc1f00b1838c8b1052464a024e2adcf6c9ce1d200d91990fb1", size = 16124, upload-time = "2025-02-25T14:17:51.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/e0/b93a18e9bb7f7d2573a9c6819d42d996851edde0b0406d017067d7d23a0a/securetar-2025.2.1-py3-none-any.whl", hash = "sha256:760ad9d93579d5923f3d0da86e0f185d0f844cf01795a8754539827bb6a1bab4", size = 11545, upload-time = "2025-02-25T14:17:50.832Z" }, +] + +[[package]] +name = "sentence-stream" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/69/f3d048692aac843f41102507f6257138392ec841c16718f0618d27051caf/sentence_stream-1.3.0.tar.gz", hash = "sha256:b06261d35729de97df9002a1cc708f9a888f662b80d5d6d008ee69c51f36041b", size = 10049, upload-time = "2026-01-08T16:25:06.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/b6/48339c109bab6f54ff608800773b9425464c6cbf7fd3f2ba01294d78be3d/sentence_stream-1.3.0-py3-none-any.whl", hash = "sha256:7448d131315b85eefdf238e5edd9caa62899acf609145d5e0e10c09812eb8a1d", size = 8707, upload-time = "2026-01-08T16:25:05.918Z" }, ] [[package]] @@ -3402,6 +4607,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -3420,53 +4634,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "snitun" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "cryptography", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e2/b5bbf04971d1c3e07a3e16a706ea3c1a4b711c6d8c9566e8012772d3351a/snitun-0.45.1.tar.gz", hash = "sha256:d76d48cf4190ea59e8f63892da9c18499bfc6ca796220a463c6f3b32099d661c", size = 43335, upload-time = "2025-09-25T05:24:07.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1b/83ff83003994bc8b56483c75a710de588896c167c7c42d66d059a2eb48dc/snitun-0.45.1-py3-none-any.whl", hash = "sha256:c1fa4536320ec3126926ade775c429e20664db1bc61d8fec0e181dc393d36ab4", size = 51236, upload-time = "2025-09-25T05:24:06.412Z" }, +] + [[package]] name = "sqlalchemy" -version = "2.0.45" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, - { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, - { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, - { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, - { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, - { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, - { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'WIN32' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'amd64' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'ppc64le' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'win32' and sys_platform != 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/12/d7c445b1940276a828efce7331cb0cb09d6e5f049651db22f4ebb0922b77/sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", size = 2117967, upload-time = "2025-05-14T17:48:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b8/cb90f23157e28946b27eb01ef401af80a1fab7553762e87df51507eaed61/sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", size = 2107583, upload-time = "2025-05-14T17:48:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/eef84283a1c8164a207d898e063edf193d36a24fb6a5bb3ce0634b92a1e8/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", size = 3186025, upload-time = "2025-05-14T17:51:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/49d52bd3c5e63a1d458fd6d289a1523a8015adedbddf2c07408ff556e772/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", size = 3186259, upload-time = "2025-05-14T17:55:22.526Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9e/e3ffc37d29a3679a50b6bbbba94b115f90e565a2b4545abb17924b94c52d/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", size = 3126803, upload-time = "2025-05-14T17:51:53.277Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/56b21e363f6039978ae0b72690237b38383e4657281285a09456f313dd77/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", size = 3148566, upload-time = "2025-05-14T17:55:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, ] [[package]] @@ -3474,22 +4689,52 @@ name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, + { name = "asttokens", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "executing", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pure-eval", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "standard-chunk", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + +[[package]] +name = "standard-telnetlib" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/06/7bf7c0ec16574aeb1f6602d6a7bdb020084362fb4a9b177c5465b0aae0b6/standard_telnetlib-3.13.0.tar.gz", hash = "sha256:243333696bf1659a558eb999c23add82c41ffc2f2d04a56fae13b61b536fb173", size = 12636, upload-time = "2024-10-30T16:01:42.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" }, +] + [[package]] name = "starlette" version = "0.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "anyio", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } wheels = [ @@ -3501,8 +4746,8 @@ name = "stone" version = "3.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ply" }, - { name = "six" }, + { name = "ply", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "six", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/99/6f/ef25bbc1aefeb9c905d527f1d3cd3f41f22f40566d33001b8bb14ae0cdaf/stone-3.3.1.tar.gz", hash = "sha256:4ef0397512f609757975f7ec09b35639d72ba7e3e17ce4ddf399578346b4cb50", size = 190888, upload-time = "2022-01-25T21:32:16.729Z" } wheels = [ @@ -3514,13 +4759,25 @@ name = "sympy" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mpmath" }, + { name = "mpmath", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "syrupy" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/90/1a442d21527009d4b40f37fe50b606ebb68a6407142c2b5cc508c34b696b/syrupy-5.0.0.tar.gz", hash = "sha256:3282fe963fa5d4d3e47231b16d1d4d0f4523705e8199eeb99a22a1bc9f5942f2", size = 48881, upload-time = "2025-09-28T21:15:12.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/9a/6c68aad2ccfce6e2eeebbf5bb709d0240592eb51ff142ec4c8fbf3c2460a/syrupy-5.0.0-py3-none-any.whl", hash = "sha256:c848e1a980ca52a28715cd2d2b4d434db424699c05653bd1158fb31cf56e9546", size = 49087, upload-time = "2025-09-28T21:15:11.639Z" }, +] + [[package]] name = "termcolor" version = "3.2.0" @@ -3531,52 +4788,93 @@ wheels = [ ] [[package]] -name = "tomli" -version = "2.3.0" +name = "text-unidecode" +version = "1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, ] [[package]] @@ -3584,60 +4882,52 @@ name = "torch" version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "filelock", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "fsspec", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "jinja2", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "networkx", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "sympy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "triton", marker = "python_full_version >= '3.13.2' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/bb/86/245c240d2138c17ed572c943c289056c2721abab70810d772c6bf5495b28/torch-2.9.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:030bbfe367379ae6a4ae4042b6c44da25383343b8b3c68abaa9c7231efbaf2dd", size = 104213554, upload-time = "2025-10-15T15:45:59.798Z" }, { url = "https://files.pythonhosted.org/packages/58/1d/fd1e88ae0948825efcab7dd66d12bec23f05d4d38ed81573c8d453c14c06/torch-2.9.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:51cb63902182a78e90886e8068befd8ea102af4b00e420263591a3d70c7d3c6c", size = 899795167, upload-time = "2025-10-15T15:47:12.695Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/496197b45c14982bef4e079b24c61dc108e3ab0d0cc9718dba9f54f45a46/torch-2.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:3f6aad4d2f0ee2248bac25339d74858ff846c3969b27d14ac235821f055af83d", size = 109310314, upload-time = "2025-10-15T15:46:16.633Z" }, { url = "https://files.pythonhosted.org/packages/58/b0/2b4e647b0fc706e88eb6c253d05511865578f5f67b55fad639bf3272a4a1/torch-2.9.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:413e1654c9203733138858780e184d9fc59442f0b3b209e16f39354eb893db9b", size = 74452019, upload-time = "2025-10-15T15:46:04.296Z" }, { url = "https://files.pythonhosted.org/packages/58/fe/334225e6330e672b36aef23d77451fa906ea12881570c08638a91331a212/torch-2.9.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c596708b5105d0b199215acf0c9be7c1db5f1680d88eddadf4b75a299259a677", size = 104230578, upload-time = "2025-10-15T15:46:08.182Z" }, { url = "https://files.pythonhosted.org/packages/05/cc/49566caaa218872ec9a2912456f470ff92649894a4bc2e5274aa9ef87c4a/torch-2.9.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:51de31219c97c51cf4bf2be94d622e3deb5dcc526c6dc00e97c17eaec0fc1d67", size = 899815990, upload-time = "2025-10-15T15:48:03.336Z" }, - { url = "https://files.pythonhosted.org/packages/74/25/e9ab21d5925b642d008f139d4a3c9664fc9ee1faafca22913c080cc4c0a5/torch-2.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd515c70059afd95f48b8192733764c08ca37a1d19803af6401b5ecad7c8676e", size = 109313698, upload-time = "2025-10-15T15:46:12.425Z" }, { url = "https://files.pythonhosted.org/packages/b3/b7/205ef3e94de636feffd64b28bb59a0dfac0771221201b9871acf9236f5ca/torch-2.9.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:614a185e4986326d526a91210c8fc1397e76e8cfafa78baf6296a790e53a9eec", size = 74463678, upload-time = "2025-10-15T15:46:29.779Z" }, { url = "https://files.pythonhosted.org/packages/d1/d3/3985739f3b8e88675127bf70f82b3a48ae083e39cda56305dbd90398fec0/torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642", size = 104107898, upload-time = "2025-10-15T15:46:20.883Z" }, { url = "https://files.pythonhosted.org/packages/a5/4b/f4bb2e6c25d0272f798cd6d7a04ed315da76cec68c602d87040c7847287f/torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6", size = 899738273, upload-time = "2025-10-15T15:50:04.188Z" }, - { url = "https://files.pythonhosted.org/packages/66/11/c1c5ba6691cda6279087c35bd626536e4fd29521fe740abf5008377a9a02/torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b", size = 109280887, upload-time = "2025-10-15T15:46:26.228Z" }, { url = "https://files.pythonhosted.org/packages/dd/5f/b85bd8c05312d71de9402bf5868d217c38827cfd09d8f8514e5be128a52b/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695", size = 74478983, upload-time = "2025-10-15T15:46:39.406Z" }, { url = "https://files.pythonhosted.org/packages/c2/1c/90eb13833cdf4969ea9707586d7b57095c3b6e2b223a7256bf111689bcb8/torch-2.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c30a17fc83eeab346913e237c64b15b5ba6407fff812f6c541e322e19bc9ea0e", size = 104111330, upload-time = "2025-10-15T15:46:35.238Z" }, { url = "https://files.pythonhosted.org/packages/0e/21/2254c54b8d523592c25ef4434769aa23e29b1e6bf5f4c0ad9e27bf442927/torch-2.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f25033b8667b57857dfd01458fbf2a9e6a6df1f8def23aef0dc46292f6aa642", size = 899750243, upload-time = "2025-10-15T15:48:57.459Z" }, - { url = "https://files.pythonhosted.org/packages/b7/a5/5cb94fa4fd1e78223455c23c200f30f6dc10c6d4a2bcc8f6e7f2a2588370/torch-2.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d037f1b4ffd25013be4a7bf3651a0a910c68554956c7b2c92ebe87c76475dece", size = 109284513, upload-time = "2025-10-15T15:46:45.061Z" }, { url = "https://files.pythonhosted.org/packages/66/e8/fc414d8656250ee46120b44836ffbb3266343db424b3e18ca79ebbf69d4f/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e4e5b5cba837a2a8d1a497ba9a58dae46fa392593eaa13b871c42f71847503a5", size = 74830362, upload-time = "2025-10-15T15:46:48.983Z" }, { url = "https://files.pythonhosted.org/packages/ed/5f/9474c98fc5ae0cd04b9466035428cd360e6611a86b8352a0fc2fa504acdc/torch-2.9.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:64693568f5dc4dbd5f880a478b1cea0201cc6b510d91d1bc54fea86ac5d1a637", size = 104144940, upload-time = "2025-10-15T15:47:29.076Z" }, { url = "https://files.pythonhosted.org/packages/2d/5a/8e0c1cf57830172c109d4bd6be2708cabeaf550983eee7029291322447a0/torch-2.9.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:f8ed31ddd7d10bfb3fbe0b9fe01b1243577f13d75e6f4a0839a283915ce3791e", size = 899744054, upload-time = "2025-10-15T15:48:29.864Z" }, - { url = "https://files.pythonhosted.org/packages/6d/28/82c28b30fcb4b7c9cdd995763d18bbb830d6521356712faebbad92ffa61d/torch-2.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eff527d4e4846e6f70d2afd8058b73825761203d66576a7e04ea2ecfebcb4ab8", size = 109517546, upload-time = "2025-10-15T15:47:33.395Z" }, { url = "https://files.pythonhosted.org/packages/ff/c3/a91f96ec74347fa5fd24453fa514bc61c61ecc79196fa760b012a1873d96/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:f8877779cf56d1ce431a7636703bdb13307f5960bb1af49716d8b179225e0e6a", size = 74480732, upload-time = "2025-10-15T15:47:38.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/73/9f70af34b334a7e0ef496ceec96b7ec767bd778ea35385ce6f77557534d1/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e614fae699838038d888729f82b687c03413c5989ce2a9481f9a7e7a396e0bb", size = 74433037, upload-time = "2025-10-15T15:47:41.894Z" }, { url = "https://files.pythonhosted.org/packages/b7/84/37cf88625901934c97109e583ecc21777d21c6f54cda97a7e5bbad1ee2f2/torch-2.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:dfb5b8cd310ba3436c7e14e8b7833ef658cf3045e50d2bdaed23c8fc517065eb", size = 104116482, upload-time = "2025-10-15T15:47:46.266Z" }, { url = "https://files.pythonhosted.org/packages/56/8e/ca8b17866943a8d4f4664d402ea84210aa274588b4c5d89918f5caa24eec/torch-2.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b3d29524993a478e46f5d598b249cd824b7ed98d7fba538bd9c4cde6c803948f", size = 899746916, upload-time = "2025-10-15T15:50:40.294Z" }, - { url = "https://files.pythonhosted.org/packages/43/65/3b17c0fbbdab6501c5b320a52a648628d0d44e7379f64e27d9eef701b6bf/torch-2.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:71c7578984f5ec0eb645eb4816ac8435fcf3e3e2ae1901bcd2f519a9cafb5125", size = 109275151, upload-time = "2025-10-15T15:49:20.715Z" }, { url = "https://files.pythonhosted.org/packages/83/36/74f8c051f785500396e42f93542422422dfd874a174f21f8d955d36e5d64/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:71d9309aee457bbe0b164bce2111cd911c4ed4e847e65d5077dbbcd3aba6befc", size = 74823353, upload-time = "2025-10-15T15:49:16.59Z" }, { url = "https://files.pythonhosted.org/packages/62/51/dc3b4e2f9ba98ae27238f0153ca098bf9340b2dafcc67fde645d496dfc2a/torch-2.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c08fb654d783899e204a32cca758a7ce8a45b2d78eeb89517cc937088316f78e", size = 104140340, upload-time = "2025-10-15T15:50:19.67Z" }, { url = "https://files.pythonhosted.org/packages/c0/8d/b00657f8141ac16af7bb6cda2e67de18499a3263b78d516b9a93fcbc98e3/torch-2.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ec8feb0099b2daa5728fbc7abb0b05730fd97e0f359ff8bda09865aaa7bd7d4b", size = 899731750, upload-time = "2025-10-15T15:49:36.673Z" }, - { url = "https://files.pythonhosted.org/packages/fc/29/bd361e0cbb2c79ce6450f42643aaf6919956f89923a50571b0ebfe92d142/torch-2.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:695ba920f234ad4170c9c50e28d56c848432f8f530e6bc7f88fcb15ddf338e75", size = 109503850, upload-time = "2025-10-15T15:50:24.118Z" }, ] [[package]] @@ -3645,39 +4935,32 @@ name = "torchvision" version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "pillow" }, - { name = "torch" }, + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pillow", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "torch", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/63/5b/1404eeab00819df71a30e916c2081654366741f7838fcc4fff86b7bd9e7e/torchvision-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8d5e667deff87bd66d26df6d225f46224bb0782d4f3f8f5d2f3068b5fd4492", size = 1891723, upload-time = "2025-10-15T15:51:08.5Z" }, { url = "https://files.pythonhosted.org/packages/88/e3/1b003ecd52bd721f8304aeb66691edfbc2002747ec83d36188ad6abab506/torchvision-0.24.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a110a51c75e89807a8382b0d8034f5e180fb9319570be3389ffd3d4ac4fd57a9", size = 2418988, upload-time = "2025-10-15T15:51:25.195Z" }, { url = "https://files.pythonhosted.org/packages/56/2e/3c19a35e62da0f606baf8f6e2ceeab1eb66aaa2f84c6528538b06b416d54/torchvision-0.24.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:81d5b12a6df1bb2cc8bdbad837b637d6ea446f2866e6d94f1b5d478856331be3", size = 8046769, upload-time = "2025-10-15T15:51:15.221Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1d/e7ab614a1ace820a2366eab1532679fbe81bd9501ffd6a1b7be14936366d/torchvision-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:0839dbb305d34671f5a64f558782095134b04bbeff8b90f11eb80515d7d50092", size = 3686529, upload-time = "2025-10-15T15:51:20.982Z" }, { url = "https://files.pythonhosted.org/packages/a3/17/54ed2ec6944ea972b461a86424c8c7f98835982c90cbc45bf59bd962863a/torchvision-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f771cf918351ad509a28488be475f3e9cc71a750d6b1467842bfb64863a5e986", size = 1891719, upload-time = "2025-10-15T15:51:10.384Z" }, { url = "https://files.pythonhosted.org/packages/f8/07/0cd6776eee784742ad3cb2bfd3295383d84cb2f9e87386119333d1587f0f/torchvision-0.24.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbd63bf4ebff84c48c50123eba90526cc9f794fe45bc9f5dd07cec19e8c62bce", size = 2420513, upload-time = "2025-10-15T15:51:18.087Z" }, { url = "https://files.pythonhosted.org/packages/1a/f4/6026c08011ddcefcbc14161c5aa9dce55c35c6b045e04ef0952e88bf4594/torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:78fe414b3bb6dbf7e6f6da6f733ba96881f6b29a9b997228de7c5f603e5ed940", size = 8048018, upload-time = "2025-10-15T15:51:13.579Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b4/362b4e67ed87cee0fb4f8f0363a852eaeef527968bf62c07ed56f764d729/torchvision-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:629584b94e52f32a6278f2a35d85eeaae95fcc38730fcb765064f26c3c96df5d", size = 4027686, upload-time = "2025-10-15T15:51:19.189Z" }, { url = "https://files.pythonhosted.org/packages/47/ef/81e4e69e02e2c4650b30e8c11c8974f946682a30e0ab7e9803a831beff76/torchvision-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c61d40bcd2e2451e932902a702ad495ba1ec6f279e90b1e15cef2bb55dc911e2", size = 1891726, upload-time = "2025-10-15T15:51:16.977Z" }, { url = "https://files.pythonhosted.org/packages/00/7b/e3809b3302caea9a12c13f3adebe4fef127188438e719fd6c8dc93db1da6/torchvision-0.24.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b0531d1483fc322d7da0d83be52f0df860a75114ab87dbeeb9de765feaeda843", size = 2419495, upload-time = "2025-10-15T15:51:11.885Z" }, { url = "https://files.pythonhosted.org/packages/7e/e6/7324ead6793075a8c75c56abeed1236d1750de16a5613cfe2ddad164a92a/torchvision-0.24.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:26b9dd9c083f8e5f7ac827de6d5b88c615d9c582dc87666770fbdf16887e4c25", size = 8050480, upload-time = "2025-10-15T15:51:24.012Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ad/3c56fcd2a0d6e8afa80e115b5ade4302232ec99655220a51d05709819523/torchvision-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:060b7c50ed4b3fb0316b08e2e31bfd874ec2f63ef5ae02f81e54341ca4e88703", size = 4292225, upload-time = "2025-10-15T15:51:27.699Z" }, { url = "https://files.pythonhosted.org/packages/4f/b5/b2008e4b77a8d6aada828dd0f6a438d8f94befa23fdd2d62fa0ac6e60113/torchvision-0.24.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84d79cfc6457310107ce4d712de7a3d388b24484bc9aeded4a76d8f8e3a2813d", size = 1891722, upload-time = "2025-10-15T15:51:28.854Z" }, { url = "https://files.pythonhosted.org/packages/8f/02/e2f6b0ff93ca4db5751ac9c5be43f13d5e53d9e9412324f464dca1775027/torchvision-0.24.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fec12a269cf80f6b0b71471c8d498cd3bdd9d8e892c425bf39fecb604852c3b0", size = 2371478, upload-time = "2025-10-15T15:51:37.842Z" }, { url = "https://files.pythonhosted.org/packages/77/85/42e5fc4f716ec7b73cf1f32eeb5c77961be4d4054b26cd6a5ff97f20c966/torchvision-0.24.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7323a9be5e3da695605753f501cdc87824888c5655d27735cdeaa9986b45884c", size = 8050200, upload-time = "2025-10-15T15:51:46.276Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/48cb0b6b26276d2120b1e0dbc877579a748eae02b4091a7522ce54f6d5e1/torchvision-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:08cad8b204196e945f0b2d73adee952d433db1c03645851d52b22a45f1015b13", size = 4309939, upload-time = "2025-10-15T15:51:39.002Z" }, { url = "https://files.pythonhosted.org/packages/7d/d7/3dd10830b047eeb46ae6b465474258d7b4fbb7d8872dca69bd42449f5c82/torchvision-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ab956a6e588623353e0f20d4b03eb1656cb4a3c75ca4dd8b4e32e01bc43271a", size = 2028355, upload-time = "2025-10-15T15:51:22.384Z" }, { url = "https://files.pythonhosted.org/packages/f7/cf/2d7e43409089ce7070f5336161f9216d58653ee1cb26bcb5d6c84cc2de36/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b1b3db80609c32a088554e8e94b4fc31f1033fe5bb4ac0673ec49c3eb03fb4da", size = 2374466, upload-time = "2025-10-15T15:51:35.382Z" }, { url = "https://files.pythonhosted.org/packages/e9/30/8f7c328fd7e0a9665da4b6b56b1c627665c18470bfe62f3729ad3eda9aec/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:e6635f100d455c80b43f297df4b8585a76c6a2e114802f6567ddd28d7b5479b0", size = 8217068, upload-time = "2025-10-15T15:51:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/55/a2/b6f9e40e2904574c80b3bb872c66af20bbd642053e7c8e1b9e99ab396535/torchvision-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4ce158bbdc3a9086034bced0b5212888bd5b251fee6d08a9eff151d30b4b228a", size = 4273912, upload-time = "2025-10-15T15:51:33.866Z" }, { url = "https://files.pythonhosted.org/packages/1b/24/790a39645cc8c71bf442d54a76da9bda5caeb2a44c5f7e02498649cd99d4/torchvision-0.24.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4bdfc85a5ed706421555f32cdc5e3ddb6d40bf65ef03a274ce3c176393e2904b", size = 2028335, upload-time = "2025-10-15T15:51:26.252Z" }, { url = "https://files.pythonhosted.org/packages/b0/d7/69479a066ea773653e88eda99031e38681e9094046f87cb957af5036db0e/torchvision-0.24.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:73576a9c4a593223fbae85a64e8bbd77049abd1101893ecf3c5e981284fd58b4", size = 2371609, upload-time = "2025-10-15T15:51:29.859Z" }, { url = "https://files.pythonhosted.org/packages/46/64/3c7fdb3771ec992b9445a1f7a969466b23ce2cdb14e09303b3db351a0655/torchvision-0.24.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:dd565b1b06666ff399d0801d4d1824fa570c0167a179ca700a5be232527b3c62", size = 8214918, upload-time = "2025-10-15T15:51:41.465Z" }, - { url = "https://files.pythonhosted.org/packages/58/51/abc416bc34d574ad479af738e413d9ebf93027ee92d0f4ae38f966b818f7/torchvision-0.24.0-cp314-cp314-win_amd64.whl", hash = "sha256:eb45d12ac48d757738788fd3fb8e88e647d6b2ab2424134ca87556efc72d81b5", size = 4257776, upload-time = "2025-10-15T15:51:42.642Z" }, { url = "https://files.pythonhosted.org/packages/08/f7/261d1353c611820541ecd43046b89da3f1ae998dc786e4288b890a009883/torchvision-0.24.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:68120e7e03c31900e499a10bb7fdd63cfd67f0054c9fa108e7e27f9cd372f315", size = 2028359, upload-time = "2025-10-15T15:51:32.119Z" }, { url = "https://files.pythonhosted.org/packages/a2/fd/615d8a86db1578345de7fa1edaf476fbcf4f057bf7e4fd898306b620c487/torchvision-0.24.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:64e54494043eecf9f57a9881c6fdea49c62282782e737c002ae8b1639e6ea80e", size = 2374469, upload-time = "2025-10-15T15:51:40.19Z" }, { url = "https://files.pythonhosted.org/packages/04/98/bac11e8fdbf00d6c398246ff2781370aa72c99f2ac685c01ce79354c9a32/torchvision-0.24.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:75ef9546323b321a451239d886f0cb528f7e98bb294da47a3200effd4e572064", size = 8217060, upload-time = "2025-10-15T15:51:45.033Z" }, - { url = "https://files.pythonhosted.org/packages/47/6f/9fba8abc468c904570699eceeb51588f9622172b8fffa4ab11bcf15598c2/torchvision-0.24.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2efb617667950814fc8bb9437e5893861b3616e214285be33cbc364a3f42c599", size = 4358490, upload-time = "2025-10-15T15:51:43.884Z" }, ] [[package]] @@ -3694,9 +4977,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] @@ -3722,6 +5011,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/b7/1dec8433ac604c061173d0589d99217fe7bf90a70bdc375e745d044b8aad/triton-3.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:317fe477ea8fd4524a6a8c499fb0a36984a56d0b75bf9c9cb6133a1c56d5a6e7", size = 170580176, upload-time = "2025-10-13T16:38:31.14Z" }, ] +[[package]] +name = "typer-slim" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20250915" @@ -3745,31 +5047,97 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uart-devices" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/08/a8fd6b3dd2cb92344fb4239d4e81ee121767430d7ce71f3f41282f7334e0/uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34", size = 5167, upload-time = "2025-02-22T16:47:05.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/64/edf33c2d7fba7d6bf057c9dc4235bfc699517ea4c996240a1a9c2bf51c29/uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123", size = 4827, upload-time = "2025-02-22T16:47:04.286Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/44/2ef5e7218ad021fda3fedcb6c1347dd3bf972e9cbdea94644aaa7e4884bb/ulid_transform-1.5.2.tar.gz", hash = "sha256:9a5caf279ec21789ddc2f36b9008ce33a3197d7d66fdd7628fbebec9ba778829", size = 14247, upload-time = "2025-10-04T20:57:22.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/fc/ad77d487c7eaee709c041b8a9b0331d5ddcc2a37b492b133482f2a268676/ulid_transform-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:591f669a29cac276863fea87569a02932b6cc1f53f622862adaefc3f27d45fec", size = 41258, upload-time = "2025-10-04T21:03:35.998Z" }, + { url = "https://files.pythonhosted.org/packages/40/c0/bdcda5be74bb6330b0a26c1018556a27345af4d2cba19519697a8d2adafc/ulid_transform-1.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7dcda634b26ed9f084c1c16b891c8026b5dac5d0cb7a3bd34837c3031d1cf4eb", size = 41948, upload-time = "2025-10-04T21:03:37.191Z" }, + { url = "https://files.pythonhosted.org/packages/8f/76/33b630386adacacbc9edd37d51313e900da32517bf50f83b0b084611004f/ulid_transform-1.5.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e4c07ccbe3152ff2d3c2c3981005e5942ba855cadab1f40904c5f68b9b02c9", size = 48390, upload-time = "2025-10-04T21:03:38.588Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/803d3d072355e9349e1ca6c3c3b0b6e36b37627035f64dc5364e621072b4/ulid_transform-1.5.2-cp311-cp311-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1de8fdf788583b9c785645be8bd7af4c4cc5c2f90121fdd1e08ee95cb83d5e99", size = 45693, upload-time = "2025-10-04T21:03:39.93Z" }, + { url = "https://files.pythonhosted.org/packages/56/ed/2b91522fafe4bfe4af3d2c66e413a623f05e1fffe8d73bc0e0b8b7205eb0/ulid_transform-1.5.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee54c4ff4e7e81648d9ccb09f6723e021585ac5479c64623eb80f55788ef6a64", size = 48983, upload-time = "2025-10-04T21:03:40.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/715c3636d2e2cc0567eb38ee93a1153b70cf266fb60db54bf2137284497a/ulid_transform-1.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67a24f546d4a6ac59049165c1764d5731371f829b47ad0f879b780070a360c31", size = 1027571, upload-time = "2025-10-04T21:03:42.487Z" }, + { url = "https://files.pythonhosted.org/packages/fc/64/a6994684f4ba97310126d370219c46dfcbeecc80d8284d3174d0948c6cd0/ulid_transform-1.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ae1e2a9c299ff98498470a23cd4a8ea7491225697565189aca5f4bba81573a9a", size = 893742, upload-time = "2025-10-04T21:03:44.115Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4f/7814189b6c4f9d82d04ecdc1721bc2aeb639826f582670d7bc3b9912f56c/ulid_transform-1.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8b3d1525a2e3ac785c120c3858aee2ca17d90b6903936c7ff6429e2c9ec2b090", size = 1079949, upload-time = "2025-10-04T21:03:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3a/7b9e8cfa2739fd31bdfc0e0e9065a237c2b4a1164331eb0a859e46b3dee8/ulid_transform-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2fbadf7fc2b72b1ab0ae176d42492e2b8f69817db32e653fe8d2817ca5b1a714", size = 41637, upload-time = "2025-10-04T21:03:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/f9c05b0c2b341db1adbffa2507665557cd1cce403150174322537e4d5532/ulid_transform-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5766373871bdf874e3468c1e1e9c291e628223fbc085ca10a961f26f720fe9d", size = 42186, upload-time = "2025-10-04T21:03:50.736Z" }, + { url = "https://files.pythonhosted.org/packages/25/c3/6903a4d068b2e93727ee5c590aa8025da6a7927b10718e624129ad5daf22/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5074c362d7bcf53b4a5acaeb0416542ba3838ebabe54d60c3b6ac920aa612d6c", size = 49292, upload-time = "2025-10-04T21:03:52.615Z" }, + { url = "https://files.pythonhosted.org/packages/7f/aa/36cf33b3a514f295ec379908597872ae5c3d334bec942777d6afe6449c60/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e986b5079764350a586006b6bfda04f9ae87170f03efe377a3a6ec3e0ac086e6", size = 46576, upload-time = "2025-10-04T21:03:53.574Z" }, + { url = "https://files.pythonhosted.org/packages/85/7f/f39c7b8c7987c82f01384c615a75bcf6bc3b37fab8a64029d40c71752fa5/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba5f9ed304f956e83c29642704a0f814c05c23c68cc028a27bd234acc2f04782", size = 49702, upload-time = "2025-10-04T21:03:54.611Z" }, + { url = "https://files.pythonhosted.org/packages/1e/45/68cb9b8a86bdabc3ba06a00451a6c8b720213cc7eb56adc1b3c741bbe421/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2303395992e6a3d95f589173a78952a8690bf3b8bd97d248eee580ad9c1898eb", size = 1028866, upload-time = "2025-10-04T21:03:55.708Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b1/17a9fbddf4371ec04f958ec157e10f4b2361829b9f9c7263b88a2cf8906a/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:07cd1b250293d731e3fcf1ad3a4eb260e78f03c51676bf96d4e126c77eca48c4", size = 894658, upload-time = "2025-10-04T21:03:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/43/421cbc0f751d09b09b563f69ea4b99a95c8a269417b6ff3266e27a5a8f3b/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:547a824a7db7436bc68afd39ccdec0ce5f7e0b235897cc7f01ab148257db9ab9", size = 1080539, upload-time = "2025-10-04T21:03:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/32/e5/f0d51b4b67e91dae04416623d81017863ea576dfa3ead2a8639761564423/ulid_transform-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:74ba0045e2ab94be1fa6a7901f9958cef6d35cda58546cdfbadc7129ebcdc88b", size = 41138, upload-time = "2025-10-04T21:04:02.198Z" }, + { url = "https://files.pythonhosted.org/packages/43/07/a80882cbc9557996996aae583c2d98bd90e54573390ae1332fed1a7e0124/ulid_transform-1.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6354467dab6aa922cdd7e4a8a2da31222d07609616df167e656dac7244d0f658", size = 41495, upload-time = "2025-10-04T21:04:03.173Z" }, + { url = "https://files.pythonhosted.org/packages/23/81/768b26d31ba4e7002abfd507e2ed42731e8bc4de5c38d10b5530ec19d52f/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4faec817e9e5a031d4887c69e1254c428683c62e6f26ae9fd2f0a330a7c4c85", size = 48781, upload-time = "2025-10-04T21:04:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/13f10f9cf258f4faf8340f9e7dbb9c001340c27b3b68a4baa9688af2b428/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40897a0189cef7cce7c0b26bcff9daafa9df2ce68249e7e6095090a6372ed6ac", size = 45918, upload-time = "2025-10-04T21:04:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/38/9d/aed53563a544556a2c547e33b950a861d006feeda1ded38e79ade6212672/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:397a2d6c2030a3c3d572dfa35c27c647911219daba93056a80091f76888f597e", size = 49232, upload-time = "2025-10-04T21:04:06.123Z" }, + { url = "https://files.pythonhosted.org/packages/c5/76/47c2a1940c732a3842163668a7ebe11796fc02c1fa170c4abe9868a78193/ulid_transform-1.5.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:6794612dbc085abdac5ee3c4e3ec141ab8eb0e7b31f94f56111cacbe36137339", size = 49269, upload-time = "2025-10-04T20:57:20.283Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/d15aa9cfe960ce9fe8e1a14a4b7222c928862ae14128e7480bcee142b546/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497e2dbca8b53c0d072da2c662ac8779faf698ccd52827932a8dcbc5d15960d4", size = 1028355, upload-time = "2025-10-04T21:04:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/37/d259be5021e95d443d8659e1bc2dd90b2248d6ef5ce8e4c97c434fe688ae/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a51bab261fc5a50e1eb81f022129ffc98e64b917a69b29ca8411a00464c09d47", size = 894259, upload-time = "2025-10-04T21:04:08.663Z" }, + { url = "https://files.pythonhosted.org/packages/15/1d/568e1abf4089d4f7b8bcef09abe99fd4b2a56c9ca1ea81fc94e8d7a3cdff/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52f728769c822a52c05310598d949541d1e929dd1a481b2e73c971beea089a2a", size = 1080159, upload-time = "2025-10-04T21:04:10.321Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c5/8eb0aa7bcd5cfb772ae8535b63d8d5fe7503c3d0adda931e7ee7e5e9af39/ulid_transform-1.5.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bae1b0f6041bd8c7d49019c14fb94e42ccab2b59083e7b0cb9f4d13483d7435a", size = 41085, upload-time = "2025-10-04T21:04:13.468Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ff/45cfb8ceaa67eea28c7a1a90242c1ade01b2a1b714feec7af47c086c6945/ulid_transform-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9c4a61fd13ced6b0b96f5983ef4e57ad8adefed4361b6d0f55a2bbfbb18b17d8", size = 41575, upload-time = "2025-10-04T21:04:14.379Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/ab80688863e7228732736ec39764910891b0978ae0d1953395ce2d505cdc/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8065ddfd43827b1299a64da4437161a4f3fa1f03c05d838a3a8c82bc1d014518", size = 48948, upload-time = "2025-10-04T21:04:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/9bd352aac0fddf167a70dcab386cc4d8d099676531a89afa5019c6f1dbe7/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b60965067d13942b0eaaaf986da5eff4ba8f1261b379ca1dac78afe47d940f1a", size = 45789, upload-time = "2025-10-04T21:04:16.861Z" }, + { url = "https://files.pythonhosted.org/packages/cb/90/0b4b4e0ac6061ea90cbdc526e17a75aad0fefafadbe43c56bfd7a77b8a85/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e97a11b56b9e5537ef4521a97fc095b49d849c0ac0ec8d30a2974bd69e5948d", size = 49351, upload-time = "2025-10-04T21:04:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7b/57da5afcd538306c44c093ef314bef4b04768f04b3c509144ed201b7d374/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4915062eee740eefa937459ef468f7f1e35bd2ad5bffdf4245051d656df2c4", size = 1028528, upload-time = "2025-10-04T21:04:19.278Z" }, + { url = "https://files.pythonhosted.org/packages/a1/96/5d3c3464bb64b4fd6b6605787b3a4fef982ba207ba8a8ecc432543fe954e/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7d4cf4bb26fe102dfd1bd10c5b18712fe7640433839c8d9dd20e2d8ccefa972d", size = 894080, upload-time = "2025-10-04T21:04:20.548Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/05dc2a2b2f981617a3ba1cdd8277b86504c4293feefc3a3ba342bac7cbec/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad4953675e6dec400de633087f61cbb38d0ad978d57b60cc3539f7b821d9559", size = 1080292, upload-time = "2025-10-04T21:04:22.092Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/4f74de4fe96bb85fcc1e7fd572527aafd0999d48de3f6d1a41c66cdebc41/ulid_transform-1.5.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:08286ccc6bac0107e1bd5415a28e730d88089293ba5ce51dc5883175eccc31e2", size = 68442, upload-time = "2025-10-04T21:04:25.66Z" }, + { url = "https://files.pythonhosted.org/packages/03/58/854e4bd4539be70e5714002198e0565f897cf5b65203d9c230b362cc41df/ulid_transform-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed0b533c936cb120312cd98ca1c8ec1f8af66bac6bc08426c030b48291d5505e", size = 69113, upload-time = "2025-10-04T21:04:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/6e/45/73d6aa7c63e1e472bbe8e54a0892cdec78dc8438798e9ea518f1c371f640/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58617bae6fc21507f5151328faf7b77c6ba6a615b42efd18f494564354a3ce68", size = 85341, upload-time = "2025-10-04T21:04:28.046Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fb/263774b2249d683addd0de5746d4c4debbb33966277d4d33390150944ba3/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e24e68971a64a04af2d0b3df98bfe0087c85d35a1b02fa0bbf43a3a0a99dccf6", size = 78616, upload-time = "2025-10-04T21:04:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/2eda3645a002a9fd141c19fd7416de87adaa12b25f8916b42b78644dbddd/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb5da66ec5e7d97f695dd16637d5a8816bb9661df43ff1f2de0d46071d96a7a8", size = 85582, upload-time = "2025-10-04T21:04:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/e08e975e4ed61fb2ae7014591fcc7e5f1a62966c7ed53fc1c95c3da78923/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:397646cf156aa46456cd8504075d117d2983ebf2cff01955c3add6280d0fb3c8", size = 1066091, upload-time = "2025-10-04T21:04:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e7/43474bf4ec56eadf2509939585894ef094dc364143596f62639bebd6d42e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:dc5ac2ffa704a21df2a36cea47ac1022fb6f8ab03abe31a5f7b81312f972e2c2", size = 926160, upload-time = "2025-10-04T21:04:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/ca/48/36df80548d96ffdaa3ae124854bbd5a0a0b07da22a8d22b301e5ec17de6e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6f29b8004fba0da7061b5eecf6101c6283377b6cd04f3626089cc67d9342c8fd", size = 1116228, upload-time = "2025-10-04T21:04:35.325Z" }, +] + [[package]] name = "ultralytics" version = "8.3.226" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib" }, - { name = "numpy" }, - { name = "opencv-python" }, - { name = "pillow" }, - { name = "polars" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "torch" }, - { name = "torchvision" }, - { name = "ultralytics-thop" }, + { name = "matplotlib", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "opencv-python", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pillow", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "polars", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "psutil", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "scipy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "torch", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "torchvision", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "ultralytics-thop", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/a3/b367da00e675b2ac1685427c77b4e3b8f79001db2e0a631a31611c58e569/ultralytics-8.3.226.tar.gz", hash = "sha256:fd259e398bae94c468224ab626192643402665e19eca6c36713ee813323a15d0", size = 926684, upload-time = "2025-11-07T17:33:04.276Z" } wheels = [ @@ -3781,8 +5149,8 @@ name = "ultralytics-thop" version = "2.0.18" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "torch" }, + { name = "numpy", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "torch", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/63/21a32e1facfeee245dbdfb7b4669faf7a36ff7c00b50987932bdab126f4b/ultralytics_thop-2.0.18.tar.gz", hash = "sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf", size = 34557, upload-time = "2025-10-29T16:58:13.526Z" } wheels = [ @@ -3798,20 +5166,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[[package]] +name = "usb-devices" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/48/dbe6c4c559950ebebd413e8c40a8a60bfd47ddd79cb61b598a5987e03aad/usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d", size = 5421, upload-time = "2023-12-16T19:59:53.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c9/26171ae5b78d72dd006bbc51ca9baa2cbb889ae8e91608910207482108fd/usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf", size = 5349, upload-time = "2023-12-16T19:59:51.604Z" }, +] + +[[package]] +name = "uv" +version = "0.9.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/1a/cb0c37ae8513b253bcbc13d42392feb7d95ea696eb398b37535a28df9040/uv-0.9.17.tar.gz", hash = "sha256:6d93ab9012673e82039cfa7f9f66f69b388bc3f910f9e8a2ebee211353f620aa", size = 3815957, upload-time = "2025-12-09T23:01:21.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/e2/b6e2d473bdc37f4d86307151b53c0776e9925de7376ce297e92eab2e8894/uv-0.9.17-py3-none-linux_armv6l.whl", hash = "sha256:c708e6560ae5bc3cda1ba93f0094148ce773b6764240ced433acf88879e57a67", size = 21254511, upload-time = "2025-12-09T23:00:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:233b3d90f104c59d602abf434898057876b87f64df67a37129877d6dab6e5e10", size = 20384366, upload-time = "2025-12-09T23:01:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4b8e5513d48a267bfa180ca7fefaf6f27b1267e191573b3dba059981143e88ef", size = 18924624, upload-time = "2025-12-09T23:01:10.291Z" }, + { url = "https://files.pythonhosted.org/packages/21/56/9daf8bbe4a9a36eb0b9257cf5e1e20f9433d0ce996778ccf1929cbe071a4/uv-0.9.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8f283488bbcf19754910cc1ae7349c567918d6367c596e5a75d4751e0080eee0", size = 20671687, upload-time = "2025-12-09T23:00:51.927Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c8/4050ff7dc692770092042fcef57223b8852662544f5981a7f6cac8fc488d/uv-0.9.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9cf8052ba669dc17bdba75dae655094d820f4044990ea95c01ec9688c182f1da", size = 20861866, upload-time = "2025-12-09T23:01:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/208e62b7db7a65cb3390a11604c59937e387d07ed9f8b63b54edb55e2292/uv-0.9.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06749461b11175a884be193120044e7f632a55e2624d9203398808907d346aad", size = 21858420, upload-time = "2025-12-09T23:01:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/86/2c/91288cd5a04db37dfc1e0dad26ead84787db5832d9836b4cc8e0fa7f3c53/uv-0.9.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:35eb1a519688209160e48e1bb8032d36d285948a13b4dd21afe7ec36dc2a9787", size = 23471658, upload-time = "2025-12-09T23:00:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/44/ba/493eba650ffad1df9e04fd8eabfc2d0aebc23e8f378acaaee9d95ca43518/uv-0.9.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bfb60a533e82690ab17dfe619ff7f294d053415645800d38d13062170230714", size = 23062950, upload-time = "2025-12-09T23:00:39.055Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/f7f679503c06843ba59451e3193f35fb7c782ff0afc697020d4718a7de46/uv-0.9.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd0f3e380ff148aff3d769e95a9743cb29c7f040d7ef2896cafe8063279a6bc1", size = 22080299, upload-time = "2025-12-09T23:00:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2c3d25fbd8f91b30d0fac69a13b8e2c2cd8e606d7e6e924c1423e4ff84e616", size = 22087554, upload-time = "2025-12-09T23:00:41.715Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:330e7085857e4205c5196a417aca81cfbfa936a97dd2a0871f6560a88424ebf2", size = 20823225, upload-time = "2025-12-09T23:00:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/e0f816cacd802a1cb25e71de9d60e57fa1f6c659eb5599cef708668618cc/uv-0.9.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:45880faa9f6cf91e3cda4e5f947da6a1004238fdc0ed4ebc18783a12ce197312", size = 22004893, upload-time = "2025-12-09T23:01:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/15/6b/700f6256ee191136eb06e40d16970a4fc687efdccf5e67c553a258063019/uv-0.9.17-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8e775a1b94c6f248e22f0ce2f86ed37c24e10ae31fb98b7e1b9f9a3189d25991", size = 20853850, upload-time = "2025-12-09T23:01:02.694Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6a/13f02e2ed6510223c40f74804586b09e5151d9319f93aab1e49d91db13bb/uv-0.9.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8650c894401ec96488a6fd84a5b4675e09be102f5525c902a12ba1c8ef8ff230", size = 21322623, upload-time = "2025-12-09T23:00:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/d0/18/2d19780cebfbec877ea645463410c17859f8070f79c1a34568b153d78e1d/uv-0.9.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:673066b72d8b6c86be0dae6d5f73926bcee8e4810f1690d7b8ce5429d919cde3", size = 22290123, upload-time = "2025-12-09T23:00:54.394Z" }, +] + [[package]] name = "uvicorn" version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "click", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "h11", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] +[[package]] +name = "voluptuous" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/af/a54ce0fb6f1d867e0b9f0efe5f082a691f51ccf705188fca67a3ecefd7f4/voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa", size = 51651, upload-time = "2024-07-02T19:10:00.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/a8/8f9cc6749331186e6a513bfe3745454f81d25f6e34c6024f88f80c71ed28/voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566", size = 31349, upload-time = "2024-07-02T19:09:58.125Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "voluptuous", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/15/ac7a98afd478e9afc804354fe9d9715e0e560a590fdd425b22b65a152bb3/voluptuous_openapi-0.2.0.tar.gz", hash = "sha256:2366be934c37bb5fd8ed6bd5a2a46b1079b57dfbdf8c6c02e88f4ca13e975073", size = 15789, upload-time = "2025-08-21T04:49:16.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/eb/2ae58431a078318f03267f196137282ea6f01ea7f7e0fcba2b25a30b0bf2/voluptuous_openapi-0.2.0-py3-none-any.whl", hash = "sha256:d51f07be8af44b11570b7366785d90daa716b7fd11ea2845803763ae551f35cf", size = 10180, upload-time = "2025-08-21T04:49:15.885Z" }, +] + +[[package]] +name = "voluptuous-serialize" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "voluptuous", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/70/03a9b61324e1bb8b16682455b8b953bccd1001a28e43478c86f539e26285/voluptuous_serialize-2.7.0.tar.gz", hash = "sha256:d0da959f2fd93c8f1eb779c5d116231940493b51020c2c1026bab76eb56cd09e", size = 9202, upload-time = "2025-08-17T10:43:04.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/41/d536d9cf39821c35cc13aff403728e60e32b2fd711c240b6b9980af1c03f/voluptuous_serialize-2.7.0-py3-none-any.whl", hash = "sha256:ee3ebecace6136f38d0bf8c20ee97155db2486c6b2d0795563fafd04a519e76f", size = 7850, upload-time = "2025-08-17T10:43:03.498Z" }, +] + [[package]] name = "wcwidth" version = "0.2.14" @@ -3821,14 +5253,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, ] +[[package]] +name = "webrtc-models" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mashumaro", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "orjson", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/e8/050ffe3b71ff44d3885eee2bed763ca937e2a30bc950d866f22ba657776b/webrtc_models-0.3.0.tar.gz", hash = "sha256:559c743e5cc3bcc8133be1b6fb5e8492a9ddb17151129c21cbb2e3f2a1166526", size = 9411, upload-time = "2024-11-18T17:43:45.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/e7/62f29980c9e8d75af93b642a0c37aa8e201fd5268ba3a7179c172549bac3/webrtc_models-0.3.0-py3-none-any.whl", hash = "sha256:8fddded3ffd7ca837de878033501927580799a2c1b7829f7ae8a0f43b49004ea", size = 7476, upload-time = "2024-11-18T17:43:44.165Z" }, +] + [[package]] name = "yarl" version = "1.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, + { name = "idna", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "multidict", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, + { name = "propcache", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } wheels = [ @@ -3845,9 +5290,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, - { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, - { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, - { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, @@ -3861,9 +5303,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, @@ -3877,9 +5316,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, @@ -3893,9 +5329,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, @@ -3909,9 +5342,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, @@ -3925,9 +5355,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, @@ -3941,8 +5368,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, ] + +[[package]] +name = "zeroconf" +version = "0.148.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ifaddr", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/46/10db987799629d01930176ae523f70879b63577060d63e05ebf9214aba4b/zeroconf-0.148.0.tar.gz", hash = "sha256:03fcca123df3652e23d945112d683d2f605f313637611b7d4adf31056f681702", size = 164447, upload-time = "2025-10-05T00:21:19.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/47/a2ff13f3a0a7b9bd4cc1a904e7ddfd4f327043387915607db1117e1c1417/zeroconf-0.148.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9146731bb82bc7b42f009aa69619b17a4b6ddecc75eee9a59249c12c804d0637", size = 1708548, upload-time = "2025-10-05T01:07:30.722Z" }, + { url = "https://files.pythonhosted.org/packages/54/11/7c871eba676458e5f3943e45281db91c3b01743b8e7f5401640855ca863e/zeroconf-0.148.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db24dc2e5367dc61bacbf302b7c85cc10ee1a9de8f1710380027992afd1ddcb4", size = 1682018, upload-time = "2025-10-05T01:07:33.879Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/62149096a758e6036625a66b1053af8600126cb8e50ca8adcb705732e211/zeroconf-0.148.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2202ac7dc2777249561292c9151919d70fbe25a31983b7e127b43878ea67483c", size = 2195888, upload-time = "2025-10-05T01:07:35.833Z" }, + { url = "https://files.pythonhosted.org/packages/c5/35/9ace30a86ec42b11a1904d63fa260c166c972a743079d6ffaa69f8085924/zeroconf-0.148.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:876e9e61a7065d201d39c466449e01fa9e19c3c7b2c5ee57bc628f15e21653fb", size = 1970965, upload-time = "2025-10-05T01:07:37.809Z" }, + { url = "https://files.pythonhosted.org/packages/6a/15/b463e84c221ccbbc04a6d4f69a2e2c2c005ce04a233ec2d398987f50177f/zeroconf-0.148.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:171ff9d59283737946d79c6a290a597a3d10d0d24d6a3a87de67ce3064157afc", size = 2269837, upload-time = "2025-10-05T01:07:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/85/9d/b56979c5abb14d18790eca845428b417990177ebd9674b1b1f61491efd3b/zeroconf-0.148.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:76d53985fa40cefb3a82c1d5d761217392bbc811964715e1bf73e74084012062", size = 2224442, upload-time = "2025-10-05T01:07:41.816Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/6b6cf5d1f75f464cb697bc4fcb59baaa792485d5f8a29ac460ed75d1afe8/zeroconf-0.148.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:429e8ed8428737f2586992aaf11a21302184cd4e1c641fbd7abe8946d9ff7089", size = 2017165, upload-time = "2025-10-05T01:07:43.708Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/cce930ce7d57da67ce7662b188907415441c3edfca4aac97be53049c0cc9/zeroconf-0.148.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aa0cdcb91f231789d8f6ba7ed702d05a36975e7b06fd663aff25205ddca2b659", size = 2293150, upload-time = "2025-10-05T01:07:45.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/1f/dcaed909fabbdf760739b3081cbea3f7cd564e61a708dca00a55960da11c/zeroconf-0.148.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b923e26369e302863aa5370eff4d4d72a0b90ba85d3b9f608c62cbab78f14dc2", size = 1723036, upload-time = "2025-10-05T01:07:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/05/37/849d419ccd60e37e02ca7364ac9451e500e517cebf884bee88e6811c442b/zeroconf-0.148.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cbffd751877b74cd64c529061e5a524ebfa59af16930330548033e307701fee", size = 1696983, upload-time = "2025-10-05T01:07:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/1511a2f10e22e51e391638dc58786af74c996513b6588c9219f085cc898e/zeroconf-0.148.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34275f60a5ab01d2d0a190662d16603b75f9225cee4ab58d388ff86d8011352a", size = 2208149, upload-time = "2025-10-05T01:07:54.7Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8e/744b4cc8d9ee314be5fe3fbd597baef4cc3998d24d70ac1265dfa7750544/zeroconf-0.148.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:556ff0b9dfc0189f33c6e6110aa23d9f7564a7475f4cdc624a0584c1133ae44b", size = 1978151, upload-time = "2025-10-05T01:07:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/42/1f/d8b365a3f3979ea3f0ecb02c22c61d2cdc4fc5bc0bc182ff52547935923c/zeroconf-0.148.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8aa15461e35169b4ec25cc45ec82750023e6c2e96ebc099a014caaf544316f7", size = 2285527, upload-time = "2025-10-05T01:07:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/0a27fd233240a911a58eae6037357f0d088cdcbff295cf01ad05a0b91bd6/zeroconf-0.148.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:25b8b72177bbe0792f5873c16330d759811540edb24ed9ead305874183eaefd5", size = 2237619, upload-time = "2025-10-05T01:07:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/56d6155eb3b8a77a11dcf0c77ef747c46a1c778d8957b818dfdc0578886e/zeroconf-0.148.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:10ce75cdb524f955773114697633d73644aad6c35daef5315fa478bff9bee24d", size = 2031313, upload-time = "2025-10-05T01:08:01.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/4521a8c469802c16f61e6b12b674a239684d0fe8e51fe66ebe0223ca4d2d/zeroconf-0.148.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49512e6d59be66be769f36e1f7f025a2841783da1be708d4b4a92a7b63135b68", size = 2309244, upload-time = "2025-10-05T01:08:03.311Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/6c08ccbda1e78c8f538d8add49fac2fe49ef85ee34b62877df4154715583/zeroconf-0.148.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aef8699ea47cd47c9219e3f110a35ad50c13c34c7c6db992f3c9f75feec6ef8f", size = 1735431, upload-time = "2025-10-05T01:08:09.375Z" }, + { url = "https://files.pythonhosted.org/packages/cb/37/6b91c4a4258863e485602e6b1eb098fe406142a653112e8719c49b69afc4/zeroconf-0.148.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9097e7010b9f9a64e5f2084493e9973d446bd85c7a7cbef5032b2b0a2ecc5a12", size = 1701594, upload-time = "2025-10-05T01:08:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/5eaaf66d39b3bccc17b52187eebb2dde93f761f4ee8b6c83b8fe764273f5/zeroconf-0.148.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdc566c387260fb7bf89f91d00460d0c9b9373dfddcf1fcc980ab3f7270154f9", size = 2134103, upload-time = "2025-10-05T01:08:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/19/a5/e4ebe7b5fbea512fe13efb466d855124126d2f531a18216c7cb509b8a4dd/zeroconf-0.148.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10cbd4134cacc22c3b3b169d7f782472a1dd36895e1421afa4f681caf181c07b", size = 1930109, upload-time = "2025-10-05T01:08:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/7f7c5cee5279afe2a6a8b9657de9a587ccb34168d7c99acc6d2b40b9d87e/zeroconf-0.148.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde01541e6a45c4d1b6e6d97b532ea241abc32c183745a74021b134d867388d8", size = 2230425, upload-time = "2025-10-05T01:08:16.296Z" }, + { url = "https://files.pythonhosted.org/packages/cd/41/0e1999db76e390fca9eef8257455955445a0386b94ce0ef6ce74896d7e2a/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8ceab8f10ab6fc0847a2de74377663793a974fdba77e7e6ba1ff47679f4bb845", size = 2161052, upload-time = "2025-10-05T01:08:17.976Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/6585fe6308b8f1ac0ac4d37ac69064ec2a36b81cf9080813cb666229694c/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0a8c36c37d8835420fc337be4aaa03c3a34272028919de575124c10d31a7e304", size = 2015005, upload-time = "2025-10-05T01:08:20.318Z" }, + { url = "https://files.pythonhosted.org/packages/74/ec/a9d0a577be157170f513e6ad6ebb3cd8dd9602c670d74911e9c5534e1c1d/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:848d57df1bb3b48279ba9b66e6c1f727570e2c8e7e0c4518c2daffaf23419d03", size = 2253785, upload-time = "2025-10-05T01:08:21.971Z" }, + { url = "https://files.pythonhosted.org/packages/46/09/394a24a633645063557c5144c9abb694699df76155dcab5e1e3078dd1323/zeroconf-0.148.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ad889929bdc3953530546a4a2486d8c07f5a18d4ef494a98446bf17414897a7", size = 1714465, upload-time = "2025-10-05T01:08:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/3d/db/f57c4bfcceb67fe474705cbadba3f8f7a88bdc95892e74ba6d85e24d28c3/zeroconf-0.148.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29fb10be743650eb40863f1a1ee868df1869357a0c2ab75140ee3d7079540c1e", size = 1683877, upload-time = "2025-10-05T01:08:30.42Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/b3e2d39c40802a8cc9415357acdb76ff01bc29e25ffaa811771b6fffc428/zeroconf-0.148.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2995e74969c577461060539164c47e1ba674470585cb0f954ebeb77f032f3c2", size = 2122874, upload-time = "2025-10-05T01:08:32.11Z" }, + { url = "https://files.pythonhosted.org/packages/66/eb/0ac2bf51d58d47cfa854628036a7ad95544a1802bc890f3d69649dc35e46/zeroconf-0.148.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5be50346efdc20823f9d68d8757612767d11ceb8da7637d46080977b87912551", size = 1922164, upload-time = "2025-10-05T01:08:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/59/ff/c7372507c7e25ad3499fe08d4678deb1ed41c57f78ff5df43bd2d4d98cfc/zeroconf-0.148.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc88fd01b5552ffb4d5bc551d027ac28a1852c03ceab754d02bd0d5f04c54e85", size = 2214119, upload-time = "2025-10-05T01:08:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/57f0889f47923b4fa4364b62b7b3ffc347f6bad09a25ce4e578b8991a86d/zeroconf-0.148.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:5af260c74187751c0df6a40f38d6fd17cb8658a734b0e1148a86084b71c1977c", size = 2137609, upload-time = "2025-10-05T00:21:15.953Z" }, + { url = "https://files.pythonhosted.org/packages/3b/33/9cb5558695c1377941dbb10a5591f88a787f9e1fba130642693d5c80663b/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6078c73a76d49ba969ca2bb7067e4d58ebd2b79a5f956e45c4c989b11d36e03", size = 2154314, upload-time = "2025-10-05T01:08:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/38/06/cf4e17a86922b4561d85d36f50f1adada1328723e882d95aa42baefa5479/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3e686bf741158f4253d5e0aa6a8f9d34b3140bf5826c0aca9b906273b9c77a5f", size = 2004973, upload-time = "2025-10-05T01:08:39.825Z" }, + { url = "https://files.pythonhosted.org/packages/a4/61/937a405783317639cd11e7bfab3879669896297b6ca2edfb0d2d9c8dbb30/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52d6ac06efe05a1e46089cfde066985782824f64b64c6982e8678e70b4b49453", size = 2237775, upload-time = "2025-10-05T01:08:41.535Z" }, + { url = "https://files.pythonhosted.org/packages/a5/46/ac86e3a3ff355058cd0818b01a3a97ca3f2abc0a034f1edb8eea27cea65c/zeroconf-0.148.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:2158d8bfefcdb90237937df65b2235870ccef04644497e4e29d3ab5a4b3199b6", size = 1714870, upload-time = "2025-10-05T01:08:47.624Z" }, + { url = "https://files.pythonhosted.org/packages/de/02/c5e8cd8dfda0ca16c7309c8d12c09a3114e5b50054bce3c93da65db8b8e4/zeroconf-0.148.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:695f6663bf8df30fe1826a2c4d5acd8213d9cbd9111f59d375bf1ad635790e98", size = 1697756, upload-time = "2025-10-05T01:08:49.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/04/a66c1011d05d7bb8ae6a847d41ac818271a942390f3d8c83c776389ca094/zeroconf-0.148.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa65a24ec055be0a1cba2b986ac3e1c5d97a40abe164991aabc6a6416cc9df02", size = 2146784, upload-time = "2025-10-05T01:08:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/2239d87c3f60f886bd2dd299e9c63b811efd58b8b6fc659d8fd0900db3bc/zeroconf-0.148.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79890df4ff696a5cdc4a59152957be568bea1423ed13632fc09e2a196c6721d5", size = 1899394, upload-time = "2025-10-05T01:08:53.457Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/534a4b576a8f9f5edff648ac9a5417323bef3086a77397f2f2058125a3c8/zeroconf-0.148.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c0ca6e8e063eb5a385469bb8d8dec12381368031cb3a82c446225511863ede3", size = 2221319, upload-time = "2025-10-05T01:08:55.271Z" }, + { url = "https://files.pythonhosted.org/packages/b5/8c/1c8e9b7d604910830243ceb533d796dae98ed0c72902624a642487edfd61/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ece6f030cc7a771199760963c11ce4e77ed95011eedffb1ca5186247abfec24a", size = 2178586, upload-time = "2025-10-05T01:08:56.966Z" }, + { url = "https://files.pythonhosted.org/packages/16/55/178c4b95840dc687d45e413a74d2236a25395ab036f4813628271306ab9d/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c3f860ad0003a8999736fa2ae4c2051dd3c2e5df1bc1eaea2f872f5fcbd1f1c1", size = 1972371, upload-time = "2025-10-05T01:08:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/b599421fe634d9f3a2799f69e6e7db9f13f77d326331fa2bb5982e936665/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ab8e687255cf54ebeae7ede6a8be0566aec752c570e16dbea84b3f9b149ba829", size = 2244286, upload-time = "2025-10-05T01:09:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/36/fb/53d749793689279bc9657d818615176577233ad556d62f76f719e86ead1d/zeroconf-0.148.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:40fe100381365c983a89e4b219a7ececcc2a789ac179cd26d4a6bbe00ae3e8fe", size = 3418152, upload-time = "2025-10-05T01:09:06.71Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/5eb647f7277378cbfdb6943dc8e60c3b17cdd1556f5082ccfdd6813e1ce8/zeroconf-0.148.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b9c7bcae8af8e27593bad76ee0f0c21d43c6a2324cd1e34d06e6e08cb3fd922", size = 3389671, upload-time = "2025-10-05T01:09:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/3134aa54d30a9ae2e2473212eab586fe1779f845bf241e68729eca63d2ab/zeroconf-0.148.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8ba75dacd58558769afb5da24d83da4fdc2a5c43a52f619aaa107fa55d3fdc", size = 4123125, upload-time = "2025-10-05T01:09:11.064Z" }, + { url = "https://files.pythonhosted.org/packages/12/23/4a0284254ebce373ff1aee7240932a0599ecf47e3c711f93242a861aa382/zeroconf-0.148.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75f9a8212c541a4447c064433862fd4b23d75d47413912a28204d2f9c4929a59", size = 3651426, upload-time = "2025-10-05T01:09:13.725Z" }, + { url = "https://files.pythonhosted.org/packages/76/9a/7b79ef986b5467bb8f17b9a9e6eea887b0b56ecafc00515c81d118e681b4/zeroconf-0.148.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be64c0eb48efa1972c13f7f17a7ac0ed7932ebb9672e57f55b17536412146206", size = 4263151, upload-time = "2025-10-05T01:09:15.732Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0a/caa6d05548ca7cf28a0b8aa20a9dbb0f8176172f28799e53ea11f78692a3/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac1d4ee1d5bac71c27aea6d1dc1e1485423a1631a81be1ea65fb45ac280ade96", size = 4191717, upload-time = "2025-10-05T01:09:18.071Z" }, + { url = "https://files.pythonhosted.org/packages/46/f6/dbafa3b0f2d7a09315ed3ad588d36de79776ce49e00ec945c6195cad3f18/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8da9bdb39ead9d5971136046146cd5e11413cb979c011e19f717b098788b5c37", size = 3793490, upload-time = "2025-10-05T01:09:20.045Z" }, + { url = "https://files.pythonhosted.org/packages/c4/05/f8b88937659075116c122355bdd9ce52376cc46e2269d91d7d4f10c9a658/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6e3dd22732df47a126aefb5ca4b267e828b47098a945d4468d38c72843dd6df", size = 4311455, upload-time = "2025-10-05T01:09:22.042Z" }, +] + +[[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" }, +] From 013e0588d67fb88904b1a30c186f37347be7cf14 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Feb 2026 07:08:17 +0000 Subject: [PATCH 30/31] docs: add Docker setup guide for HA Container users Comprehensive onboarding documentation for running HomeSec alongside Home Assistant Docker. Covers quick start, docker-compose setup, network configurations, camera setup, and troubleshooting. https://claude.ai/code/session_019LEwJ9ARyfpJqZMqTPpZxn --- homeassistant/integration/DOCKER.md | 379 ++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 homeassistant/integration/DOCKER.md diff --git a/homeassistant/integration/DOCKER.md b/homeassistant/integration/DOCKER.md new file mode 100644 index 00000000..02e46c21 --- /dev/null +++ b/homeassistant/integration/DOCKER.md @@ -0,0 +1,379 @@ +# Running HomeSec with Home Assistant Docker + +This guide is for users running Home Assistant as a Docker container (not HA OS). + +If you're using **Home Assistant OS**, use the [HomeSec Add-on](../addon/homesec/DOCS.md) instead for a zero-config experience. + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Your Docker Host │ +│ │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Home Assistant Container │ │ HomeSec All-in-One │ │ +│ │ │ │ │ │ +│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │ +│ │ │ Your existing config │ │ │ │ HomeSec App │ │ │ +│ │ │ Your automations │ │ │ │ (FastAPI) │ │ │ +│ │ │ Your integrations │ │ │ └──────────┬────────────┘ │ │ +│ │ └───────────────────────┘ │ │ │ │ │ +│ │ │ │ ┌──────────▼────────────┐ │ │ +│ │ ┌───────────────────────┐ │ HTTP │ │ Bundled Postgres │ │ │ +│ │ │ HomeSec Integration │◄─┼──────┼──│ (managed by s6) │ │ │ +│ │ │ (installed via HACS) │ │ │ └───────────────────────┘ │ │ +│ │ └───────────────────────┘ │ │ │ │ +│ │ │ │ │ ┌───────────────────────┐ │ │ +│ │ │ Events API │ │ │ /config volume │ │ │ +│ │ └───────────────┼──────┼─►│ /data volume │ │ │ +│ │ │ │ └───────────────────────┘ │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +│ :8123 :8080 │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Data Flow:** + +``` +┌──────────┐ RTSP/FTP ┌──────────┐ homesec_alert ┌──────────┐ +│ Camera │ ───────────────►│ HomeSec │ ─────────────────►│ Home │ +│ │ │ │ (HA Events API) │Assistant │ +└──────────┘ └──────────┘ └──────────┘ + │ │ + │ REST API │ + │ (cameras, clips, stats) │ + ◄──────────────────────────────┘ + HomeSec Integration polls +``` + +--- + +## Quick Start + +### Step 1: Create a Home Assistant Long-Lived Access Token + +1. In Home Assistant, go to your profile (click your name in the sidebar) +2. Scroll to "Long-Lived Access Tokens" +3. Click "Create Token" +4. Name it `homesec` and copy the token + +### Step 2: Run HomeSec Container + +```bash +docker run -d \ + --name homesec \ + --restart unless-stopped \ + -e HA_URL=http://YOUR_HA_IP:8123 \ + -e HA_TOKEN=YOUR_LONG_LIVED_TOKEN \ + -v homesec-data:/data \ + -v homesec-config:/config \ + -v /path/to/clips:/media/clips \ + -p 8080:8080 \ + ghcr.io/lan17/homesec:latest +``` + +Replace: +- `YOUR_HA_IP` - Your Home Assistant IP address (e.g., `192.168.1.100`) +- `YOUR_LONG_LIVED_TOKEN` - The token from Step 1 +- `/path/to/clips` - Where you want video clips stored + +### Step 3: Install the Integration via HACS + +1. Open HACS in Home Assistant +2. Go to Integrations → Custom repositories +3. Add: `https://github.com/lan17/homesec` (category: Integration) +4. Search for "HomeSec" and install +5. Restart Home Assistant + +### Step 4: Configure the Integration + +1. Go to Settings → Devices & Services → Add Integration +2. Search for "HomeSec" +3. Enter your HomeSec URL: `http://YOUR_DOCKER_HOST_IP:8080` +4. Complete the setup + +--- + +## Docker Compose Setup + +For users who prefer docker-compose, here's a complete setup: + +```yaml +# docker-compose.yml +version: "3.8" + +services: + homesec: + image: ghcr.io/lan17/homesec:latest + container_name: homesec + restart: unless-stopped + environment: + # Home Assistant connection (for sending alerts) + HA_URL: http://homeassistant:8123 + HA_TOKEN: ${HA_TOKEN} # Set in .env file + + # Optional: Use external Postgres instead of bundled + # DATABASE_URL: postgresql+asyncpg://user:pass@postgres:5432/homesec + volumes: + - homesec-data:/data # Postgres data + internal state + - homesec-config:/config # HomeSec configuration + - ./clips:/media/clips # Video clip storage + ports: + - "8080:8080" + networks: + - ha-network + + # Your existing Home Assistant container + homeassistant: + image: ghcr.io/home-assistant/home-assistant:stable + container_name: homeassistant + restart: unless-stopped + volumes: + - ./ha-config:/config + - /etc/localtime:/etc/localtime:ro + ports: + - "8123:8123" + networks: + - ha-network + +networks: + ha-network: + driver: bridge + +volumes: + homesec-data: + homesec-config: +``` + +Create a `.env` file: + +```bash +# .env +HA_TOKEN=your_long_lived_access_token_here +``` + +Then run: + +```bash +docker-compose up -d +``` + +--- + +## Configuration + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `HA_URL` | Yes* | Home Assistant URL for sending alerts | +| `HA_TOKEN` | Yes* | Long-lived access token for HA Events API | +| `DATABASE_URL` | No | External Postgres URL (uses bundled Postgres if not set) | +| `CONFIG_PATH` | No | Path to config.yaml (default: `/config/config.yaml`) | + +*Required for alerts to appear in Home Assistant + +### Volumes + +| Path | Purpose | +|------|---------| +| `/data` | Postgres data, internal state (persist this!) | +| `/config` | HomeSec configuration files | +| `/media/clips` | Video clip storage | + +### Ports + +| Port | Purpose | +|------|---------| +| `8080` | HomeSec REST API and Web UI | + +--- + +## Network Configurations + +### Option A: Shared Docker Network (Recommended) + +Both containers on the same Docker network can communicate by container name: + +``` +HomeSec → http://homeassistant:8123 (HA_URL) +HA Integration → http://homesec:8080 +``` + +### Option B: Host Network + +If using `network_mode: host`: + +``` +HomeSec → http://localhost:8123 or http://YOUR_IP:8123 +HA Integration → http://localhost:8080 or http://YOUR_IP:8080 +``` + +### Option C: Separate Docker Hosts + +If HomeSec runs on a different machine: + +``` +HomeSec → http://HA_MACHINE_IP:8123 +HA Integration → http://HOMESEC_MACHINE_IP:8080 +``` + +--- + +## Camera Configuration + +After starting HomeSec, configure your cameras by editing `/config/config.yaml` or using the REST API. + +### Example config.yaml + +```yaml +cameras: + - name: front_door + enabled: true + source: + backend: rtsp + config: + url_env: FRONT_DOOR_RTSP_URL + + - name: backyard + enabled: true + source: + backend: rtsp + config: + url_env: BACKYARD_RTSP_URL + +storage: + backend: local + config: + base_path: /media/clips + +notifiers: + - backend: home_assistant + config: + url_env: HA_URL + token_env: HA_TOKEN + +server: + enabled: true + host: 0.0.0.0 + port: 8080 +``` + +### Adding Cameras via REST API + +```bash +curl -X POST http://localhost:8080/api/v1/cameras \ + -H "Content-Type: application/json" \ + -d '{ + "name": "front_door", + "enabled": true, + "source_backend": "rtsp", + "source_config": { + "url": "rtsp://user:pass@192.168.1.50:554/stream1" + } + }' +``` + +--- + +## Verifying the Setup + +### Check HomeSec is Running + +```bash +curl http://localhost:8080/api/v1/health +``` + +Expected response: +```json +{ + "status": "healthy", + "pipeline": "running", + "postgres": "connected", + "cameras_online": 2 +} +``` + +### Check Integration Connection + +In Home Assistant: +1. Go to Settings → Devices & Services +2. Find HomeSec +3. Check that entities are available: + - `sensor.homesec_cameras_online` + - `sensor.homesec_alerts_today` + - `binary_sensor.front_door_motion` + - etc. + +### Test Alert Flow + +Trigger motion on a camera and verify: +1. HomeSec processes the clip +2. Alert appears in HA as `homesec_alert` event +3. Camera's motion binary sensor turns on +4. Motion clears after timeout (default: 30s) + +--- + +## Troubleshooting + +### HomeSec can't connect to Home Assistant + +``` +ERROR Home Assistant token not found in env: HA_TOKEN +``` + +**Fix**: Ensure `HA_TOKEN` environment variable is set correctly. + +### Integration can't connect to HomeSec + +``` +Error communicating with HomeSec +``` + +**Fix**: +- Verify HomeSec is running: `docker logs homesec` +- Check network connectivity: `curl http://HOMESEC_IP:8080/api/v1/health` +- Ensure firewall allows port 8080 + +### No alerts appearing in Home Assistant + +1. Check HomeSec logs: `docker logs homesec` +2. Verify HA URL is correct and reachable from HomeSec container +3. Test HA token: + ```bash + curl -H "Authorization: Bearer YOUR_TOKEN" http://HA_IP:8123/api/ + ``` + +### Database connection errors + +If using bundled Postgres: +- Check `/data` volume is mounted and writable +- View Postgres logs: `docker exec homesec cat /data/postgres/log` + +If using external Postgres: +- Verify `DATABASE_URL` format: `postgresql+asyncpg://user:pass@host:5432/dbname` +- Ensure database exists and user has permissions + +--- + +## Comparison: Add-on vs Docker + +| Feature | HA OS Add-on | Docker | +|---------|--------------|--------| +| Setup complexity | One-click | Manual | +| Supervisor integration | Yes | No | +| Zero-config auth | Yes (SUPERVISOR_TOKEN) | No (need HA token) | +| Automatic discovery | Yes | Manual URL entry | +| Updates | Via HA UI | `docker pull` | +| Best for | HA OS users | HA Container users | + +--- + +## Next Steps + +- [Configure automations](https://www.home-assistant.io/docs/automation/) based on HomeSec alerts +- Set up [notification actions](https://www.home-assistant.io/integrations/notify/) for high-risk events +- Create a [Lovelace dashboard](https://www.home-assistant.io/lovelace/) for camera monitoring From 96db49eb804a64f114403ff2999e82436f8c95e1 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Thu, 12 Feb 2026 19:34:58 -0800 Subject: [PATCH 31/31] WIP --- docker-compose.yml | 2 +- homeassistant/integration/DOCKER.md | 45 +++++++++-- .../custom_components/homesec/config_flow.py | 63 ++++++++++++++++ .../custom_components/homesec/const.py | 2 + .../custom_components/homesec/strings.json | 1 + .../homesec/translations/en.json | 1 + .../integration/tests/test_config_flow.py | 8 ++ src/homesec/api/dependencies.py | 4 +- src/homesec/api/routes/__init__.py | 3 +- src/homesec/api/routes/cameras.py | 16 ++++ src/homesec/api/routes/clips.py | 12 +++ src/homesec/api/routes/config.py | 2 +- src/homesec/api/routes/health.py | 17 +++++ src/homesec/api/routes/stats.py | 9 +++ src/homesec/api/server.py | 2 +- src/homesec/app.py | 58 ++++++++++++--- src/homesec/config/__init__.py | 2 + src/homesec/config/loader.py | 27 +++++++ src/homesec/config/manager.py | 74 +++++++++++++++++-- tests/homesec/conftest.py | 51 ++++++++++++- tests/homesec/test_api_routes.py | 9 +++ tests/homesec/test_config.py | 36 +++++++++ tests/homesec/test_config_manager.py | 60 +++++++++++++++ tests/homesec/test_health.py | 31 ++++++++ tests/homesec/test_state_store.py | 33 +++++---- uv.lock | 2 +- 26 files changed, 524 insertions(+), 46 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ce08d991..aee0fd52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: homesec: build: . - image: leva/homesec:latest + image: leva/homesec:claude-homesec-home-assistant-integration-6kzkl pull_policy: always container_name: homesec restart: unless-stopped diff --git a/homeassistant/integration/DOCKER.md b/homeassistant/integration/DOCKER.md index 02e46c21..8e2e2035 100644 --- a/homeassistant/integration/DOCKER.md +++ b/homeassistant/integration/DOCKER.md @@ -79,13 +79,46 @@ Replace: - `YOUR_LONG_LIVED_TOKEN` - The token from Step 1 - `/path/to/clips` - Where you want video clips stored -### Step 3: Install the Integration via HACS +### Bootstrap Mode (Empty Config) -1. Open HACS in Home Assistant -2. Go to Integrations → Custom repositories -3. Add: `https://github.com/lan17/homesec` (category: Integration) -4. Search for "HomeSec" and install -5. Restart Home Assistant +HomeSec can start with an empty or missing `/config/config.yaml`. In this mode: +- Only the API server runs (no pipeline, no cameras). +- The HA integration can still connect. +- The integration will enable the Home Assistant notifier automatically. + +You still need to provide a full config and restart HomeSec to run the pipeline. +When you switch to a full config, remove any `bootstrap: true` flag if present. + +### Step 3: Install the Integration (Git Sparse-Checkout) + +If you want to install directly from git (e.g., a dev branch), use sparse checkout +so only the integration files are pulled into Home Assistant. + +Run this inside the Home Assistant container (or on the host if `/config` is local): + +```bash +cd /config/custom_components + +git clone --no-checkout https://github.com/lan17/homesec.git homesec_git +cd homesec_git + +git sparse-checkout init --cone +git sparse-checkout set homeassistant/integration/custom_components/homesec +git checkout + +rm -rf /config/custom_components/homesec +cp -a homeassistant/integration/custom_components/homesec /config/custom_components/homesec +``` + +After updating the repo later: + +```bash +cd /config/custom_components/homesec_git +git pull +cp -a homeassistant/integration/custom_components/homesec /config/custom_components/homesec +``` + +Restart Home Assistant after any update. ### Step 4: Configure the Integration diff --git a/homeassistant/integration/custom_components/homesec/config_flow.py b/homeassistant/integration/custom_components/homesec/config_flow.py index 116218ed..1ec067de 100644 --- a/homeassistant/integration/custom_components/homesec/config_flow.py +++ b/homeassistant/integration/custom_components/homesec/config_flow.py @@ -24,6 +24,8 @@ CONF_HOST, CONF_PORT, CONF_VERIFY_SSL, + DEFAULT_HA_TOKEN_ENV, + DEFAULT_HA_URL_ENV, DEFAULT_MOTION_RESET_SECONDS, DEFAULT_PORT, DEFAULT_VERIFY_SSL, @@ -38,6 +40,10 @@ class InvalidAuth(Exception): """Error to indicate there is invalid auth.""" +class NotifierSetupError(Exception): + """Error to indicate notifier setup failed.""" + + class HomesecConfigFlow(config_entries.ConfigFlow, domain="homesec"): """Handle a config flow for HomeSec.""" @@ -78,6 +84,13 @@ async def async_step_addon(self, user_input: dict[str, Any] | None = None) -> Fl None, self._config_data[CONF_VERIFY_SSL], ) + await enable_home_assistant_notifier( + self.hass, + self._config_data[CONF_HOST], + self._config_data[CONF_PORT], + None, + self._config_data[CONF_VERIFY_SSL], + ) self._title = info.get("title", "HomeSec") self._cameras = info.get("cameras", []) return await self.async_step_cameras() @@ -85,6 +98,8 @@ async def async_step_addon(self, user_input: dict[str, Any] | None = None) -> Fl errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" + except NotifierSetupError: + errors["base"] = "notifier_setup_failed" return self.async_show_form( step_id="addon", @@ -105,6 +120,13 @@ async def async_step_manual(self, user_input: dict[str, Any] | None = None) -> F user_input.get(CONF_API_KEY), user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), ) + await enable_home_assistant_notifier( + self.hass, + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input.get(CONF_API_KEY), + user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ) self._title = info.get("title", "HomeSec") self._cameras = info.get("cameras", []) self._config_data = { @@ -119,6 +141,8 @@ async def async_step_manual(self, user_input: dict[str, Any] | None = None) -> F errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" + except NotifierSetupError: + errors["base"] = "notifier_setup_failed" schema = vol.Schema( { @@ -235,6 +259,45 @@ async def _request(path: str, auth: bool = True) -> Any: } +async def enable_home_assistant_notifier( + hass: HomeAssistant, + host: str, + port: int, + api_key: str | None = None, + verify_ssl: bool = True, + url_env: str = DEFAULT_HA_URL_ENV, + token_env: str = DEFAULT_HA_TOKEN_ENV, +) -> None: + """Ensure the Home Assistant notifier is enabled in HomeSec config.""" + base_url = _format_base_url(host, port) + session = async_get_clientsession(hass) + + headers = {} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + payload = {"enabled": True, "config": {"url_env": url_env, "token_env": token_env}} + + try: + async with ( + async_timeout.timeout(10), + session.post( + f"{base_url}/api/v1/notifiers/home_assistant/enable", + json=payload, + headers=headers, + ssl=verify_ssl, + ) as response, + ): + if response.status in (401, 403): + raise InvalidAuth + if response.status >= 400: + message = await response.text() + raise NotifierSetupError(message) + await response.read() + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + raise CannotConnect from exc + + async def detect_addon(hass: HomeAssistant) -> tuple[bool, str | None]: """Detect HomeSec add-on via Supervisor API.""" token = os.getenv("SUPERVISOR_TOKEN") diff --git a/homeassistant/integration/custom_components/homesec/const.py b/homeassistant/integration/custom_components/homesec/const.py index cc9f4f62..fde7e4aa 100644 --- a/homeassistant/integration/custom_components/homesec/const.py +++ b/homeassistant/integration/custom_components/homesec/const.py @@ -20,6 +20,8 @@ DEFAULT_PORT = 8080 DEFAULT_VERIFY_SSL = True ADDON_SLUG = "homesec" +DEFAULT_HA_URL_ENV = "HA_URL" +DEFAULT_HA_TOKEN_ENV = "HA_TOKEN" # Motion sensor DEFAULT_MOTION_RESET_SECONDS = 30 diff --git a/homeassistant/integration/custom_components/homesec/strings.json b/homeassistant/integration/custom_components/homesec/strings.json index cf42f5d0..e05162c5 100644 --- a/homeassistant/integration/custom_components/homesec/strings.json +++ b/homeassistant/integration/custom_components/homesec/strings.json @@ -22,6 +22,7 @@ "error": { "cannot_connect": "Failed to connect to HomeSec.", "invalid_auth": "Invalid authentication.", + "notifier_setup_failed": "Failed to enable Home Assistant notifier in HomeSec.", "single_instance_allowed": "Only one HomeSec instance is allowed." }, "abort": { diff --git a/homeassistant/integration/custom_components/homesec/translations/en.json b/homeassistant/integration/custom_components/homesec/translations/en.json index cf42f5d0..e05162c5 100644 --- a/homeassistant/integration/custom_components/homesec/translations/en.json +++ b/homeassistant/integration/custom_components/homesec/translations/en.json @@ -22,6 +22,7 @@ "error": { "cannot_connect": "Failed to connect to HomeSec.", "invalid_auth": "Invalid authentication.", + "notifier_setup_failed": "Failed to enable Home Assistant notifier in HomeSec.", "single_instance_allowed": "Only one HomeSec instance is allowed." }, "abort": { diff --git a/homeassistant/integration/tests/test_config_flow.py b/homeassistant/integration/tests/test_config_flow.py index 5099955b..1e1d3e54 100644 --- a/homeassistant/integration/tests/test_config_flow.py +++ b/homeassistant/integration/tests/test_config_flow.py @@ -19,6 +19,10 @@ async def test_config_flow_manual_success( # Given: A reachable HomeSec API with cameras aioclient_mock.get(f"{homesec_base_url}/api/v1/health", json=health_payload) aioclient_mock.get(f"{homesec_base_url}/api/v1/cameras", json=cameras_payload) + aioclient_mock.post( + f"{homesec_base_url}/api/v1/notifiers/home_assistant/enable", + json={"restart_required": True}, + ) # When: User starts manual config flow result = await hass.config_entries.flow.async_init( @@ -113,6 +117,10 @@ async def test_config_flow_addon_detected( ) aioclient_mock.get("http://abc123-homesec:8080/api/v1/health", json=health_payload) aioclient_mock.get("http://abc123-homesec:8080/api/v1/cameras", json=cameras_payload) + aioclient_mock.post( + "http://abc123-homesec:8080/api/v1/notifiers/home_assistant/enable", + json={"restart_required": True}, + ) # When: User starts config flow result = await hass.config_entries.flow.async_init( diff --git a/src/homesec/api/dependencies.py b/src/homesec/api/dependencies.py index b0d2266a..0e8fa87d 100644 --- a/src/homesec/api/dependencies.py +++ b/src/homesec/api/dependencies.py @@ -29,7 +29,7 @@ async def verify_api_key(request: Request, app: Application = Depends(get_homese if path in ("/api/v1/health", "/api/v1/diagnostics"): return - server_config = app.config.server + server_config = app.server_config if not server_config.auth_enabled: return @@ -48,6 +48,8 @@ async def verify_api_key(request: Request, app: Application = Depends(get_homese async def require_database(app: Application = Depends(get_homesec_app)) -> None: """Ensure the database is reachable for data endpoints.""" + if app.bootstrap_mode: + return repository = app.repository ok = await repository.ping() if not ok: diff --git a/src/homesec/api/routes/__init__.py b/src/homesec/api/routes/__init__.py index d85657c8..0e6fdf57 100644 --- a/src/homesec/api/routes/__init__.py +++ b/src/homesec/api/routes/__init__.py @@ -5,7 +5,7 @@ from fastapi import Depends, FastAPI from homesec.api.dependencies import require_database, verify_api_key -from homesec.api.routes import cameras, clips, config, health, stats, system +from homesec.api.routes import cameras, clips, config, health, notifiers, stats, system def register_routes(app: FastAPI) -> None: @@ -31,3 +31,4 @@ def register_routes(app: FastAPI) -> None: system.router, dependencies=[Depends(verify_api_key), Depends(require_database)], ) + app.include_router(notifiers.router, dependencies=[Depends(verify_api_key)]) diff --git a/src/homesec/api/routes/cameras.py b/src/homesec/api/routes/cameras.py index 401179f5..232560a2 100644 --- a/src/homesec/api/routes/cameras.py +++ b/src/homesec/api/routes/cameras.py @@ -72,6 +72,8 @@ def _camera_response(app: Application, camera: CameraConfig) -> CameraResponse: @router.get("/api/v1/cameras", response_model=list[CameraResponse]) async def list_cameras(app: Application = Depends(get_homesec_app)) -> list[CameraResponse]: """List all cameras.""" + if app.bootstrap_mode: + return [] config = await asyncio.to_thread(app.config_manager.get_config) return [_camera_response(app, camera) for camera in config.cameras] @@ -79,6 +81,8 @@ async def list_cameras(app: Application = Depends(get_homesec_app)) -> list[Came @router.get("/api/v1/cameras/{name}", response_model=CameraResponse) async def get_camera(name: str, app: Application = Depends(get_homesec_app)) -> CameraResponse: """Get a single camera.""" + if app.bootstrap_mode: + raise HTTPException(status_code=404, detail="Camera not found") config = await asyncio.to_thread(app.config_manager.get_config) camera = next((cam for cam in config.cameras if cam.name == name), None) if camera is None: @@ -91,6 +95,10 @@ async def create_camera( payload: CameraCreate, app: Application = Depends(get_homesec_app) ) -> ConfigChangeResponse: """Create a new camera.""" + if app.bootstrap_mode: + raise HTTPException( + status_code=409, detail="HomeSec is in bootstrap mode; configure full settings first" + ) try: result = await app.config_manager.add_camera( name=payload.name, @@ -119,6 +127,10 @@ async def update_camera( app: Application = Depends(get_homesec_app), ) -> ConfigChangeResponse: """Update a camera.""" + if app.bootstrap_mode: + raise HTTPException( + status_code=409, detail="HomeSec is in bootstrap mode; configure full settings first" + ) try: result = await app.config_manager.update_camera( camera_name=name, @@ -147,6 +159,10 @@ async def delete_camera( name: str, app: Application = Depends(get_homesec_app) ) -> ConfigChangeResponse: """Delete a camera.""" + if app.bootstrap_mode: + raise HTTPException( + status_code=409, detail="HomeSec is in bootstrap mode; configure full settings first" + ) try: result = await app.config_manager.remove_camera(camera_name=name) except ValueError as exc: diff --git a/src/homesec/api/routes/clips.py b/src/homesec/api/routes/clips.py index 212a37c3..a85d30ae 100644 --- a/src/homesec/api/routes/clips.py +++ b/src/homesec/api/routes/clips.py @@ -83,6 +83,10 @@ async def list_clips( app: Application = Depends(get_homesec_app), ) -> ClipListResponse: """List clips with filtering and pagination.""" + if app.bootstrap_mode: + raise HTTPException( + status_code=409, detail="HomeSec is in bootstrap mode; configure full settings first" + ) offset = (page - 1) * page_size clips, total = await app.repository.list_clips( camera=camera, @@ -107,6 +111,10 @@ async def list_clips( @router.get("/api/v1/clips/{clip_id}", response_model=ClipResponse) async def get_clip(clip_id: str, app: Application = Depends(get_homesec_app)) -> ClipResponse: """Get a single clip.""" + if app.bootstrap_mode: + raise HTTPException( + status_code=409, detail="HomeSec is in bootstrap mode; configure full settings first" + ) state = await app.repository.get_clip(clip_id) if state is None: raise HTTPException(status_code=404, detail="Clip not found") @@ -116,6 +124,10 @@ async def get_clip(clip_id: str, app: Application = Depends(get_homesec_app)) -> @router.delete("/api/v1/clips/{clip_id}", response_model=ClipResponse) async def delete_clip(clip_id: str, app: Application = Depends(get_homesec_app)) -> ClipResponse: """Delete a clip and its storage object.""" + if app.bootstrap_mode: + raise HTTPException( + status_code=409, detail="HomeSec is in bootstrap mode; configure full settings first" + ) try: state = await app.repository.delete_clip(clip_id) except ValueError as exc: diff --git a/src/homesec/api/routes/config.py b/src/homesec/api/routes/config.py index 6239b87a..49349bef 100644 --- a/src/homesec/api/routes/config.py +++ b/src/homesec/api/routes/config.py @@ -25,5 +25,5 @@ class ConfigResponse(BaseModel): @router.get("/api/v1/config", response_model=ConfigResponse) async def get_config(app: Application = Depends(get_homesec_app)) -> ConfigResponse: """Return full configuration.""" - config = await asyncio.to_thread(app.config_manager.get_config) + config = await asyncio.to_thread(app.config_manager.get_config_or_bootstrap) return ConfigResponse(config=config.model_dump(mode="json")) diff --git a/src/homesec/api/routes/health.py b/src/homesec/api/routes/health.py index b0bd05f3..912e5515 100644 --- a/src/homesec/api/routes/health.py +++ b/src/homesec/api/routes/health.py @@ -47,6 +47,14 @@ class DiagnosticsResponse(BaseModel): @router.get("/api/v1/health", response_model=HealthResponse) async def get_health(app: Application = Depends(get_homesec_app)) -> HealthResponse | JSONResponse: """Basic health check.""" + if app.bootstrap_mode: + return HealthResponse( + status="bootstrap", + pipeline="stopped", + postgres="unavailable", + cameras_online=0, + ) + pipeline_running = app.pipeline_running postgres_ok = await app.repository.ping() cameras_online = sum(1 for source in app.sources if source.is_healthy()) @@ -73,6 +81,15 @@ async def get_health(app: Application = Depends(get_homesec_app)) -> HealthRespo @router.get("/api/v1/diagnostics", response_model=DiagnosticsResponse) async def get_diagnostics(app: Application = Depends(get_homesec_app)) -> DiagnosticsResponse: """Detailed component diagnostics.""" + if app.bootstrap_mode: + return DiagnosticsResponse( + status="bootstrap", + uptime_seconds=app.uptime_seconds, + postgres=ComponentStatus(status="error", error="bootstrap_mode"), + storage=ComponentStatus(status="error", error="bootstrap_mode"), + cameras={}, + ) + pipeline_running = app.pipeline_running async def _check(ping: Any) -> ComponentStatus: diff --git a/src/homesec/api/routes/stats.py b/src/homesec/api/routes/stats.py index 0ff00e2f..4dd1494c 100644 --- a/src/homesec/api/routes/stats.py +++ b/src/homesec/api/routes/stats.py @@ -27,6 +27,15 @@ class StatsResponse(BaseModel): @router.get("/api/v1/stats", response_model=StatsResponse) async def get_stats(app: Application = Depends(get_homesec_app)) -> StatsResponse: """Return system statistics.""" + if app.bootstrap_mode: + return StatsResponse( + clips_today=0, + alerts_today=0, + cameras_total=0, + cameras_online=0, + uptime_seconds=app.uptime_seconds, + ) + today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) clips_today = await app.repository.count_clips_since(today_start) alerts_today = await app.repository.count_alerts_since(today_start) diff --git a/src/homesec/api/server.py b/src/homesec/api/server.py index 86a7620a..472f59c5 100644 --- a/src/homesec/api/server.py +++ b/src/homesec/api/server.py @@ -25,7 +25,7 @@ def create_app(app_instance: Application) -> FastAPI: app = FastAPI(title="HomeSec API", version="1.0.0") app.state.homesec = app_instance - server_config = app_instance.config.server + server_config = app_instance.server_config app.add_middleware( CORSMiddleware, allow_origins=server_config.cors_origins, diff --git a/src/homesec/app.py b/src/homesec/app.py index 4457c783..e6c1e453 100644 --- a/src/homesec/app.py +++ b/src/homesec/app.py @@ -10,9 +10,16 @@ from typing import TYPE_CHECKING from homesec.api import APIServer, create_app -from homesec.config import load_config, resolve_env_var, validate_config, validate_plugin_names +from homesec.config import ( + load_config_or_bootstrap, + resolve_env_var, + validate_config, + validate_plugin_names, +) from homesec.config.manager import ConfigManager from homesec.interfaces import EventStore +from homesec.models.bootstrap import BootstrapConfig +from homesec.models.config import FastAPIServerConfig from homesec.notifiers.multiplex import MultiplexNotifier, NotifierEntry from homesec.pipeline import ClipPipeline from homesec.plugins.alert_policies import load_alert_policy @@ -54,12 +61,17 @@ def __init__(self, config_path: Path) -> None: """ self._config_path = config_path self._config: Config | None = None + self._bootstrap_config: BootstrapConfig | None = None + self._bootstrap_mode = False # Components (created in _create_components) self._storage: StorageBackend | None = None self._state_store: StateStore = NoopStateStore() self._event_store: EventStore = NoopEventStore() - self._repository: ClipRepository | None = None + self._repository: ClipRepository | None = ClipRepository( + self._state_store, + self._event_store, + ) self._notifier: Notifier | None = None self._notifier_entries: list[NotifierEntry] = [] self._filter: ObjectFilter | None = None @@ -83,17 +95,26 @@ async def run(self) -> None: logger.info("Starting HomeSec application...") # Load config - self._config = load_config(self._config_path) - logger.info("Config loaded from %s", self._config_path) + loaded = load_config_or_bootstrap(self._config_path) + if isinstance(loaded, BootstrapConfig): + self._bootstrap_mode = True + self._bootstrap_config = loaded + logger.warning( + "Starting in bootstrap mode (empty or missing config at %s)", + self._config_path, + ) + else: + self._config = loaded + logger.info("Config loaded from %s", self._config_path) - # Create components - await self._create_components() + # Create components + await self._create_components() # Set up signal handlers self._setup_signal_handlers() # Start API server - server_cfg = self._require_config().server + server_cfg = self.server_config if server_cfg.enabled: self._api_server = APIServer( create_app(self), @@ -102,9 +123,10 @@ async def run(self) -> None: ) await self._api_server.start() - # Start sources - for source in self._sources: - await source.start() + if not self._bootstrap_mode: + # Start sources + for source in self._sources: + await source.start() # Track start time for uptime self._start_time = time.time() @@ -268,7 +290,7 @@ def _create_sources(self, config: Config) -> list[ClipSource]: def _require_config(self) -> Config: if self._config is None: - raise RuntimeError("Config not loaded") + raise RuntimeError("Config not loaded (bootstrap mode)") return self._config def _validate_config(self, config: Config) -> None: @@ -343,6 +365,18 @@ async def shutdown(self) -> None: def config(self) -> Config: return self._require_config() + @property + def server_config(self) -> FastAPIServerConfig: + if self._config is not None: + return self._config.server + if self._bootstrap_config is not None: + return self._bootstrap_config.server + return FastAPIServerConfig() + + @property + def bootstrap_mode(self) -> bool: + return self._bootstrap_mode + @property def repository(self) -> ClipRepository: if self._repository is None: @@ -365,6 +399,8 @@ def config_manager(self) -> ConfigManager: @property def pipeline_running(self) -> bool: + if self._bootstrap_mode: + return False return self._pipeline is not None and not self._shutdown_event.is_set() @property diff --git a/src/homesec/config/__init__.py b/src/homesec/config/__init__.py index f8ef6215..5eb51c1f 100644 --- a/src/homesec/config/__init__.py +++ b/src/homesec/config/__init__.py @@ -4,6 +4,7 @@ ConfigError, load_config, load_config_from_dict, + load_config_or_bootstrap, resolve_env_var, ) from homesec.config.validation import ( @@ -17,6 +18,7 @@ "ConfigError", "load_config", "load_config_from_dict", + "load_config_or_bootstrap", "resolve_env_var", "validate_camera_references", "validate_config", diff --git a/src/homesec/config/loader.py b/src/homesec/config/loader.py index 6395d51d..992121da 100644 --- a/src/homesec/config/loader.py +++ b/src/homesec/config/loader.py @@ -9,6 +9,7 @@ import yaml from pydantic import ValidationError +from homesec.models.bootstrap import BootstrapConfig from homesec.models.config import Config @@ -61,6 +62,32 @@ def load_config(path: Path) -> Config: return config +def load_config_or_bootstrap(path: Path) -> Config | BootstrapConfig: + """Load config or fall back to bootstrap mode for empty/missing config files.""" + if not path.exists(): + return BootstrapConfig() + + try: + with path.open() as f: + raw = yaml.safe_load(f) + except yaml.YAMLError as e: + raise ConfigError(f"Invalid YAML in {path}: {e}", path=path) from e + + if raw is None or raw == {}: + return BootstrapConfig() + + if not isinstance(raw, dict): + raise ConfigError(f"Config must be a YAML mapping, got {type(raw).__name__}", path=path) + + if raw.get("bootstrap") is True: + try: + return BootstrapConfig.model_validate(raw) + except ValidationError as e: + raise ConfigError(format_validation_error(e, path), path=path) from e + + return load_config(path) + + def load_config_from_dict(data: dict[str, Any]) -> Config: """Load and validate configuration from a dict (useful for testing). diff --git a/src/homesec/config/manager.py b/src/homesec/config/manager.py index 1db57882..6a183545 100644 --- a/src/homesec/config/manager.py +++ b/src/homesec/config/manager.py @@ -10,8 +10,14 @@ import yaml from pydantic import BaseModel -from homesec.config.loader import ConfigError, load_config, load_config_from_dict -from homesec.models.config import CameraConfig, CameraSourceConfig, Config +from homesec.config.loader import ( + ConfigError, + load_config, + load_config_from_dict, + load_config_or_bootstrap, +) +from homesec.models.bootstrap import BootstrapConfig +from homesec.models.config import CameraConfig, CameraSourceConfig, Config, NotifierConfig class ConfigUpdateResult(BaseModel): @@ -33,6 +39,10 @@ def get_config(self) -> Config: """Get the current configuration.""" return load_config(self._config_path) + def get_config_or_bootstrap(self) -> Config | BootstrapConfig: + """Get the current configuration or bootstrap config.""" + return load_config_or_bootstrap(self._config_path) + async def add_camera( self, name: str, @@ -41,7 +51,9 @@ async def add_camera( source_config: dict[str, object], ) -> ConfigUpdateResult: """Add a new camera to the config.""" - config = await asyncio.to_thread(self.get_config) + config = await asyncio.to_thread(self.get_config_or_bootstrap) + if isinstance(config, BootstrapConfig): + raise ValueError("HomeSec is in bootstrap mode; configure full settings first") if any(camera.name == name for camera in config.cameras): raise ValueError(f"Camera already exists: {name}") @@ -65,7 +77,9 @@ async def update_camera( source_config: dict[str, object] | None, ) -> ConfigUpdateResult: """Update an existing camera in the config.""" - config = await asyncio.to_thread(self.get_config) + config = await asyncio.to_thread(self.get_config_or_bootstrap) + if isinstance(config, BootstrapConfig): + raise ValueError("HomeSec is in bootstrap mode; configure full settings first") camera = next((cam for cam in config.cameras if cam.name == camera_name), None) if camera is None: @@ -88,7 +102,9 @@ async def remove_camera( camera_name: str, ) -> ConfigUpdateResult: """Remove a camera from the config.""" - config = await asyncio.to_thread(self.get_config) + config = await asyncio.to_thread(self.get_config_or_bootstrap) + if isinstance(config, BootstrapConfig): + raise ValueError("HomeSec is in bootstrap mode; configure full settings first") updated = [camera for camera in config.cameras if camera.name != camera_name] if len(updated) == len(config.cameras): @@ -100,6 +116,46 @@ async def remove_camera( await self._save_config(validated) return ConfigUpdateResult() + async def upsert_notifier( + self, + backend: str, + enabled: bool | None = True, + config: dict[str, object] | None = None, + ) -> ConfigUpdateResult: + """Create or update a notifier configuration entry.""" + current = await asyncio.to_thread(self.get_config_or_bootstrap) + backend_key = backend.lower() + notifier = next( + (entry for entry in current.notifiers if entry.backend.lower() == backend_key), + None, + ) + + if notifier is None: + current.notifiers.append( + NotifierConfig( + backend=backend, + enabled=enabled if enabled is not None else True, + config=config or {}, + ) + ) + else: + if enabled is not None: + notifier.enabled = enabled + if config is not None: + existing = _config_to_dict(notifier.config) + for key, value in config.items(): + if key not in existing or existing[key] in (None, ""): + existing[key] = value + notifier.config = existing + + if isinstance(current, BootstrapConfig): + await self._save_config(current) + return ConfigUpdateResult() + + validated = await self._validate_config(current) + await self._save_config(validated) + return ConfigUpdateResult() + async def _validate_config(self, config: Config) -> Config: """Validate configuration via the standard loader path.""" payload = config.model_dump(mode="json") @@ -108,7 +164,7 @@ async def _validate_config(self, config: Config) -> Config: except ConfigError as exc: raise ValueError(str(exc)) from exc - async def _save_config(self, config: Config) -> None: + async def _save_config(self, config: BaseModel) -> None: """Save config to disk with backup.""" def _write() -> None: @@ -128,3 +184,9 @@ def _write() -> None: os.replace(tmp_path, self._config_path) await asyncio.to_thread(_write) + + +def _config_to_dict(value: dict[str, object] | BaseModel) -> dict[str, object]: + if isinstance(value, BaseModel): + return value.model_dump(mode="json") + return dict(value) diff --git a/tests/homesec/conftest.py b/tests/homesec/conftest.py index 50d3b61b..068f4f20 100644 --- a/tests/homesec/conftest.py +++ b/tests/homesec/conftest.py @@ -1,8 +1,11 @@ """Shared pytest fixtures for HomeSec tests.""" +import os +import socket import sys from datetime import datetime from pathlib import Path +from urllib.parse import urlsplit # Add src to sys.path for imports src_path = Path(__file__).parent.parent.parent / "src" @@ -87,9 +90,20 @@ def sample_clip() -> Clip: @pytest.fixture def postgres_dsn() -> str: """Return test Postgres DSN (requires local DB running).""" - import os + dsn = os.getenv( + "TEST_DB_DSN", + os.getenv("DB_DSN", "postgresql://homesec:homesec@127.0.0.1:5432/homesec"), + ) + + if os.getenv("SKIP_POSTGRES_TESTS") == "1": + pytest.skip("Skipping Postgres tests (SKIP_POSTGRES_TESTS=1)") + + if not _is_ci(): + host, port = _dsn_host_port(dsn) + if host and not _can_connect(host, port): + pytest.skip(f"Postgres not available at {host}:{port}") - return os.getenv("TEST_DB_DSN", "postgresql://homesec:homesec@127.0.0.1:5432/homesec") + return dsn @pytest.fixture @@ -100,7 +114,19 @@ async def clean_test_db(postgres_dsn: str) -> None: from homesec.state.postgres import Base, ClipEvent, ClipState, PostgresStateStore store = PostgresStateStore(postgres_dsn) - await store.initialize() + try: + initialized = await store.initialize() + except Exception as exc: # pragma: no cover - defensive + if _is_ci(): + raise + pytest.skip(f"Postgres not available: {exc}") + return + + if not initialized: + if _is_ci(): + raise AssertionError("Failed to initialize PostgresStateStore") + pytest.skip("Postgres not available") + return if store._engine is not None: async with store._engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) @@ -115,3 +141,22 @@ async def clean_test_db(postgres_dsn: str) -> None: await conn.execute(delete(ClipEvent).where(ClipEvent.clip_id.like("test-%"))) await conn.execute(delete(ClipState).where(ClipState.clip_id.like("test-%"))) await store.shutdown() + + +def _is_ci() -> bool: + return os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" + + +def _dsn_host_port(dsn: str) -> tuple[str | None, int]: + parsed = urlsplit(dsn) + host = parsed.hostname + port = parsed.port or 5432 + return host, port + + +def _can_connect(host: str, port: int) -> bool: + try: + with socket.create_connection((host, port), timeout=1.0): + return True + except OSError: + return False diff --git a/tests/homesec/test_api_routes.py b/tests/homesec/test_api_routes.py index 0807831c..3fd95e94 100644 --- a/tests/homesec/test_api_routes.py +++ b/tests/homesec/test_api_routes.py @@ -145,11 +145,20 @@ def __init__( ) self._pipeline_running = pipeline_running self.uptime_seconds = 0.0 + self._bootstrap_mode = False @property def config(self): # type: ignore[override] return self._config + @property + def server_config(self): + return self._config.server + + @property + def bootstrap_mode(self) -> bool: + return self._bootstrap_mode + @property def pipeline_running(self) -> bool: return self._pipeline_running diff --git a/tests/homesec/test_config.py b/tests/homesec/test_config.py index 44312e46..81250389 100644 --- a/tests/homesec/test_config.py +++ b/tests/homesec/test_config.py @@ -11,11 +11,13 @@ ConfigError, load_config, load_config_from_dict, + load_config_or_bootstrap, resolve_env_var, validate_camera_references, validate_config, validate_plugin_names, ) +from homesec.models.bootstrap import BootstrapConfig from homesec.models.config import Config from homesec.plugins.registry import PluginType, plugin @@ -251,6 +253,40 @@ def test_load_config_empty_file() -> None: path.unlink() +def test_load_config_or_bootstrap_missing_file() -> None: + """Test that missing file returns bootstrap config.""" + # Given a missing config path + path = Path("/tmp/homesec-missing-config.yaml") + if path.exists(): + path.unlink() + + # When loading with bootstrap fallback + config = load_config_or_bootstrap(path) + + # Then bootstrap config is returned + assert isinstance(config, BootstrapConfig) + assert config.bootstrap is True + + +def test_load_config_or_bootstrap_empty_file() -> None: + """Test that empty file returns bootstrap config.""" + # Given an empty YAML file + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + f.write("") + f.flush() + path = Path(f.name) + + try: + # When loading with bootstrap fallback + config = load_config_or_bootstrap(path) + + # Then bootstrap config is returned + assert isinstance(config, BootstrapConfig) + assert config.bootstrap is True + finally: + path.unlink() + + def test_per_camera_override_merge() -> None: """Test that per-camera overrides are preserved in config.""" # Given a config with per-camera alert overrides diff --git a/tests/homesec/test_config_manager.py b/tests/homesec/test_config_manager.py index a58835b7..770a9500 100644 --- a/tests/homesec/test_config_manager.py +++ b/tests/homesec/test_config_manager.py @@ -7,7 +7,9 @@ import pytest import yaml +from homesec.config.loader import load_config_or_bootstrap from homesec.config.manager import ConfigManager +from homesec.models.bootstrap import BootstrapConfig def _write_config(path: Path, cameras: list[dict[str, object]]) -> ConfigManager: @@ -150,3 +152,61 @@ async def test_config_manager_invalid_update_raises(tmp_path: Path) -> None: ) # Then it raises a validation error + + +@pytest.mark.asyncio +async def test_config_manager_upsert_notifier_adds_and_merges(tmp_path: Path) -> None: + """ConfigManager should add and merge notifier config.""" + # Given: A config with only MQTT notifier + config_path = tmp_path / "config.yaml" + manager = _write_config(config_path, cameras=[]) + + # When: Enabling the Home Assistant notifier + await manager.upsert_notifier( + backend="home_assistant", + enabled=True, + config={"url_env": "HA_URL", "token_env": "HA_TOKEN"}, + ) + + # Then: The notifier is added with config values + config = manager.get_config() + notifier = next(n for n in config.notifiers if n.backend == "home_assistant") + assert notifier.enabled is True + assert notifier.config["url_env"] == "HA_URL" + assert notifier.config["token_env"] == "HA_TOKEN" + + # When: Upserting again with only url_env + await manager.upsert_notifier( + backend="home_assistant", + enabled=True, + config={"url_env": "CUSTOM_HA_URL"}, + ) + + # Then: Existing token_env is preserved + config = manager.get_config() + notifier = next(n for n in config.notifiers if n.backend == "home_assistant") + assert notifier.config["url_env"] == "HA_URL" + assert notifier.config["token_env"] == "HA_TOKEN" + + +@pytest.mark.asyncio +async def test_config_manager_upsert_notifier_bootstrap(tmp_path: Path) -> None: + """ConfigManager should create bootstrap config when empty.""" + # Given: An empty config file + config_path = tmp_path / "config.yaml" + config_path.write_text("") + manager = ConfigManager(config_path) + + # When: Enabling the Home Assistant notifier + await manager.upsert_notifier( + backend="home_assistant", + enabled=True, + config={"url_env": "HA_URL", "token_env": "HA_TOKEN"}, + ) + + # Then: Bootstrap config is persisted with the notifier + loaded = load_config_or_bootstrap(config_path) + assert isinstance(loaded, BootstrapConfig) + notifier = next(n for n in loaded.notifiers if n.backend == "home_assistant") + assert notifier.config["url_env"] == "HA_URL" + assert notifier.config["token_env"] == "HA_TOKEN" diff --git a/tests/homesec/test_health.py b/tests/homesec/test_health.py index a253f8ac..9d9ac50f 100644 --- a/tests/homesec/test_health.py +++ b/tests/homesec/test_health.py @@ -74,11 +74,20 @@ def __init__( self._sources_by_name = sources_by_name self._pipeline_running = pipeline_running self.uptime_seconds = 123.0 + self._bootstrap_mode = False @property def config(self): # type: ignore[override] return self._config + @property + def server_config(self): + return self._config.server + + @property + def bootstrap_mode(self) -> bool: + return self._bootstrap_mode + @property def pipeline_running(self) -> bool: return self._pipeline_running @@ -156,6 +165,28 @@ def test_health_unhealthy_when_pipeline_stopped() -> None: assert payload["pipeline"] == "stopped" +def test_health_bootstrap_mode() -> None: + """Health should indicate bootstrap mode when config is empty.""" + # Given a bootstrap-mode app + app = _StubApp( + repository=_StubRepository(ok=False), + storage=_StubStorage(ok=False), + sources_by_name={}, + pipeline_running=False, + ) + app._bootstrap_mode = True + client = _client(app) + + # When requesting health + response = client.get("/api/v1/health") + + # Then it reports bootstrap status + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "bootstrap" + assert payload["pipeline"] == "stopped" + + def test_diagnostics_reports_camera_status() -> None: """Diagnostics should include per-camera health.""" # Given a running pipeline with one camera diff --git a/tests/homesec/test_state_store.py b/tests/homesec/test_state_store.py index 762d6777..b21f9c65 100644 --- a/tests/homesec/test_state_store.py +++ b/tests/homesec/test_state_store.py @@ -10,23 +10,24 @@ from homesec.state import PostgresStateStore from homesec.state.postgres import Base, ClipState, _normalize_async_dsn -# Default DSN for local Docker Postgres (matches docker-compose.postgres.yml) -DEFAULT_DSN = "postgresql://homesec:homesec@127.0.0.1:5432/homesec" - - -def get_test_dsn() -> str: - """Get test database DSN from environment or use default.""" - return os.environ.get("TEST_DB_DSN", DEFAULT_DSN) - @pytest.fixture -async def state_store() -> PostgresStateStore: +async def state_store(postgres_dsn: str) -> PostgresStateStore: """Create and initialize a PostgresStateStore for testing.""" - dsn = get_test_dsn() - assert dsn is not None - store = PostgresStateStore(dsn) - initialized = await store.initialize() - assert initialized, "Failed to initialize state store" + store = PostgresStateStore(postgres_dsn) + try: + initialized = await store.initialize() + except Exception as exc: # pragma: no cover - defensive + if _is_ci(): + raise + pytest.skip(f"Postgres not available: {exc}") + return + + if not initialized: + if _is_ci(): + raise AssertionError("Failed to initialize state store") + pytest.skip("Postgres not available") + return if store._engine is not None: async with store._engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) @@ -39,6 +40,10 @@ async def state_store() -> PostgresStateStore: await store.shutdown() +def _is_ci() -> bool: + return os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" + + def sample_state(clip_id: str = "test_clip_001") -> ClipStateData: """Create a sample ClipStateData for testing.""" return ClipStateData( diff --git a/uv.lock b/uv.lock index ce0b36be..4fd8ed17 100644 --- a/uv.lock +++ b/uv.lock @@ -1848,7 +1848,7 @@ wheels = [ [[package]] name = "homesec" -version = "1.2.2" +version = "1.2.3" source = { editable = "." } dependencies = [ { name = "aiohttp", marker = "python_full_version >= '3.13.2' and sys_platform != 'win32'" },