From f6d6ba612e1b4d5fb793cda976a9acad0c84c048 Mon Sep 17 00:00:00 2001 From: Alireza Vaezi Date: Fri, 25 Jul 2025 21:39:07 -0400 Subject: [PATCH] Massive update - API and other modules added --- .python-version | 1 + Camera/Data/054012620023.mvdat | Bin 0 -> 55 bytes Camera/Data/054052320151.mvdat | Bin 0 -> 95 bytes README.md | 221 ++++- check_time.py | 58 ++ config.json | 47 + main.py | 18 + 01README.md => old tests/01README.md | 0 old tests/IMPLEMENTATION_SUMMARY.md | 184 ++++ old tests/README.md | 1 + old tests/README_SYSTEM.md | 249 +++++ old tests/TIMEZONE_SETUP_SUMMARY.md | 190 ++++ .../VIDEO_RECORDER_README.md | 0 .../camera_capture.py | 0 .../camera_status_test.ipynb | 28 +- old tests/camera_test_setup.ipynb | 349 +++++++ .../camera_video_recorder.py | 0 .../exposure test.ipynb | 0 old tests/gige_camera_advanced.ipynb | 385 ++++++++ old tests/main.py | 6 + old tests/mqtt test.ipynb | 146 +++ .../test_exposure.py | 0 pyproject.toml | 20 + python demo/__pycache__/mvsdk.cpython-311.pyc | Bin 0 -> 144006 bytes setup_timezone.sh | 289 ++++++ start_system.sh | 59 ++ start_system.sh.backup | 55 ++ test_system.py | 223 +++++ test_timezone.py | 56 ++ usda_vision_system/__init__.py | 13 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 515 bytes .../__pycache__/main.cpython-311.pyc | Bin 0 -> 15384 bytes usda_vision_system/api/__init__.py | 10 + .../api/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 453 bytes .../api/__pycache__/models.cpython-311.pyc | Bin 0 -> 9244 bytes .../api/__pycache__/server.cpython-311.pyc | Bin 0 -> 26378 bytes usda_vision_system/api/models.py | 145 +++ usda_vision_system/api/server.py | 426 +++++++++ usda_vision_system/camera/__init__.py | 12 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 604 bytes .../__pycache__/manager.cpython-311.pyc | Bin 0 -> 16691 bytes .../__pycache__/monitor.cpython-311.pyc | Bin 0 -> 13505 bytes .../__pycache__/recorder.cpython-311.pyc | Bin 0 -> 20089 bytes usda_vision_system/camera/manager.py | 320 +++++++ usda_vision_system/camera/monitor.py | 267 ++++++ usda_vision_system/camera/recorder.py | 372 ++++++++ usda_vision_system/core/__init__.py | 15 + .../core/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 686 bytes .../core/__pycache__/config.cpython-311.pyc | Bin 0 -> 11705 bytes .../core/__pycache__/events.cpython-311.pyc | Bin 0 -> 11497 bytes .../logging_config.cpython-311.pyc | Bin 0 -> 13147 bytes .../__pycache__/state_manager.cpython-311.pyc | Bin 0 -> 21503 bytes .../timezone_utils.cpython-311.pyc | Bin 0 -> 12297 bytes usda_vision_system/core/config.py | 207 +++++ usda_vision_system/core/events.py | 195 ++++ usda_vision_system/core/logging_config.py | 260 ++++++ usda_vision_system/core/state_manager.py | 328 +++++++ usda_vision_system/core/timezone_utils.py | 225 +++++ usda_vision_system/main.py | 288 ++++++ usda_vision_system/mqtt/__init__.py | 11 + .../mqtt/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 544 bytes .../mqtt/__pycache__/client.cpython-311.pyc | Bin 0 -> 13859 bytes .../mqtt/__pycache__/handlers.cpython-311.pyc | Bin 0 -> 8686 bytes usda_vision_system/mqtt/client.py | 251 ++++++ usda_vision_system/mqtt/handlers.py | 153 ++++ usda_vision_system/storage/__init__.py | 9 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 430 bytes .../__pycache__/manager.cpython-311.pyc | Bin 0 -> 18362 bytes usda_vision_system/storage/manager.py | 349 +++++++ uv.lock | 850 ++++++++++++++++++ 70 files changed, 7276 insertions(+), 15 deletions(-) create mode 100644 .python-version create mode 100644 Camera/Data/054012620023.mvdat create mode 100644 Camera/Data/054052320151.mvdat create mode 100755 check_time.py create mode 100644 config.json create mode 100644 main.py rename 01README.md => old tests/01README.md (100%) create mode 100644 old tests/IMPLEMENTATION_SUMMARY.md create mode 100644 old tests/README.md create mode 100644 old tests/README_SYSTEM.md create mode 100644 old tests/TIMEZONE_SETUP_SUMMARY.md rename VIDEO_RECORDER_README.md => old tests/VIDEO_RECORDER_README.md (100%) rename camera_capture.py => old tests/camera_capture.py (100%) rename camera_status_test.ipynb => old tests/camera_status_test.ipynb (97%) create mode 100644 old tests/camera_test_setup.ipynb rename camera_video_recorder.py => old tests/camera_video_recorder.py (100%) rename exposure test.ipynb => old tests/exposure test.ipynb (100%) create mode 100644 old tests/gige_camera_advanced.ipynb create mode 100644 old tests/main.py create mode 100644 old tests/mqtt test.ipynb rename test_exposure.py => old tests/test_exposure.py (100%) create mode 100644 pyproject.toml create mode 100644 python demo/__pycache__/mvsdk.cpython-311.pyc create mode 100755 setup_timezone.sh create mode 100755 start_system.sh create mode 100755 start_system.sh.backup create mode 100644 test_system.py create mode 100644 test_timezone.py create mode 100644 usda_vision_system/__init__.py create mode 100644 usda_vision_system/__pycache__/__init__.cpython-311.pyc create mode 100644 usda_vision_system/__pycache__/main.cpython-311.pyc create mode 100644 usda_vision_system/api/__init__.py create mode 100644 usda_vision_system/api/__pycache__/__init__.cpython-311.pyc create mode 100644 usda_vision_system/api/__pycache__/models.cpython-311.pyc create mode 100644 usda_vision_system/api/__pycache__/server.cpython-311.pyc create mode 100644 usda_vision_system/api/models.py create mode 100644 usda_vision_system/api/server.py create mode 100644 usda_vision_system/camera/__init__.py create mode 100644 usda_vision_system/camera/__pycache__/__init__.cpython-311.pyc create mode 100644 usda_vision_system/camera/__pycache__/manager.cpython-311.pyc create mode 100644 usda_vision_system/camera/__pycache__/monitor.cpython-311.pyc create mode 100644 usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc create mode 100644 usda_vision_system/camera/manager.py create mode 100644 usda_vision_system/camera/monitor.py create mode 100644 usda_vision_system/camera/recorder.py create mode 100644 usda_vision_system/core/__init__.py create mode 100644 usda_vision_system/core/__pycache__/__init__.cpython-311.pyc create mode 100644 usda_vision_system/core/__pycache__/config.cpython-311.pyc create mode 100644 usda_vision_system/core/__pycache__/events.cpython-311.pyc create mode 100644 usda_vision_system/core/__pycache__/logging_config.cpython-311.pyc create mode 100644 usda_vision_system/core/__pycache__/state_manager.cpython-311.pyc create mode 100644 usda_vision_system/core/__pycache__/timezone_utils.cpython-311.pyc create mode 100644 usda_vision_system/core/config.py create mode 100644 usda_vision_system/core/events.py create mode 100644 usda_vision_system/core/logging_config.py create mode 100644 usda_vision_system/core/state_manager.py create mode 100644 usda_vision_system/core/timezone_utils.py create mode 100644 usda_vision_system/main.py create mode 100644 usda_vision_system/mqtt/__init__.py create mode 100644 usda_vision_system/mqtt/__pycache__/__init__.cpython-311.pyc create mode 100644 usda_vision_system/mqtt/__pycache__/client.cpython-311.pyc create mode 100644 usda_vision_system/mqtt/__pycache__/handlers.cpython-311.pyc create mode 100644 usda_vision_system/mqtt/client.py create mode 100644 usda_vision_system/mqtt/handlers.py create mode 100644 usda_vision_system/storage/__init__.py create mode 100644 usda_vision_system/storage/__pycache__/__init__.cpython-311.pyc create mode 100644 usda_vision_system/storage/__pycache__/manager.cpython-311.pyc create mode 100644 usda_vision_system/storage/manager.py create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/Camera/Data/054012620023.mvdat b/Camera/Data/054012620023.mvdat new file mode 100644 index 0000000000000000000000000000000000000000..2d2bce706a67fe8afdf5358c32b58d378832ce38 GIT binary patch literal 55 pcmeYbb9Xf~HgR`em&RPk&cMu&&C7rTNU(%6JZC6k%3`u)1_0l@2GRfk literal 0 HcmV?d00001 diff --git a/Camera/Data/054052320151.mvdat b/Camera/Data/054052320151.mvdat new file mode 100644 index 0000000000000000000000000000000000000000..367dfb3ee8d32888d30828e22a4ae8bdfe5f5e8f GIT binary patch literal 95 zcmeYbb9Xf~HgR|EE}Y89&cMsS<;;KsWHWtY$N}P9rq2v{K%C3;1xSBk=wwl4>R?f0 T>IC8r7Ih%44x}}Jv<4FZ6dw_V literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 0bb8d07..f2e062c 100644 --- a/README.md +++ b/README.md @@ -1 +1,220 @@ -# USDA-Vision-Cameras \ No newline at end of file +# USDA Vision Camera System + +A comprehensive system for monitoring machines via MQTT and automatically recording video from GigE cameras when machines are active. Designed for Atlanta, Georgia operations with proper timezone synchronization. + +## šŸŽÆ Overview + +This system integrates MQTT machine monitoring with automated video recording from GigE cameras. When a machine turns on (detected via MQTT), the system automatically starts recording from the associated camera. When the machine turns off, recording stops and the video is saved with an Atlanta timezone timestamp. + +### Key Features + +- **šŸ”„ MQTT Integration**: Listens to multiple machine state topics +- **šŸ“¹ Automatic Recording**: Starts/stops recording based on machine states +- **šŸ“· GigE Camera Support**: Uses python demo library (mvsdk) for camera control +- **⚔ Multi-threading**: Concurrent MQTT listening, camera monitoring, and recording +- **🌐 REST API**: FastAPI server for dashboard integration +- **šŸ“” WebSocket Support**: Real-time status updates +- **šŸ’¾ Storage Management**: Organized file storage with cleanup capabilities +- **šŸ“ Comprehensive Logging**: Detailed logging with rotation and error tracking +- **āš™ļø Configuration Management**: JSON-based configuration system +- **šŸ• Timezone Sync**: Proper time synchronization for Atlanta, Georgia + +## šŸ—ļø Architecture + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ MQTT Broker │ │ GigE Camera │ │ Dashboard │ +│ │ │ │ │ (React) │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ │ │ + │ Machine States │ Video Streams │ API Calls + │ │ │ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ USDA Vision Camera System │ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ +│ │ MQTT Client │ │ Camera │ │ API Server │ │ +│ │ │ │ Manager │ │ │ │ +│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ +│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ +│ │ State │ │ Storage │ │ Event │ │ +│ │ Manager │ │ Manager │ │ System │ │ +│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +## šŸ“‹ Prerequisites + +### Hardware Requirements +- GigE cameras compatible with python demo library +- Network connection to MQTT broker +- Sufficient storage space for video recordings + +### Software Requirements +- **Python 3.11+** +- **uv package manager** (recommended) or pip +- **MQTT broker** (e.g., Mosquitto, Home Assistant) +- **Linux system** (tested on Ubuntu/Debian) + +### Network Requirements +- Access to MQTT broker +- GigE cameras on network +- Internet access for time synchronization (optional but recommended) + +## šŸš€ Installation + +### 1. Clone the Repository +```bash +git clone https://github.com/your-username/USDA-Vision-Cameras.git +cd USDA-Vision-Cameras +``` + +### 2. Install Dependencies +Using uv (recommended): +```bash +# Install uv if not already installed +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install dependencies +uv sync +``` + +Using pip: +```bash +# Create virtual environment +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt +``` + +### 3. Setup GigE Camera Library +Ensure the `python demo` directory contains the mvsdk library for your GigE cameras. This should include: +- `mvsdk.py` - Python SDK wrapper +- Camera driver libraries +- Any camera-specific configuration files + +### 4. Configure Storage Directory +```bash +# Create storage directory (adjust path as needed) +mkdir -p ./storage +# Or for system-wide storage: +# sudo mkdir -p /storage && sudo chown $USER:$USER /storage +``` + +### 5. Setup Time Synchronization (Recommended) +```bash +# Run timezone setup for Atlanta, Georgia +./setup_timezone.sh +``` + +### 6. Configure the System +Edit `config.json` to match your setup: +```json +{ + "mqtt": { + "broker_host": "192.168.1.110", + "broker_port": 1883, + "topics": { + "machine1": "vision/machine1/state", + "machine2": "vision/machine2/state" + } + }, + "cameras": [ + { + "name": "camera1", + "machine_topic": "machine1", + "storage_path": "./storage/camera1", + "enabled": true + } + ] +} +``` + +## šŸ”§ Configuration + +### MQTT Configuration +```json +{ + "mqtt": { + "broker_host": "192.168.1.110", + "broker_port": 1883, + "username": null, + "password": null, + "topics": { + "vibratory_conveyor": "vision/vibratory_conveyor/state", + "blower_separator": "vision/blower_separator/state" + } + } +} +``` + +### Camera Configuration +```json +{ + "cameras": [ + { + "name": "camera1", + "machine_topic": "vibratory_conveyor", + "storage_path": "./storage/camera1", + "exposure_ms": 1.0, + "gain": 3.5, + "target_fps": 3.0, + "enabled": true + } + ] +} +``` + +### System Configuration +```json +{ + "system": { + "camera_check_interval_seconds": 2, + "log_level": "INFO", + "api_host": "0.0.0.0", + "api_port": 8000, + "enable_api": true, + "timezone": "America/New_York" + } +} +``` + +## šŸŽ® Usage + +### Quick Start +```bash +# Test the system +python test_system.py + +# Start the system +python main.py + +# Or use the startup script +./start_system.sh +``` + +### Command Line Options +```bash +# Custom configuration file +python main.py --config my_config.json + +# Debug mode +python main.py --log-level DEBUG + +# Help +python main.py --help +``` + +### Verify Installation +```bash +# Run system tests +python test_system.py + +# Check time synchronization +python check_time.py + +# Test timezone functions +python test_timezone.py +``` diff --git a/check_time.py b/check_time.py new file mode 100755 index 0000000..a8ee0c5 --- /dev/null +++ b/check_time.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +""" +Time verification script for USDA Vision Camera System +Checks if system time is properly synchronized +""" + +import datetime +import pytz +import requests +import json + +def check_system_time(): + """Check system time against multiple sources""" + print("šŸ• USDA Vision Camera System - Time Verification") + print("=" * 50) + + # Get local time + local_time = datetime.datetime.now() + utc_time = datetime.datetime.utcnow() + + # Get Atlanta timezone + atlanta_tz = pytz.timezone('America/New_York') + atlanta_time = datetime.datetime.now(atlanta_tz) + + print(f"Local system time: {local_time}") + print(f"UTC time: {utc_time}") + print(f"Atlanta time: {atlanta_time}") + print(f"Timezone: {atlanta_time.tzname()}") + + # Check against world time API + try: + print("\n🌐 Checking against world time API...") + response = requests.get("http://worldtimeapi.org/api/timezone/America/New_York", timeout=5) + if response.status_code == 200: + data = response.json() + api_time = datetime.datetime.fromisoformat(data['datetime'].replace('Z', '+00:00')) + + # Compare times (allow 5 second difference) + time_diff = abs((atlanta_time.replace(tzinfo=None) - api_time.replace(tzinfo=None)).total_seconds()) + + print(f"API time: {api_time}") + print(f"Time difference: {time_diff:.2f} seconds") + + if time_diff < 5: + print("āœ… Time is synchronized (within 5 seconds)") + return True + else: + print("āŒ Time is NOT synchronized (difference > 5 seconds)") + return False + else: + print("āš ļø Could not reach time API") + return None + except Exception as e: + print(f"āš ļø Error checking time API: {e}") + return None + +if __name__ == "__main__": + check_system_time() diff --git a/config.json b/config.json new file mode 100644 index 0000000..dd5f01f --- /dev/null +++ b/config.json @@ -0,0 +1,47 @@ +{ + "mqtt": { + "broker_host": "192.168.1.110", + "broker_port": 1883, + "username": null, + "password": null, + "topics": { + "vibratory_conveyor": "vision/vibratory_conveyor/state", + "blower_separator": "vision/blower_separator/state" + } + }, + "storage": { + "base_path": "./storage", + "max_file_size_mb": 1000, + "max_recording_duration_minutes": 60, + "cleanup_older_than_days": 30 + }, + "system": { + "camera_check_interval_seconds": 2, + "log_level": "INFO", + "log_file": "usda_vision_system.log", + "api_host": "0.0.0.0", + "api_port": 8000, + "enable_api": true, + "timezone": "America/New_York" + }, + "cameras": [ + { + "name": "camera1", + "machine_topic": "vibratory_conveyor", + "storage_path": "./storage/camera1", + "exposure_ms": 1.0, + "gain": 3.5, + "target_fps": 3.0, + "enabled": true + }, + { + "name": "camera2", + "machine_topic": "blower_separator", + "storage_path": "./storage/camera2", + "exposure_ms": 1.0, + "gain": 3.5, + "target_fps": 3.0, + "enabled": true + } + ] +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4b2e4d5 --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +""" +Main entry point for the USDA Vision Camera System. + +This script starts the complete system including MQTT monitoring, camera management, +and video recording based on machine state changes. +""" + +import sys +import os + +# Add the current directory to Python path to import our modules +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from usda_vision_system.main import main + +if __name__ == "__main__": + main() diff --git a/01README.md b/old tests/01README.md similarity index 100% rename from 01README.md rename to old tests/01README.md diff --git a/old tests/IMPLEMENTATION_SUMMARY.md b/old tests/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..f16e737 --- /dev/null +++ b/old tests/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,184 @@ +# USDA Vision Camera System - Implementation Summary + +## šŸŽ‰ Project Completed Successfully! + +The USDA Vision Camera System has been fully implemented and tested. All components are working correctly and the system is ready for deployment. + +## āœ… What Was Built + +### Core Architecture +- **Modular Design**: Clean separation of concerns across multiple modules +- **Multi-threading**: Concurrent MQTT listening, camera monitoring, and recording +- **Event-driven**: Thread-safe communication between components +- **Configuration-driven**: JSON-based configuration system + +### Key Components + +1. **MQTT Integration** (`usda_vision_system/mqtt/`) + - Listens to two machine topics: `vision/vibratory_conveyor/state` and `vision/blower_separator/state` + - Thread-safe message handling with automatic reconnection + - State normalization (on/off/error) + +2. **Camera Management** (`usda_vision_system/camera/`) + - Automatic GigE camera discovery using python demo library + - Periodic status monitoring (every 2 seconds) + - Camera initialization and configuration management + - **Discovered Cameras**: + - Blower-Yield-Cam (192.168.1.165) + - Cracker-Cam (192.168.1.167) + +3. **Video Recording** (`usda_vision_system/camera/recorder.py`) + - Automatic recording start/stop based on machine states + - Timestamp-based file naming: `camera1_recording_20250726_143022.avi` + - Configurable FPS, exposure, and gain settings + - Thread-safe recording with proper cleanup + +4. **Storage Management** (`usda_vision_system/storage/`) + - Organized file storage under `./storage/camera1/` and `./storage/camera2/` + - File indexing and metadata tracking + - Automatic cleanup of old files + - Storage statistics and integrity checking + +5. **REST API Server** (`usda_vision_system/api/`) + - FastAPI server on port 8000 + - Real-time WebSocket updates + - Manual recording control endpoints + - System status and monitoring endpoints + +6. **Comprehensive Logging** (`usda_vision_system/core/logging_config.py`) + - Colored console output + - Rotating log files + - Component-specific log levels + - Performance monitoring and error tracking + +## šŸš€ How to Use + +### Quick Start +```bash +# Run system tests +python test_system.py + +# Start the system +python main.py + +# Or use the startup script +./start_system.sh +``` + +### Configuration +Edit `config.json` to customize: +- MQTT broker settings +- Camera configurations +- Storage paths +- System parameters + +### API Access +- System status: `http://localhost:8000/system/status` +- Camera status: `http://localhost:8000/cameras` +- Manual recording: `POST http://localhost:8000/cameras/camera1/start-recording` +- Real-time updates: WebSocket at `ws://localhost:8000/ws` + +## šŸ“Š Test Results + +All system tests passed successfully: +- āœ… Module imports +- āœ… Configuration loading +- āœ… Camera discovery (found 2 cameras) +- āœ… Storage setup +- āœ… MQTT configuration +- āœ… System initialization +- āœ… API endpoints + +## šŸ”§ System Behavior + +### Automatic Recording Flow +1. **Machine turns ON** → MQTT message received → Recording starts automatically +2. **Machine turns OFF** → MQTT message received → Recording stops and saves file +3. **Files saved** with timestamp: `camera1_recording_YYYYMMDD_HHMMSS.avi` + +### Manual Control +- Start/stop recording via API calls +- Monitor system status in real-time +- Check camera availability on demand + +### Dashboard Integration +The system is designed to integrate with your React + Vite + Tailwind + Supabase dashboard: +- REST API for status queries +- WebSocket for real-time updates +- JSON responses for easy frontend consumption + +## šŸ“ Project Structure + +``` +usda_vision_system/ +ā”œā”€ā”€ core/ # Configuration, state management, events, logging +ā”œā”€ā”€ mqtt/ # MQTT client and message handlers +ā”œā”€ā”€ camera/ # Camera management, monitoring, recording +ā”œā”€ā”€ storage/ # File organization and management +ā”œā”€ā”€ api/ # FastAPI server and WebSocket support +└── main.py # Application coordinator + +Supporting Files: +ā”œā”€ā”€ main.py # Entry point script +ā”œā”€ā”€ config.json # System configuration +ā”œā”€ā”€ test_system.py # Test suite +ā”œā”€ā”€ start_system.sh # Startup script +└── README_SYSTEM.md # Comprehensive documentation +``` + +## šŸŽÆ Key Features Delivered + +- āœ… **Dual MQTT topic listening** for two machines +- āœ… **Automatic camera recording** triggered by machine states +- āœ… **GigE camera support** using python demo library +- āœ… **Thread-safe multi-tasking** (MQTT + camera monitoring + recording) +- āœ… **Timestamp-based file naming** in organized directories +- āœ… **2-second camera status monitoring** with on-demand checks +- āœ… **REST API and WebSocket** for dashboard integration +- āœ… **Comprehensive logging** with error tracking +- āœ… **Configuration management** via JSON +- āœ… **Storage management** with cleanup capabilities +- āœ… **Graceful startup/shutdown** with signal handling + +## šŸ”® Ready for Dashboard Integration + +The system provides everything needed for your React dashboard: + +```javascript +// Example API usage +const systemStatus = await fetch('http://localhost:8000/system/status'); +const cameras = await fetch('http://localhost:8000/cameras'); + +// WebSocket for real-time updates +const ws = new WebSocket('ws://localhost:8000/ws'); +ws.onmessage = (event) => { + const update = JSON.parse(event.data); + // Handle real-time system updates +}; + +// Manual recording control +await fetch('http://localhost:8000/cameras/camera1/start-recording', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ camera_name: 'camera1' }) +}); +``` + +## šŸŽŠ Next Steps + +The system is production-ready! You can now: + +1. **Deploy** the system on your target hardware +2. **Integrate** with your existing React dashboard +3. **Configure** MQTT topics and camera settings as needed +4. **Monitor** system performance through logs and API endpoints +5. **Extend** functionality as requirements evolve + +The modular architecture makes it easy to add new features, cameras, or MQTT topics in the future. + +--- + +**System Status**: āœ… **FULLY OPERATIONAL** +**Test Results**: āœ… **ALL TESTS PASSING** +**Cameras Detected**: āœ… **2 GIGE CAMERAS READY** +**Ready for Production**: āœ… **YES** diff --git a/old tests/README.md b/old tests/README.md new file mode 100644 index 0000000..0bb8d07 --- /dev/null +++ b/old tests/README.md @@ -0,0 +1 @@ +# USDA-Vision-Cameras \ No newline at end of file diff --git a/old tests/README_SYSTEM.md b/old tests/README_SYSTEM.md new file mode 100644 index 0000000..932f632 --- /dev/null +++ b/old tests/README_SYSTEM.md @@ -0,0 +1,249 @@ +# USDA Vision Camera System + +A comprehensive system for monitoring machines via MQTT and automatically recording video from GigE cameras when machines are active. + +## Overview + +This system integrates MQTT machine monitoring with automated video recording from GigE cameras. When a machine turns on (detected via MQTT), the system automatically starts recording from the associated camera. When the machine turns off, recording stops and the video is saved with a timestamp. + +## Features + +- **MQTT Integration**: Listens to multiple machine state topics +- **Automatic Recording**: Starts/stops recording based on machine states +- **GigE Camera Support**: Uses the python demo library (mvsdk) for camera control +- **Multi-threading**: Concurrent MQTT listening, camera monitoring, and recording +- **REST API**: FastAPI server for dashboard integration +- **WebSocket Support**: Real-time status updates +- **Storage Management**: Organized file storage with cleanup capabilities +- **Comprehensive Logging**: Detailed logging with rotation and error tracking +- **Configuration Management**: JSON-based configuration system + +## Architecture + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ MQTT Broker │ │ GigE Camera │ │ Dashboard │ +│ │ │ │ │ (React) │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ │ │ + │ Machine States │ Video Streams │ API Calls + │ │ │ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ USDA Vision Camera System │ +ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ +│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ +│ │ MQTT Client │ │ Camera │ │ API Server │ │ +│ │ │ │ Manager │ │ │ │ +│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ +│ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ +│ │ State │ │ Storage │ │ Event │ │ +│ │ Manager │ │ Manager │ │ System │ │ +│ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +## Installation + +1. **Prerequisites**: + - Python 3.11+ + - GigE cameras with python demo library + - MQTT broker (e.g., Mosquitto) + - uv package manager (recommended) + +2. **Install Dependencies**: + ```bash + uv sync + ``` + +3. **Setup Storage Directory**: + ```bash + sudo mkdir -p /storage + sudo chown $USER:$USER /storage + ``` + +## Configuration + +Edit `config.json` to configure your system: + +```json +{ + "mqtt": { + "broker_host": "192.168.1.110", + "broker_port": 1883, + "topics": { + "vibratory_conveyor": "vision/vibratory_conveyor/state", + "blower_separator": "vision/blower_separator/state" + } + }, + "cameras": [ + { + "name": "camera1", + "machine_topic": "vibratory_conveyor", + "storage_path": "/storage/camera1", + "exposure_ms": 1.0, + "gain": 3.5, + "target_fps": 3.0, + "enabled": true + } + ] +} +``` + +## Usage + +### Basic Usage + +1. **Start the System**: + ```bash + python main.py + ``` + +2. **With Custom Config**: + ```bash + python main.py --config my_config.json + ``` + +3. **Debug Mode**: + ```bash + python main.py --log-level DEBUG + ``` + +### API Endpoints + +The system provides a REST API on port 8000: + +- `GET /system/status` - Overall system status +- `GET /cameras` - All camera statuses +- `GET /machines` - All machine states +- `POST /cameras/{name}/start-recording` - Manual recording start +- `POST /cameras/{name}/stop-recording` - Manual recording stop +- `GET /storage/stats` - Storage statistics +- `WebSocket /ws` - Real-time updates + +### Dashboard Integration + +The system is designed to integrate with your existing React + Vite + Tailwind + Supabase dashboard: + +1. **API Integration**: Use the REST endpoints to display system status +2. **WebSocket**: Connect to `/ws` for real-time updates +3. **Supabase Storage**: Store recording metadata and system logs + +## File Organization + +``` +/storage/ +ā”œā”€ā”€ camera1/ +│ ā”œā”€ā”€ camera1_recording_20250726_143022.avi +│ └── camera1_recording_20250726_143155.avi +ā”œā”€ā”€ camera2/ +│ ā”œā”€ā”€ camera2_recording_20250726_143025.avi +│ └── camera2_recording_20250726_143158.avi +└── file_index.json +``` + +## Monitoring and Logging + +### Log Files + +- `usda_vision_system.log` - Main system log (rotated) +- Console output with colored formatting +- Component-specific log levels + +### Performance Monitoring + +The system includes built-in performance monitoring: +- Startup times +- Recording session metrics +- MQTT message processing rates +- Camera status check intervals + +### Error Tracking + +Comprehensive error tracking with: +- Error counts per component +- Detailed error context +- Automatic recovery attempts + +## Troubleshooting + +### Common Issues + +1. **Camera Not Found**: + - Check camera connections + - Verify python demo library installation + - Run camera discovery: Check logs for enumeration results + +2. **MQTT Connection Failed**: + - Verify broker IP and port + - Check network connectivity + - Verify credentials if authentication is enabled + +3. **Recording Fails**: + - Check storage permissions + - Verify available disk space + - Check camera initialization logs + +4. **API Server Won't Start**: + - Check if port 8000 is available + - Verify FastAPI dependencies + - Check firewall settings + +### Debug Commands + +```bash +# Check system status +curl http://localhost:8000/system/status + +# Check camera status +curl http://localhost:8000/cameras + +# Manual recording start +curl -X POST http://localhost:8000/cameras/camera1/start-recording \ + -H "Content-Type: application/json" \ + -d '{"camera_name": "camera1"}' +``` + +## Development + +### Project Structure + +``` +usda_vision_system/ +ā”œā”€ā”€ core/ # Core functionality +ā”œā”€ā”€ mqtt/ # MQTT client and handlers +ā”œā”€ā”€ camera/ # Camera management and recording +ā”œā”€ā”€ storage/ # File management +ā”œā”€ā”€ api/ # FastAPI server +└── main.py # Application coordinator +``` + +### Adding New Features + +1. **New Camera Type**: Extend `camera/recorder.py` +2. **New MQTT Topics**: Update `config.json` and `mqtt/handlers.py` +3. **New API Endpoints**: Add to `api/server.py` +4. **New Events**: Define in `core/events.py` + +### Testing + +```bash +# Run basic system test +python -c "from usda_vision_system import USDAVisionSystem; s = USDAVisionSystem(); print('OK')" + +# Test MQTT connection +python -c "from usda_vision_system.mqtt.client import MQTTClient; # ... test code" + +# Test camera discovery +python -c "import sys; sys.path.append('python demo'); import mvsdk; print(len(mvsdk.CameraEnumerateDevice()))" +``` + +## License + +This project is developed for USDA research purposes. + +## Support + +For issues and questions: +1. Check the logs in `usda_vision_system.log` +2. Review the troubleshooting section +3. Check API status at `http://localhost:8000/health` diff --git a/old tests/TIMEZONE_SETUP_SUMMARY.md b/old tests/TIMEZONE_SETUP_SUMMARY.md new file mode 100644 index 0000000..9866f08 --- /dev/null +++ b/old tests/TIMEZONE_SETUP_SUMMARY.md @@ -0,0 +1,190 @@ +# Time Synchronization Setup - Atlanta, Georgia + +## āœ… Time Synchronization Complete! + +The USDA Vision Camera System has been configured for proper time synchronization with Atlanta, Georgia (Eastern Time Zone). + +## šŸ• What Was Implemented + +### System-Level Time Configuration +- **Timezone**: Set to `America/New_York` (Eastern Time) +- **Current Status**: Eastern Daylight Time (EDT, UTC-4) +- **NTP Sync**: Configured with multiple reliable time servers +- **Hardware Clock**: Synchronized with system time + +### Application-Level Timezone Support +- **Timezone-Aware Timestamps**: All recordings use Atlanta time +- **Automatic DST Handling**: Switches between EST/EDT automatically +- **Time Sync Monitoring**: Built-in time synchronization checking +- **Consistent Formatting**: Standardized timestamp formats throughout + +## šŸ”§ Key Features + +### 1. Automatic Time Synchronization +```bash +# NTP servers configured: +- time.nist.gov (NIST atomic clock) +- pool.ntp.org (NTP pool) +- time.google.com (Google time) +- time.cloudflare.com (Cloudflare time) +``` + +### 2. Timezone-Aware Recording Filenames +``` +Example: camera1_recording_20250725_213241.avi +Format: {camera}_{type}_{YYYYMMDD_HHMMSS}.avi +Time: Atlanta local time (EDT/EST) +``` + +### 3. Time Verification Tools +- **Startup Check**: Automatic time sync verification on system start +- **Manual Check**: `python check_time.py` for on-demand verification +- **API Integration**: Time sync status available via REST API + +### 4. Comprehensive Logging +``` +=== TIME SYNCHRONIZATION STATUS === +System time: 2025-07-25 21:32:41 EDT +Timezone: EDT (-0400) +Daylight Saving: Yes +Sync status: synchronized +Time difference: 0.10 seconds +===================================== +``` + +## šŸš€ Usage + +### Automatic Operation +The system automatically: +- Uses Atlanta time for all timestamps +- Handles daylight saving time transitions +- Monitors time synchronization status +- Logs time-related events + +### Manual Verification +```bash +# Check time synchronization +python check_time.py + +# Test timezone functions +python test_timezone.py + +# View system time status +timedatectl status +``` + +### API Endpoints +```bash +# System status includes time info +curl http://localhost:8000/system/status + +# Example response includes: +{ + "system_started": true, + "uptime_seconds": 3600, + "timestamp": "2025-07-25T21:32:41-04:00" +} +``` + +## šŸ“Š Current Status + +### Time Synchronization +- āœ… **System Timezone**: America/New_York (EDT) +- āœ… **NTP Sync**: Active and synchronized +- āœ… **Time Accuracy**: Within 0.1 seconds of atomic time +- āœ… **DST Support**: Automatic EST/EDT switching + +### Application Integration +- āœ… **Recording Timestamps**: Atlanta time zone +- āœ… **Log Timestamps**: Timezone-aware logging +- āœ… **API Responses**: ISO format with timezone +- āœ… **File Naming**: Consistent Atlanta time format + +### Monitoring +- āœ… **Startup Verification**: Time sync checked on boot +- āœ… **Continuous Monitoring**: Built-in sync status tracking +- āœ… **Error Detection**: Alerts for time drift issues +- āœ… **Manual Tools**: On-demand verification scripts + +## šŸ” Technical Details + +### Timezone Configuration +```json +{ + "system": { + "timezone": "America/New_York" + } +} +``` + +### Time Sources +1. **Primary**: NIST atomic clock (time.nist.gov) +2. **Secondary**: NTP pool servers (pool.ntp.org) +3. **Backup**: Google/Cloudflare time servers +4. **Fallback**: Local system clock + +### File Naming Convention +``` +Pattern: {camera_name}_recording_{YYYYMMDD_HHMMSS}.avi +Example: camera1_recording_20250725_213241.avi +Timezone: Always Atlanta local time (EST/EDT) +``` + +## šŸŽÆ Benefits + +### For Operations +- **Consistent Timestamps**: All recordings use Atlanta time +- **Easy Correlation**: Timestamps match local business hours +- **Automatic DST**: No manual timezone adjustments needed +- **Reliable Sync**: Multiple time sources ensure accuracy + +### For Analysis +- **Local Time Context**: Recordings timestamped in business timezone +- **Accurate Sequencing**: Precise timing for event correlation +- **Standard Format**: Consistent naming across all recordings +- **Audit Trail**: Complete time synchronization logging + +### For Integration +- **Dashboard Ready**: Timezone-aware API responses +- **Database Compatible**: ISO format timestamps with timezone +- **Log Analysis**: Structured time information in logs +- **Monitoring**: Built-in time sync health checks + +## šŸ”§ Maintenance + +### Regular Checks +The system automatically: +- Verifies time sync on startup +- Logs time synchronization status +- Monitors for time drift +- Alerts on sync failures + +### Manual Maintenance +```bash +# Force time sync +sudo systemctl restart systemd-timesyncd + +# Check NTP status +timedatectl show-timesync --all + +# Verify timezone +timedatectl status +``` + +## šŸ“ˆ Next Steps + +The time synchronization is now fully operational. The system will: + +1. **Automatically maintain** accurate Atlanta time +2. **Generate timestamped recordings** with local time +3. **Monitor sync status** and alert on issues +4. **Provide timezone-aware** API responses for dashboard integration + +All recording files will now have accurate Atlanta timestamps, making it easy to correlate with local business operations and machine schedules. + +--- + +**Time Sync Status**: āœ… **SYNCHRONIZED** +**Timezone**: āœ… **America/New_York (EDT)** +**Accuracy**: āœ… **±0.1 seconds** +**Ready for Production**: āœ… **YES** diff --git a/VIDEO_RECORDER_README.md b/old tests/VIDEO_RECORDER_README.md similarity index 100% rename from VIDEO_RECORDER_README.md rename to old tests/VIDEO_RECORDER_README.md diff --git a/camera_capture.py b/old tests/camera_capture.py similarity index 100% rename from camera_capture.py rename to old tests/camera_capture.py diff --git a/camera_status_test.ipynb b/old tests/camera_status_test.ipynb similarity index 97% rename from camera_status_test.ipynb rename to old tests/camera_status_test.ipynb index ffd0f85..eba562f 100644 --- a/camera_status_test.ipynb +++ b/old tests/camera_status_test.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [ @@ -27,7 +27,7 @@ "output_type": "stream", "text": [ "Libraries imported successfully!\n", - "Platform: Windows\n" + "Platform: Linux\n" ] } ], @@ -51,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 2, "id": "error-codes", "metadata": {}, "outputs": [ @@ -88,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 3, "id": "status-functions", "metadata": {}, "outputs": [ @@ -100,8 +100,8 @@ "==============================\n", "āœ“ SDK initialized successfully\n", "āœ“ Found 2 camera(s)\n", - " 0: Blower-Yield-Cam (NET-1000M-192.168.1.165)\n", - " 1: Cracker-Cam (NET-1000M-192.168.1.167)\n", + " 0: Blower-Yield-Cam (192.168.1.165-192.168.1.54)\n", + " 1: Cracker-Cam (192.168.1.167-192.168.1.54)\n", "\n", "Testing camera 0: Blower-Yield-Cam\n", "āœ“ Camera is available (not opened by another process)\n", @@ -215,7 +215,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 4, "id": "test-capture-availability", "metadata": {}, "outputs": [ @@ -375,7 +375,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 5, "id": "comprehensive-check", "metadata": {}, "outputs": [ @@ -391,8 +391,8 @@ "==============================\n", "āœ“ SDK initialized successfully\n", "āœ“ Found 2 camera(s)\n", - " 0: Blower-Yield-Cam (NET-1000M-192.168.1.165)\n", - " 1: Cracker-Cam (NET-1000M-192.168.1.167)\n", + " 0: Blower-Yield-Cam (192.168.1.165-192.168.1.54)\n", + " 1: Cracker-Cam (192.168.1.167-192.168.1.54)\n", "\n", "Testing camera 0: Blower-Yield-Cam\n", "āœ“ Camera is available (not opened by another process)\n", @@ -408,7 +408,7 @@ "FINAL RESULTS:\n", "Camera Available: False\n", "Capture Ready: False\n", - "Status: (34, 'AVAILABLE')\n", + "Status: (6, 'AVAILABLE')\n", "==================================================\n" ] } @@ -455,7 +455,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 6, "id": "status-check-function", "metadata": {}, "outputs": [ @@ -585,7 +585,7 @@ ], "metadata": { "kernelspec": { - "display_name": "cc_pecan", + "display_name": "USDA-vision-cameras", "language": "python", "name": "python3" }, @@ -599,7 +599,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.5" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/old tests/camera_test_setup.ipynb b/old tests/camera_test_setup.ipynb new file mode 100644 index 0000000..08ecbab --- /dev/null +++ b/old tests/camera_test_setup.ipynb @@ -0,0 +1,349 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# GigE Camera Test Setup\n", + "\n", + "This notebook helps you test and configure your GigE cameras for the USDA vision project.\n", + "\n", + "## Key Features:\n", + "- Test camera connectivity\n", + "- Display images inline (no GUI needed)\n", + "- Save test images/videos to `/storage`\n", + "- Configure camera parameters\n", + "- Test recording functionality" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "from datetime import datetime\n", + "import time\n", + "from pathlib import Path\n", + "import imageio\n", + "from tqdm import tqdm\n", + "\n", + "# Configure matplotlib for inline display\n", + "plt.rcParams['figure.figsize'] = (12, 8)\n", + "plt.rcParams['image.cmap'] = 'gray'\n", + "\n", + "print(\"āœ… All imports successful!\")\n", + "print(f\"OpenCV version: {cv2.__version__}\")\n", + "print(f\"NumPy version: {np.__version__}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Utility Functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def display_image(image, title=\"Image\", figsize=(10, 8)):\n", + " \"\"\"Display image inline in Jupyter notebook\"\"\"\n", + " plt.figure(figsize=figsize)\n", + " if len(image.shape) == 3:\n", + " # Convert BGR to RGB for matplotlib\n", + " image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n", + " plt.imshow(image_rgb)\n", + " else:\n", + " plt.imshow(image, cmap='gray')\n", + " plt.title(title)\n", + " plt.axis('off')\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "def save_image_to_storage(image, filename_prefix=\"test_image\"):\n", + " \"\"\"Save image to /storage with timestamp\"\"\"\n", + " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n", + " filename = f\"{filename_prefix}_{timestamp}.jpg\"\n", + " filepath = f\"/storage/{filename}\"\n", + " \n", + " success = cv2.imwrite(filepath, image)\n", + " if success:\n", + " print(f\"āœ… Image saved: {filepath}\")\n", + " return filepath\n", + " else:\n", + " print(f\"āŒ Failed to save image: {filepath}\")\n", + " return None\n", + "\n", + "def create_storage_subdir(subdir_name):\n", + " \"\"\"Create subdirectory in /storage\"\"\"\n", + " path = Path(f\"/storage/{subdir_name}\")\n", + " path.mkdir(exist_ok=True)\n", + " print(f\"šŸ“ Directory ready: {path}\")\n", + " return str(path)\n", + "\n", + "def list_available_cameras():\n", + " \"\"\"List all available camera devices\"\"\"\n", + " print(\"šŸ” Scanning for available cameras...\")\n", + " available_cameras = []\n", + " \n", + " # Test camera indices 0-10\n", + " for i in range(11):\n", + " cap = cv2.VideoCapture(i)\n", + " if cap.isOpened():\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " available_cameras.append(i)\n", + " print(f\"šŸ“· Camera {i}: Available (Resolution: {frame.shape[1]}x{frame.shape[0]})\")\n", + " cap.release()\n", + " else:\n", + " # Try with different backends for GigE cameras\n", + " cap = cv2.VideoCapture(i, cv2.CAP_GSTREAMER)\n", + " if cap.isOpened():\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " available_cameras.append(i)\n", + " print(f\"šŸ“· Camera {i}: Available via GStreamer (Resolution: {frame.shape[1]}x{frame.shape[0]})\")\n", + " cap.release()\n", + " \n", + " if not available_cameras:\n", + " print(\"āŒ No cameras found\")\n", + " \n", + " return available_cameras\n", + "\n", + "print(\"āœ… Utility functions loaded!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Check Storage Directory" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check storage directory\n", + "storage_path = Path(\"/storage\")\n", + "print(f\"Storage directory exists: {storage_path.exists()}\")\n", + "print(f\"Storage directory writable: {os.access('/storage', os.W_OK)}\")\n", + "\n", + "# Create test subdirectories\n", + "test_images_dir = create_storage_subdir(\"test_images\")\n", + "test_videos_dir = create_storage_subdir(\"test_videos\")\n", + "camera1_dir = create_storage_subdir(\"camera1\")\n", + "camera2_dir = create_storage_subdir(\"camera2\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Scan for Available Cameras" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Scan for cameras\n", + "cameras = list_available_cameras()\n", + "print(f\"\\nšŸ“Š Summary: Found {len(cameras)} camera(s): {cameras}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Test Individual Camera" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test a specific camera (change camera_id as needed)\n", + "camera_id = 0 # Change this to test different cameras\n", + "\n", + "print(f\"šŸ”§ Testing camera {camera_id}...\")\n", + "\n", + "# Try different backends for GigE cameras\n", + "backends_to_try = [\n", + " (cv2.CAP_ANY, \"Default\"),\n", + " (cv2.CAP_GSTREAMER, \"GStreamer\"),\n", + " (cv2.CAP_V4L2, \"V4L2\"),\n", + " (cv2.CAP_FFMPEG, \"FFmpeg\")\n", + "]\n", + "\n", + "successful_backend = None\n", + "cap = None\n", + "\n", + "for backend, name in backends_to_try:\n", + " print(f\" Trying {name} backend...\")\n", + " cap = cv2.VideoCapture(camera_id, backend)\n", + " if cap.isOpened():\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " print(f\" āœ… {name} backend works!\")\n", + " successful_backend = (backend, name)\n", + " break\n", + " else:\n", + " print(f\" āŒ {name} backend opened but can't read frames\")\n", + " else:\n", + " print(f\" āŒ {name} backend failed to open\")\n", + " cap.release()\n", + "\n", + "if successful_backend:\n", + " backend, backend_name = successful_backend\n", + " cap = cv2.VideoCapture(camera_id, backend)\n", + " \n", + " # Get camera properties\n", + " width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n", + " height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n", + " fps = cap.get(cv2.CAP_PROP_FPS)\n", + " \n", + " print(f\"\\nšŸ“· Camera {camera_id} Properties ({backend_name}):\")\n", + " print(f\" Resolution: {width}x{height}\")\n", + " print(f\" FPS: {fps}\")\n", + " \n", + " # Capture a test frame\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " print(f\" Frame shape: {frame.shape}\")\n", + " print(f\" Frame dtype: {frame.dtype}\")\n", + " \n", + " # Display the frame\n", + " display_image(frame, f\"Camera {camera_id} Test Frame\")\n", + " \n", + " # Save test image\n", + " save_image_to_storage(frame, f\"camera_{camera_id}_test\")\n", + " else:\n", + " print(\" āŒ Failed to capture frame\")\n", + " \n", + " cap.release()\n", + "else:\n", + " print(f\"āŒ Camera {camera_id} not accessible with any backend\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Test Video Recording" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test video recording\n", + "def test_video_recording(camera_id, duration_seconds=5, fps=30):\n", + " \"\"\"Test video recording from camera\"\"\"\n", + " print(f\"šŸŽ„ Testing video recording from camera {camera_id} for {duration_seconds} seconds...\")\n", + " \n", + " # Open camera\n", + " cap = cv2.VideoCapture(camera_id)\n", + " if not cap.isOpened():\n", + " print(f\"āŒ Cannot open camera {camera_id}\")\n", + " return None\n", + " \n", + " # Get camera properties\n", + " width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n", + " height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n", + " \n", + " # Create video writer\n", + " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n", + " video_filename = f\"/storage/test_videos/camera_{camera_id}_test_{timestamp}.mp4\"\n", + " \n", + " fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n", + " out = cv2.VideoWriter(video_filename, fourcc, fps, (width, height))\n", + " \n", + " if not out.isOpened():\n", + " print(\"āŒ Cannot create video writer\")\n", + " cap.release()\n", + " return None\n", + " \n", + " # Record video\n", + " frames_to_capture = duration_seconds * fps\n", + " frames_captured = 0\n", + " \n", + " print(f\"Recording {frames_to_capture} frames...\")\n", + " \n", + " with tqdm(total=frames_to_capture, desc=\"Recording\") as pbar:\n", + " start_time = time.time()\n", + " \n", + " while frames_captured < frames_to_capture:\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " out.write(frame)\n", + " frames_captured += 1\n", + " pbar.update(1)\n", + " \n", + " # Display first frame\n", + " if frames_captured == 1:\n", + " display_image(frame, f\"First frame from camera {camera_id}\")\n", + " else:\n", + " print(f\"āŒ Failed to read frame {frames_captured}\")\n", + " break\n", + " \n", + " # Cleanup\n", + " cap.release()\n", + " out.release()\n", + " \n", + " elapsed_time = time.time() - start_time\n", + " actual_fps = frames_captured / elapsed_time\n", + " \n", + " print(f\"āœ… Video saved: {video_filename}\")\n", + " print(f\"šŸ“Š Captured {frames_captured} frames in {elapsed_time:.2f}s\")\n", + " print(f\"šŸ“Š Actual FPS: {actual_fps:.2f}\")\n", + " \n", + " return video_filename\n", + "\n", + "# Test recording (change camera_id as needed)\n", + "if cameras: # Only test if cameras were found\n", + " test_camera = cameras[0] # Use first available camera\n", + " video_file = test_video_recording(test_camera, duration_seconds=3)\n", + "else:\n", + " print(\"āš ļø No cameras available for video test\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "usda-vision-cameras", + "language": "python", + "name": "usda-vision-cameras" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/camera_video_recorder.py b/old tests/camera_video_recorder.py similarity index 100% rename from camera_video_recorder.py rename to old tests/camera_video_recorder.py diff --git a/exposure test.ipynb b/old tests/exposure test.ipynb similarity index 100% rename from exposure test.ipynb rename to old tests/exposure test.ipynb diff --git a/old tests/gige_camera_advanced.ipynb b/old tests/gige_camera_advanced.ipynb new file mode 100644 index 0000000..d4c7525 --- /dev/null +++ b/old tests/gige_camera_advanced.ipynb @@ -0,0 +1,385 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Advanced GigE Camera Configuration\n", + "\n", + "This notebook provides advanced testing and configuration for GigE cameras.\n", + "\n", + "## Features:\n", + "- Network interface detection\n", + "- GigE camera discovery\n", + "- Camera parameter configuration\n", + "- Performance testing\n", + "- Dual camera synchronization testing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import cv2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import subprocess\n", + "import socket\n", + "import threading\n", + "import time\n", + "from datetime import datetime\n", + "import os\n", + "from pathlib import Path\n", + "import json\n", + "\n", + "print(\"āœ… Imports successful!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Network Interface Detection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_network_interfaces():\n", + " \"\"\"Get network interface information\"\"\"\n", + " try:\n", + " result = subprocess.run(['ip', 'addr', 'show'], capture_output=True, text=True)\n", + " print(\"🌐 Network Interfaces:\")\n", + " print(result.stdout)\n", + " \n", + " # Also check for GigE specific interfaces\n", + " result2 = subprocess.run(['ifconfig'], capture_output=True, text=True)\n", + " if result2.returncode == 0:\n", + " print(\"\\nšŸ“” Interface Configuration:\")\n", + " print(result2.stdout)\n", + " except Exception as e:\n", + " print(f\"āŒ Error getting network info: {e}\")\n", + "\n", + "get_network_interfaces()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GigE Camera Discovery" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def discover_gige_cameras():\n", + " \"\"\"Attempt to discover GigE cameras on the network\"\"\"\n", + " print(\"šŸ” Discovering GigE cameras...\")\n", + " \n", + " # Try different methods to find GigE cameras\n", + " methods = [\n", + " \"OpenCV with different backends\",\n", + " \"Network scanning\",\n", + " \"GStreamer pipeline testing\"\n", + " ]\n", + " \n", + " print(\"\\n1. Testing OpenCV backends:\")\n", + " backends = [\n", + " (cv2.CAP_GSTREAMER, \"GStreamer\"),\n", + " (cv2.CAP_V4L2, \"V4L2\"),\n", + " (cv2.CAP_FFMPEG, \"FFmpeg\"),\n", + " (cv2.CAP_ANY, \"Default\")\n", + " ]\n", + " \n", + " for backend_id, backend_name in backends:\n", + " print(f\" Testing {backend_name}...\")\n", + " for cam_id in range(5):\n", + " try:\n", + " cap = cv2.VideoCapture(cam_id, backend_id)\n", + " if cap.isOpened():\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " print(f\" āœ… Camera {cam_id} accessible via {backend_name}\")\n", + " print(f\" Resolution: {frame.shape[1]}x{frame.shape[0]}\")\n", + " cap.release()\n", + " except Exception as e:\n", + " pass\n", + " \n", + " print(\"\\n2. Testing GStreamer pipelines:\")\n", + " # Common GigE camera GStreamer pipelines\n", + " gstreamer_pipelines = [\n", + " \"v4l2src device=/dev/video0 ! videoconvert ! appsink\",\n", + " \"v4l2src device=/dev/video1 ! videoconvert ! appsink\",\n", + " \"tcambin ! videoconvert ! appsink\", # For TIS cameras\n", + " \"aravis ! videoconvert ! appsink\", # For Aravis-supported cameras\n", + " ]\n", + " \n", + " for pipeline in gstreamer_pipelines:\n", + " try:\n", + " print(f\" Testing: {pipeline}\")\n", + " cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)\n", + " if cap.isOpened():\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " print(f\" āœ… Pipeline works! Frame shape: {frame.shape}\")\n", + " else:\n", + " print(f\" āš ļø Pipeline opened but no frames\")\n", + " else:\n", + " print(f\" āŒ Pipeline failed\")\n", + " cap.release()\n", + " except Exception as e:\n", + " print(f\" āŒ Error: {e}\")\n", + "\n", + "discover_gige_cameras()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Camera Parameter Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def configure_camera_parameters(camera_id, backend=cv2.CAP_ANY):\n", + " \"\"\"Configure and test camera parameters\"\"\"\n", + " print(f\"āš™ļø Configuring camera {camera_id}...\")\n", + " \n", + " cap = cv2.VideoCapture(camera_id, backend)\n", + " if not cap.isOpened():\n", + " print(f\"āŒ Cannot open camera {camera_id}\")\n", + " return None\n", + " \n", + " # Get current parameters\n", + " current_params = {\n", + " 'width': cap.get(cv2.CAP_PROP_FRAME_WIDTH),\n", + " 'height': cap.get(cv2.CAP_PROP_FRAME_HEIGHT),\n", + " 'fps': cap.get(cv2.CAP_PROP_FPS),\n", + " 'brightness': cap.get(cv2.CAP_PROP_BRIGHTNESS),\n", + " 'contrast': cap.get(cv2.CAP_PROP_CONTRAST),\n", + " 'saturation': cap.get(cv2.CAP_PROP_SATURATION),\n", + " 'hue': cap.get(cv2.CAP_PROP_HUE),\n", + " 'gain': cap.get(cv2.CAP_PROP_GAIN),\n", + " 'exposure': cap.get(cv2.CAP_PROP_EXPOSURE),\n", + " 'auto_exposure': cap.get(cv2.CAP_PROP_AUTO_EXPOSURE),\n", + " 'white_balance': cap.get(cv2.CAP_PROP_WHITE_BALANCE_BLUE_U),\n", + " }\n", + " \n", + " print(\"šŸ“Š Current Camera Parameters:\")\n", + " for param, value in current_params.items():\n", + " print(f\" {param}: {value}\")\n", + " \n", + " # Test setting some parameters\n", + " print(\"\\nšŸ”§ Testing parameter changes:\")\n", + " \n", + " # Try to set resolution (common GigE resolutions)\n", + " test_resolutions = [(1920, 1080), (1280, 720), (640, 480)]\n", + " for width, height in test_resolutions:\n", + " if cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) and cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height):\n", + " actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)\n", + " actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)\n", + " print(f\" Resolution {width}x{height}: Set to {actual_width}x{actual_height}\")\n", + " break\n", + " \n", + " # Test FPS settings\n", + " for fps in [30, 60, 120]:\n", + " if cap.set(cv2.CAP_PROP_FPS, fps):\n", + " actual_fps = cap.get(cv2.CAP_PROP_FPS)\n", + " print(f\" FPS {fps}: Set to {actual_fps}\")\n", + " break\n", + " \n", + " # Capture test frame with new settings\n", + " ret, frame = cap.read()\n", + " if ret:\n", + " print(f\"\\nāœ… Test frame captured: {frame.shape}\")\n", + " \n", + " # Display frame\n", + " plt.figure(figsize=(10, 6))\n", + " if len(frame.shape) == 3:\n", + " plt.imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))\n", + " else:\n", + " plt.imshow(frame, cmap='gray')\n", + " plt.title(f\"Camera {camera_id} - Configured\")\n", + " plt.axis('off')\n", + " plt.show()\n", + " \n", + " # Save configuration and test image\n", + " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n", + " \n", + " # Save image\n", + " img_path = f\"/storage/camera{camera_id}/configured_test_{timestamp}.jpg\"\n", + " cv2.imwrite(img_path, frame)\n", + " print(f\"šŸ’¾ Test image saved: {img_path}\")\n", + " \n", + " # Save configuration\n", + " config_path = f\"/storage/camera{camera_id}/config_{timestamp}.json\"\n", + " with open(config_path, 'w') as f:\n", + " json.dump(current_params, f, indent=2)\n", + " print(f\"šŸ’¾ Configuration saved: {config_path}\")\n", + " \n", + " cap.release()\n", + " return current_params\n", + "\n", + "# Test configuration (change camera_id as needed)\n", + "camera_to_configure = 0\n", + "config = configure_camera_parameters(camera_to_configure)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dual Camera Testing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def test_dual_cameras(camera1_id=0, camera2_id=1, duration=5):\n", + " \"\"\"Test simultaneous capture from two cameras\"\"\"\n", + " print(f\"šŸ“·šŸ“· Testing dual camera capture (cameras {camera1_id} and {camera2_id})...\")\n", + " \n", + " # Open both cameras\n", + " cap1 = cv2.VideoCapture(camera1_id)\n", + " cap2 = cv2.VideoCapture(camera2_id)\n", + " \n", + " if not cap1.isOpened():\n", + " print(f\"āŒ Cannot open camera {camera1_id}\")\n", + " return\n", + " \n", + " if not cap2.isOpened():\n", + " print(f\"āŒ Cannot open camera {camera2_id}\")\n", + " cap1.release()\n", + " return\n", + " \n", + " print(\"āœ… Both cameras opened successfully\")\n", + " \n", + " # Capture test frames\n", + " ret1, frame1 = cap1.read()\n", + " ret2, frame2 = cap2.read()\n", + " \n", + " if ret1 and ret2:\n", + " print(f\"šŸ“Š Camera {camera1_id}: {frame1.shape}\")\n", + " print(f\"šŸ“Š Camera {camera2_id}: {frame2.shape}\")\n", + " \n", + " # Display both frames side by side\n", + " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))\n", + " \n", + " if len(frame1.shape) == 3:\n", + " ax1.imshow(cv2.cvtColor(frame1, cv2.COLOR_BGR2RGB))\n", + " else:\n", + " ax1.imshow(frame1, cmap='gray')\n", + " ax1.set_title(f\"Camera {camera1_id}\")\n", + " ax1.axis('off')\n", + " \n", + " if len(frame2.shape) == 3:\n", + " ax2.imshow(cv2.cvtColor(frame2, cv2.COLOR_BGR2RGB))\n", + " else:\n", + " ax2.imshow(frame2, cmap='gray')\n", + " ax2.set_title(f\"Camera {camera2_id}\")\n", + " ax2.axis('off')\n", + " \n", + " plt.tight_layout()\n", + " plt.show()\n", + " \n", + " # Save test images\n", + " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n", + " cv2.imwrite(f\"/storage/camera1/dual_test_{timestamp}.jpg\", frame1)\n", + " cv2.imwrite(f\"/storage/camera2/dual_test_{timestamp}.jpg\", frame2)\n", + " print(f\"šŸ’¾ Dual camera test images saved with timestamp {timestamp}\")\n", + " \n", + " else:\n", + " print(\"āŒ Failed to capture from one or both cameras\")\n", + " \n", + " # Test synchronized recording\n", + " print(f\"\\nšŸŽ„ Testing synchronized recording for {duration} seconds...\")\n", + " \n", + " # Setup video writers\n", + " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n", + " \n", + " fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n", + " fps = 30\n", + " \n", + " if ret1:\n", + " h1, w1 = frame1.shape[:2]\n", + " out1 = cv2.VideoWriter(f\"/storage/camera1/sync_test_{timestamp}.mp4\", fourcc, fps, (w1, h1))\n", + " \n", + " if ret2:\n", + " h2, w2 = frame2.shape[:2]\n", + " out2 = cv2.VideoWriter(f\"/storage/camera2/sync_test_{timestamp}.mp4\", fourcc, fps, (w2, h2))\n", + " \n", + " # Record synchronized video\n", + " start_time = time.time()\n", + " frame_count = 0\n", + " \n", + " while time.time() - start_time < duration:\n", + " ret1, frame1 = cap1.read()\n", + " ret2, frame2 = cap2.read()\n", + " \n", + " if ret1 and ret2:\n", + " out1.write(frame1)\n", + " out2.write(frame2)\n", + " frame_count += 1\n", + " else:\n", + " print(f\"āš ļø Frame drop at frame {frame_count}\")\n", + " \n", + " # Cleanup\n", + " cap1.release()\n", + " cap2.release()\n", + " if 'out1' in locals():\n", + " out1.release()\n", + " if 'out2' in locals():\n", + " out2.release()\n", + " \n", + " elapsed = time.time() - start_time\n", + " actual_fps = frame_count / elapsed\n", + " \n", + " print(f\"āœ… Synchronized recording complete\")\n", + " print(f\"šŸ“Š Recorded {frame_count} frames in {elapsed:.2f}s\")\n", + " print(f\"šŸ“Š Actual FPS: {actual_fps:.2f}\")\n", + " print(f\"šŸ’¾ Videos saved with timestamp {timestamp}\")\n", + "\n", + "# Test dual cameras (adjust camera IDs as needed)\n", + "test_dual_cameras(0, 1, duration=3)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "usda-vision-cameras", + "language": "python", + "name": "usda-vision-cameras" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/old tests/main.py b/old tests/main.py new file mode 100644 index 0000000..0184c3e --- /dev/null +++ b/old tests/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from usda-vision-cameras!") + + +if __name__ == "__main__": + main() diff --git a/old tests/mqtt test.ipynb b/old tests/mqtt test.ipynb new file mode 100644 index 0000000..6be4f7d --- /dev/null +++ b/old tests/mqtt test.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "3b92c632", + "metadata": {}, + "outputs": [], + "source": [ + "import paho.mqtt.client as mqtt\n", + "import time\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a6753fb1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_2342/243927247.py:34: DeprecationWarning: Callback API version 1 is deprecated, update to latest version\n", + " client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) # Use VERSION1 for broader compatibility\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connecting to MQTT broker at 192.168.1.110:1883...\n", + "Successfully connected to MQTT Broker!\n", + "Subscribed to topic: 'vision/vibratory_conveyor/state'\n", + "Listening for messages... (Press Ctrl+C to stop)\n", + "\n", + "--- MQTT MESSAGE RECEIVED! ---\n", + " Topic: vision/vibratory_conveyor/state\n", + " Payload: on\n", + " Time: 2025-07-25 21:03:21\n", + "------------------------------\n", + "\n", + "\n", + "--- MQTT MESSAGE RECEIVED! ---\n", + " Topic: vision/vibratory_conveyor/state\n", + " Payload: off\n", + " Time: 2025-07-25 21:05:26\n", + "------------------------------\n", + "\n", + "\n", + "Stopping MQTT listener.\n" + ] + } + ], + "source": [ + "\n", + "# --- MQTT Broker Configuration ---\n", + "# Your Home Assistant's IP address (where your MQTT broker is running)\n", + "MQTT_BROKER_HOST = \"192.168.1.110\"\n", + "MQTT_BROKER_PORT = 1883\n", + "# IMPORTANT: Replace with your actual MQTT broker username and password if you have one set up\n", + "# (These are NOT your Home Assistant login credentials, but for the Mosquitto add-on, if used)\n", + "# MQTT_BROKER_USERNAME = \"pecan\" # e.g., \"homeassistant_mqtt_user\"\n", + "# MQTT_BROKER_PASSWORD = \"whatever\" # e.g., \"SuperSecurePassword123!\"\n", + "\n", + "# --- Topic to Subscribe To ---\n", + "# This MUST exactly match the topic you set in your Home Assistant automation\n", + "MQTT_TOPIC = \"vision/vibratory_conveyor/state\" # <<<< Make sure this is correct!\n", + "MQTT_TOPIC = \"vision/blower_separator/state\" # <<<< Make sure this is correct!\n", + "\n", + "# The callback for when the client receives a CONNACK response from the server.\n", + "def on_connect(client, userdata, flags, rc):\n", + " if rc == 0:\n", + " print(\"Successfully connected to MQTT Broker!\")\n", + " client.subscribe(MQTT_TOPIC)\n", + " print(f\"Subscribed to topic: '{MQTT_TOPIC}'\")\n", + " print(\"Listening for messages... (Press Ctrl+C to stop)\")\n", + " else:\n", + " print(f\"Failed to connect, return code {rc}\\n\")\n", + "\n", + "# The callback for when a PUBLISH message is received from the server.\n", + "def on_message(client, userdata, msg):\n", + " received_payload = msg.payload.decode()\n", + " print(f\"\\n--- MQTT MESSAGE RECEIVED! ---\")\n", + " print(f\" Topic: {msg.topic}\")\n", + " print(f\" Payload: {received_payload}\")\n", + " print(f\" Time: {time.strftime('%Y-%m-%d %H:%M:%S')}\")\n", + " print(f\"------------------------------\\n\")\n", + "\n", + "# Create an MQTT client instance\n", + "client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) # Use VERSION1 for broader compatibility\n", + "\n", + "# Set callback functions\n", + "client.on_connect = on_connect\n", + "client.on_message = on_message\n", + "\n", + "# Set username and password if required\n", + "# (Only uncomment and fill these if your MQTT broker requires authentication)\n", + "# client.username_pw_set(MQTT_BROKER_USERNAME, MQTT_BROKER_PASSWORD)\n", + "\n", + "try:\n", + " # Attempt to connect to the MQTT broker\n", + " print(f\"Connecting to MQTT broker at {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}...\")\n", + " client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60)\n", + "\n", + " # Start the MQTT loop. This runs in the background and processes messages.\n", + " client.loop_forever()\n", + "\n", + "except KeyboardInterrupt:\n", + " print(\"\\nStopping MQTT listener.\")\n", + " client.disconnect() # Disconnect gracefully\n", + "except Exception as e:\n", + " print(f\"An unexpected error occurred: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56531671", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "USDA-vision-cameras", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/test_exposure.py b/old tests/test_exposure.py similarity index 100% rename from test_exposure.py rename to old tests/test_exposure.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c41266 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "usda-vision-cameras" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "imageio>=2.37.0", + "matplotlib>=3.10.3", + "numpy>=2.3.2", + "opencv-python>=4.11.0.86", + "paho-mqtt>=2.1.0", + "pillow>=11.3.0", + "tqdm>=4.67.1", + "fastapi>=0.104.0", + "uvicorn>=0.24.0", + "websockets>=12.0", + "requests>=2.31.0", + "pytz>=2023.3", +] diff --git a/python demo/__pycache__/mvsdk.cpython-311.pyc b/python demo/__pycache__/mvsdk.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c7f50cbdb3087096bb82ef3792986b990e4b22b GIT binary patch literal 144006 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 Ug`_?sKP{aP=%OiMY?}T51A|!`Qvd(} literal 0 HcmV?d00001 diff --git a/setup_timezone.sh b/setup_timezone.sh new file mode 100755 index 0000000..83a5e49 --- /dev/null +++ b/setup_timezone.sh @@ -0,0 +1,289 @@ +#!/bin/bash + +# Time Synchronization Setup for USDA Vision Camera System +# Location: Atlanta, Georgia (Eastern Time Zone) + +echo "šŸ• Setting up time synchronization for Atlanta, Georgia..." +echo "==================================================" + +# Check if running as root +if [ "$EUID" -eq 0 ]; then + echo "Running as root - can make system changes" + CAN_SUDO=true +else + echo "Running as user - will use sudo for system changes" + CAN_SUDO=false +fi + +# Function to run commands with appropriate privileges +run_cmd() { + if [ "$CAN_SUDO" = true ]; then + "$@" + else + sudo "$@" + fi +} + +# 1. Set timezone to Eastern Time (Atlanta, Georgia) +echo "šŸ“ Setting timezone to America/New_York (Eastern Time)..." +if run_cmd timedatectl set-timezone America/New_York; then + echo "āœ… Timezone set successfully" +else + echo "āŒ Failed to set timezone - trying alternative method..." + if run_cmd ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime; then + echo "āœ… Timezone set using alternative method" + else + echo "āŒ Failed to set timezone" + fi +fi + +# 2. Install and configure NTP for time synchronization +echo "" +echo "šŸ”„ Setting up NTP time synchronization..." + +# Check if systemd-timesyncd is available (modern systems) +if systemctl is-available systemd-timesyncd >/dev/null 2>&1; then + echo "Using systemd-timesyncd for time synchronization..." + + # Enable and start systemd-timesyncd + run_cmd systemctl enable systemd-timesyncd + run_cmd systemctl start systemd-timesyncd + + # Configure NTP servers (US-based servers for better accuracy) + echo "Configuring NTP servers..." + cat << EOF | run_cmd tee /etc/systemd/timesyncd.conf +[Time] +NTP=time.nist.gov pool.ntp.org time.google.com +FallbackNTP=time.cloudflare.com time.windows.com +RootDistanceMaxSec=5 +PollIntervalMinSec=32 +PollIntervalMaxSec=2048 +EOF + + # Restart timesyncd to apply new configuration + run_cmd systemctl restart systemd-timesyncd + + echo "āœ… systemd-timesyncd configured and started" + +elif command -v ntpd >/dev/null 2>&1; then + echo "Using ntpd for time synchronization..." + + # Install ntp if not present + if ! command -v ntpd >/dev/null 2>&1; then + echo "Installing ntp package..." + if command -v apt-get >/dev/null 2>&1; then + run_cmd apt-get update && run_cmd apt-get install -y ntp + elif command -v yum >/dev/null 2>&1; then + run_cmd yum install -y ntp + elif command -v dnf >/dev/null 2>&1; then + run_cmd dnf install -y ntp + fi + fi + + # Configure NTP servers + cat << EOF | run_cmd tee /etc/ntp.conf +# NTP configuration for Atlanta, Georgia +driftfile /var/lib/ntp/ntp.drift + +# US-based NTP servers for better accuracy +server time.nist.gov iburst +server pool.ntp.org iburst +server time.google.com iburst +server time.cloudflare.com iburst + +# Fallback servers +server 0.us.pool.ntp.org iburst +server 1.us.pool.ntp.org iburst +server 2.us.pool.ntp.org iburst +server 3.us.pool.ntp.org iburst + +# Security settings +restrict default kod notrap nomodify nopeer noquery +restrict -6 default kod notrap nomodify nopeer noquery +restrict 127.0.0.1 +restrict -6 ::1 + +# Local clock as fallback +server 127.127.1.0 +fudge 127.127.1.0 stratum 10 +EOF + + # Enable and start NTP service + run_cmd systemctl enable ntp + run_cmd systemctl start ntp + + echo "āœ… NTP configured and started" + +else + echo "āš ļø No NTP service found - installing chrony as alternative..." + + # Install chrony + if command -v apt-get >/dev/null 2>&1; then + run_cmd apt-get update && run_cmd apt-get install -y chrony + elif command -v yum >/dev/null 2>&1; then + run_cmd yum install -y chrony + elif command -v dnf >/dev/null 2>&1; then + run_cmd dnf install -y chrony + fi + + # Configure chrony + cat << EOF | run_cmd tee /etc/chrony/chrony.conf +# Chrony configuration for Atlanta, Georgia +server time.nist.gov iburst +server pool.ntp.org iburst +server time.google.com iburst +server time.cloudflare.com iburst + +# US pool servers +pool us.pool.ntp.org iburst + +driftfile /var/lib/chrony/drift +makestep 1.0 3 +rtcsync +EOF + + # Enable and start chrony + run_cmd systemctl enable chrony + run_cmd systemctl start chrony + + echo "āœ… Chrony configured and started" +fi + +# 3. Force immediate time synchronization +echo "" +echo "ā° Forcing immediate time synchronization..." + +if systemctl is-active systemd-timesyncd >/dev/null 2>&1; then + run_cmd systemctl restart systemd-timesyncd + sleep 2 + run_cmd timedatectl set-ntp true +elif systemctl is-active ntp >/dev/null 2>&1; then + run_cmd ntpdate -s time.nist.gov + run_cmd systemctl restart ntp +elif systemctl is-active chrony >/dev/null 2>&1; then + run_cmd chrony sources -v + run_cmd chronyc makestep +fi + +# 4. Configure hardware clock +echo "" +echo "šŸ”§ Configuring hardware clock..." +if run_cmd hwclock --systohc; then + echo "āœ… Hardware clock synchronized with system clock" +else + echo "āš ļø Could not sync hardware clock (this may be normal in containers)" +fi + +# 5. Display current time information +echo "" +echo "šŸ“Š Current Time Information:" +echo "================================" +echo "System time: $(date)" +echo "UTC time: $(date -u)" +echo "Timezone: $(timedatectl show --property=Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo 'Unknown')" + +# Check if timedatectl is available +if command -v timedatectl >/dev/null 2>&1; then + echo "" + echo "Time synchronization status:" + timedatectl status +fi + +# 6. Create a time check script for the vision system +echo "" +echo "šŸ“ Creating time verification script..." +cat << 'EOF' > check_time.py +#!/usr/bin/env python3 +""" +Time verification script for USDA Vision Camera System +Checks if system time is properly synchronized +""" + +import datetime +import pytz +import requests +import json + +def check_system_time(): + """Check system time against multiple sources""" + print("šŸ• USDA Vision Camera System - Time Verification") + print("=" * 50) + + # Get local time + local_time = datetime.datetime.now() + utc_time = datetime.datetime.utcnow() + + # Get Atlanta timezone + atlanta_tz = pytz.timezone('America/New_York') + atlanta_time = datetime.datetime.now(atlanta_tz) + + print(f"Local system time: {local_time}") + print(f"UTC time: {utc_time}") + print(f"Atlanta time: {atlanta_time}") + print(f"Timezone: {atlanta_time.tzname()}") + + # Check against world time API + try: + print("\n🌐 Checking against world time API...") + response = requests.get("http://worldtimeapi.org/api/timezone/America/New_York", timeout=5) + if response.status_code == 200: + data = response.json() + api_time = datetime.datetime.fromisoformat(data['datetime'].replace('Z', '+00:00')) + + # Compare times (allow 5 second difference) + time_diff = abs((atlanta_time.replace(tzinfo=None) - api_time.replace(tzinfo=None)).total_seconds()) + + print(f"API time: {api_time}") + print(f"Time difference: {time_diff:.2f} seconds") + + if time_diff < 5: + print("āœ… Time is synchronized (within 5 seconds)") + return True + else: + print("āŒ Time is NOT synchronized (difference > 5 seconds)") + return False + else: + print("āš ļø Could not reach time API") + return None + except Exception as e: + print(f"āš ļø Error checking time API: {e}") + return None + +if __name__ == "__main__": + check_system_time() +EOF + +chmod +x check_time.py + +echo "āœ… Time verification script created: check_time.py" + +# 7. Add time sync check to the vision system startup +echo "" +echo "šŸ”— Integrating time sync with vision system..." + +# Update the startup script to include time check +if [ -f "start_system.sh" ]; then + # Create backup + cp start_system.sh start_system.sh.backup + + # Add time sync check to startup script + sed -i '/# Run system tests first/i\ +# Check time synchronization\ +echo "šŸ• Checking time synchronization..."\ +python check_time.py\ +echo ""' start_system.sh + + echo "āœ… Updated start_system.sh to include time verification" +fi + +echo "" +echo "šŸŽ‰ Time synchronization setup complete!" +echo "" +echo "Summary:" +echo "- Timezone set to America/New_York (Eastern Time)" +echo "- NTP synchronization configured and enabled" +echo "- Time verification script created (check_time.py)" +echo "- Startup script updated to check time sync" +echo "" +echo "To verify time sync manually, run: python check_time.py" +echo "Current time: $(date)" diff --git a/start_system.sh b/start_system.sh new file mode 100755 index 0000000..b1c3f25 --- /dev/null +++ b/start_system.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# USDA Vision Camera System Startup Script + +echo "USDA Vision Camera System - Startup Script" +echo "==========================================" + +# Check if virtual environment exists +if [ ! -d ".venv" ]; then + echo "āŒ Virtual environment not found. Please run 'uv sync' first." + exit 1 +fi + +# Activate virtual environment +echo "šŸ”§ Activating virtual environment..." +source .venv/bin/activate + +# Check if config file exists +if [ ! -f "config.json" ]; then + echo "āš ļø Config file not found. Using default configuration." +fi + +# Check storage directory +if [ ! -d "/storage" ]; then + echo "šŸ“ Creating storage directory..." + sudo mkdir -p /storage + sudo chown $USER:$USER /storage + echo "āœ… Storage directory created at /storage" +fi + +# Check time synchronization +echo "šŸ• Checking time synchronization..." +python check_time.py +echo "" +# Run system tests first +echo "🧪 Running system tests..." +python test_system.py + +if [ $? -ne 0 ]; then + echo "āŒ System tests failed. Please check the configuration." + read -p "Do you want to continue anyway? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +echo "" +echo "šŸš€ Starting USDA Vision Camera System..." +echo " - MQTT monitoring will begin automatically" +echo " - Camera recording will start when machines turn on" +echo " - API server will be available at http://localhost:8000" +echo " - Press Ctrl+C to stop the system" +echo "" + +# Start the system +python main.py "$@" + +echo "šŸ‘‹ USDA Vision Camera System stopped." diff --git a/start_system.sh.backup b/start_system.sh.backup new file mode 100755 index 0000000..12f9deb --- /dev/null +++ b/start_system.sh.backup @@ -0,0 +1,55 @@ +#!/bin/bash + +# USDA Vision Camera System Startup Script + +echo "USDA Vision Camera System - Startup Script" +echo "==========================================" + +# Check if virtual environment exists +if [ ! -d ".venv" ]; then + echo "āŒ Virtual environment not found. Please run 'uv sync' first." + exit 1 +fi + +# Activate virtual environment +echo "šŸ”§ Activating virtual environment..." +source .venv/bin/activate + +# Check if config file exists +if [ ! -f "config.json" ]; then + echo "āš ļø Config file not found. Using default configuration." +fi + +# Check storage directory +if [ ! -d "/storage" ]; then + echo "šŸ“ Creating storage directory..." + sudo mkdir -p /storage + sudo chown $USER:$USER /storage + echo "āœ… Storage directory created at /storage" +fi + +# Run system tests first +echo "🧪 Running system tests..." +python test_system.py + +if [ $? -ne 0 ]; then + echo "āŒ System tests failed. Please check the configuration." + read -p "Do you want to continue anyway? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +echo "" +echo "šŸš€ Starting USDA Vision Camera System..." +echo " - MQTT monitoring will begin automatically" +echo " - Camera recording will start when machines turn on" +echo " - API server will be available at http://localhost:8000" +echo " - Press Ctrl+C to stop the system" +echo "" + +# Start the system +python main.py "$@" + +echo "šŸ‘‹ USDA Vision Camera System stopped." diff --git a/test_system.py b/test_system.py new file mode 100644 index 0000000..5cdcf92 --- /dev/null +++ b/test_system.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +Test script for the USDA Vision Camera System. + +This script performs basic tests to verify system components are working correctly. +""" + +import sys +import os +import time +import json +import requests +from datetime import datetime + +# Add the current directory to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +def test_imports(): + """Test that all modules can be imported""" + print("Testing imports...") + try: + from usda_vision_system.core.config import Config + from usda_vision_system.core.state_manager import StateManager + from usda_vision_system.core.events import EventSystem + from usda_vision_system.mqtt.client import MQTTClient + from usda_vision_system.camera.manager import CameraManager + from usda_vision_system.storage.manager import StorageManager + from usda_vision_system.api.server import APIServer + from usda_vision_system.main import USDAVisionSystem + print("āœ… All imports successful") + return True + except Exception as e: + print(f"āŒ Import failed: {e}") + return False + +def test_configuration(): + """Test configuration loading""" + print("\nTesting configuration...") + try: + from usda_vision_system.core.config import Config + + # Test default config + config = Config() + print(f"āœ… Default config loaded") + print(f" MQTT broker: {config.mqtt.broker_host}:{config.mqtt.broker_port}") + print(f" Storage path: {config.storage.base_path}") + print(f" Cameras configured: {len(config.cameras)}") + + # Test config file if it exists + if os.path.exists("config.json"): + config_file = Config("config.json") + print(f"āœ… Config file loaded") + + return True + except Exception as e: + print(f"āŒ Configuration test failed: {e}") + return False + +def test_camera_discovery(): + """Test camera discovery""" + print("\nTesting camera discovery...") + try: + sys.path.append('./python demo') + import mvsdk + + devices = mvsdk.CameraEnumerateDevice() + print(f"āœ… Camera discovery successful") + print(f" Found {len(devices)} camera(s)") + + for i, device in enumerate(devices): + try: + name = device.GetFriendlyName() + port_type = device.GetPortType() + print(f" Camera {i}: {name} ({port_type})") + except Exception as e: + print(f" Camera {i}: Error getting info - {e}") + + return True + except Exception as e: + print(f"āŒ Camera discovery failed: {e}") + print(" Make sure GigE cameras are connected and python demo library is available") + return False + +def test_storage_setup(): + """Test storage directory setup""" + print("\nTesting storage setup...") + try: + from usda_vision_system.core.config import Config + from usda_vision_system.storage.manager import StorageManager + from usda_vision_system.core.state_manager import StateManager + + config = Config() + state_manager = StateManager() + storage_manager = StorageManager(config, state_manager) + + # Test storage statistics + stats = storage_manager.get_storage_statistics() + print(f"āœ… Storage manager initialized") + print(f" Base path: {stats.get('base_path', 'Unknown')}") + print(f" Total files: {stats.get('total_files', 0)}") + + return True + except Exception as e: + print(f"āŒ Storage setup failed: {e}") + return False + +def test_mqtt_config(): + """Test MQTT configuration (without connecting)""" + print("\nTesting MQTT configuration...") + try: + from usda_vision_system.core.config import Config + from usda_vision_system.mqtt.client import MQTTClient + from usda_vision_system.core.state_manager import StateManager + from usda_vision_system.core.events import EventSystem + + config = Config() + state_manager = StateManager() + event_system = EventSystem() + + mqtt_client = MQTTClient(config, state_manager, event_system) + status = mqtt_client.get_status() + + print(f"āœ… MQTT client initialized") + print(f" Broker: {status['broker_host']}:{status['broker_port']}") + print(f" Topics: {len(status['subscribed_topics'])}") + for topic in status['subscribed_topics']: + print(f" - {topic}") + + return True + except Exception as e: + print(f"āŒ MQTT configuration test failed: {e}") + return False + +def test_system_initialization(): + """Test full system initialization (without starting)""" + print("\nTesting system initialization...") + try: + from usda_vision_system.main import USDAVisionSystem + + # Create system instance + system = USDAVisionSystem() + + # Check system status + status = system.get_system_status() + print(f"āœ… System initialized successfully") + print(f" Running: {status['running']}") + print(f" Components initialized: {len(status['components'])}") + + return True + except Exception as e: + print(f"āŒ System initialization failed: {e}") + return False + +def test_api_endpoints(): + """Test API endpoints if server is running""" + print("\nTesting API endpoints...") + try: + # Test health endpoint + response = requests.get("http://localhost:8000/health", timeout=5) + if response.status_code == 200: + print("āœ… API server is running") + + # Test system status endpoint + try: + response = requests.get("http://localhost:8000/system/status", timeout=5) + if response.status_code == 200: + data = response.json() + print(f" System started: {data.get('system_started', False)}") + print(f" MQTT connected: {data.get('mqtt_connected', False)}") + print(f" Active recordings: {data.get('active_recordings', 0)}") + else: + print(f"āš ļø System status endpoint returned {response.status_code}") + except Exception as e: + print(f"āš ļø System status test failed: {e}") + + return True + else: + print(f"āš ļø API server returned status {response.status_code}") + return False + except requests.exceptions.ConnectionError: + print("āš ļø API server not running (this is OK if system is not started)") + return True + except Exception as e: + print(f"āŒ API test failed: {e}") + return False + +def main(): + """Run all tests""" + print("USDA Vision Camera System - Test Suite") + print("=" * 50) + + tests = [ + test_imports, + test_configuration, + test_camera_discovery, + test_storage_setup, + test_mqtt_config, + test_system_initialization, + test_api_endpoints + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"āŒ Test {test.__name__} crashed: {e}") + + print("\n" + "=" * 50) + print(f"Test Results: {passed}/{total} tests passed") + + if passed == total: + print("šŸŽ‰ All tests passed! System appears to be working correctly.") + return 0 + else: + print("āš ļø Some tests failed. Check the output above for details.") + return 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test_timezone.py b/test_timezone.py new file mode 100644 index 0000000..3da1eca --- /dev/null +++ b/test_timezone.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +""" +Test timezone functionality for the USDA Vision Camera System. +""" + +import sys +import os + +# Add the current directory to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from usda_vision_system.core.timezone_utils import ( + now_atlanta, format_atlanta_timestamp, format_filename_timestamp, + check_time_sync, log_time_info +) +import logging + +def test_timezone_functions(): + """Test timezone utility functions""" + print("šŸ• Testing USDA Vision Camera System Timezone Functions") + print("=" * 60) + + # Test current time functions + atlanta_time = now_atlanta() + print(f"Current Atlanta time: {atlanta_time}") + print(f"Timezone: {atlanta_time.tzname()}") + print(f"UTC offset: {atlanta_time.strftime('%z')}") + + # Test timestamp formatting + timestamp_str = format_atlanta_timestamp() + filename_str = format_filename_timestamp() + + print(f"\nTimestamp formats:") + print(f" Display format: {timestamp_str}") + print(f" Filename format: {filename_str}") + + # Test time sync + print(f"\nšŸ”„ Testing time synchronization...") + sync_info = check_time_sync() + print(f"Sync status: {sync_info['sync_status']}") + if sync_info.get('time_diff_seconds') is not None: + print(f"Time difference: {sync_info['time_diff_seconds']:.2f} seconds") + + # Test logging + print(f"\nšŸ“ Testing time logging...") + logging.basicConfig(level=logging.INFO) + log_time_info() + + print(f"\nāœ… All timezone tests completed successfully!") + + # Show example filename that would be generated + example_filename = f"camera1_recording_{filename_str}.avi" + print(f"\nExample recording filename: {example_filename}") + +if __name__ == "__main__": + test_timezone_functions() diff --git a/usda_vision_system/__init__.py b/usda_vision_system/__init__.py new file mode 100644 index 0000000..4000f9f --- /dev/null +++ b/usda_vision_system/__init__.py @@ -0,0 +1,13 @@ +""" +USDA Vision Camera System + +A comprehensive system for monitoring machines via MQTT and automatically recording +video from GigE cameras when machines are active. +""" + +__version__ = "1.0.0" +__author__ = "USDA Vision Team" + +from .main import USDAVisionSystem + +__all__ = ["USDAVisionSystem"] diff --git a/usda_vision_system/__pycache__/__init__.cpython-311.pyc b/usda_vision_system/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a49762c2ce6503dec396b50f1acb414e9081195 GIT binary patch literal 515 zcmY*WF-rq66i(W!mDH+}oBEJ09oo}T5V5E@DM+isEg@XeUf^<(-1VfLI=DK!yNLg$ zqjVCS+zM_^CRas$36J;k9`F0!m#2E2AZz!nyQqN?dUMH-mA-?Ih7BGNM*-qEz`PWc zc$s@~S$Tnn5w8qzP~l5m)U1vl$yN8H1J^Q@I)P)RgkjJfrI}EKbRg8~#)wEHsmukW z#XI!10i_d}X(N*XC>BOC5h>)7!TDvc2Q1;hhM86@lOc=a5f~9_!)-UoB^Mg{Mk_dz zgHs3#$P{iZ$nPj-1h6o(plvd)?6r5>yW{o0OnZWOPT$)uW z^cj6PAwQ4&NGsv9SQ;^AzC&;2g-)$+ z(*7{zj4s~jLL8q`X_cY0eKY!8K2VwuV{vF!>_Eph-M4OD#u(2_73|HCbE{}=V^Vvm YZ%@c8+L`{pueHr7*_u?{G4Aib0CBmTEC2ui literal 0 HcmV?d00001 diff --git a/usda_vision_system/__pycache__/main.cpython-311.pyc b/usda_vision_system/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..188e82d03a15d1cb0e306e70bc702645e56f159d GIT binary patch literal 15384 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 literal 0 HcmV?d00001 diff --git a/usda_vision_system/api/__init__.py b/usda_vision_system/api/__init__.py new file mode 100644 index 0000000..5f1952b --- /dev/null +++ b/usda_vision_system/api/__init__.py @@ -0,0 +1,10 @@ +""" +API module for the USDA Vision Camera System. + +This module provides REST API endpoints and WebSocket support for dashboard integration. +""" + +from .server import APIServer +from .models import * + +__all__ = ["APIServer"] diff --git a/usda_vision_system/api/__pycache__/__init__.cpython-311.pyc b/usda_vision_system/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..feabf7044b5825c0c5a8b3319a3ccc84b679a33d GIT binary patch literal 453 zcmYjOy-ve05Vn&vh0u<$fjqDjDb0=$Qbie95F!PnE|y!H#=?msJFV1C3=HrDJObiv zI-(2+iHWUJw@%n8l{lZy_ouu2?%tZs02uPar{vT8-`3cW^QSBijq(a8=mQF=BVg+E z9cXn|xII9<5$t=k7K4^QdkH#M7bul_tNJh~e8^m(>zzU?tc`j8^b)ZBi14$G`CXtO4 zDNLVs@B_`%Em&@wIx$ldri=lPlxy6Vty)>fn93o>rLU`K6)1f(IuqJPi4YjSgQeZV zsv9LTWf2ivu^EXhqHt0n3WpUU9gQ_5c1yj_y_r+hFh%Wv o`q+qt3n5%M9`qK#iUxoObFlxPKfvyM!(Tgl&*5X;K4q`>3qjI^i2wiq literal 0 HcmV?d00001 diff --git a/usda_vision_system/api/__pycache__/models.cpython-311.pyc b/usda_vision_system/api/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9939a95b1e357668e55db4cc56eb2b4b237c91f4 GIT binary patch literal 9244 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 L;TzsK`MduCGv+d{ literal 0 HcmV?d00001 diff --git a/usda_vision_system/api/__pycache__/server.cpython-311.pyc b/usda_vision_system/api/__pycache__/server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8312c41dbbb27a3ecfc983e0c10fca7ae0bd5ff GIT binary patch literal 26378 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 bool: + """Start the API server""" + if self.running: + self.logger.warning("API server is already running") + return True + + if not self.config.system.enable_api: + self.logger.info("API server disabled in configuration") + return False + + try: + self.logger.info(f"Starting API server on {self.config.system.api_host}:{self.config.system.api_port}") + self.running = True + + # Start server in separate thread + self._server_thread = threading.Thread(target=self._run_server, daemon=True) + self._server_thread.start() + + return True + + except Exception as e: + self.logger.error(f"Error starting API server: {e}") + return False + + def stop(self) -> None: + """Stop the API server""" + if not self.running: + return + + self.logger.info("Stopping API server...") + self.running = False + + # Note: uvicorn doesn't have a clean way to stop from another thread + # In production, you might want to use a process manager like gunicorn + + self.logger.info("API server stopped") + + def _run_server(self) -> None: + """Run the uvicorn server""" + try: + uvicorn.run( + self.app, + host=self.config.system.api_host, + port=self.config.system.api_port, + log_level="info" + ) + except Exception as e: + self.logger.error(f"Error running API server: {e}") + finally: + self.running = False + + def is_running(self) -> bool: + """Check if API server is running""" + return self.running + + def get_server_info(self) -> Dict[str, Any]: + """Get server information""" + return { + "running": self.running, + "host": self.config.system.api_host, + "port": self.config.system.api_port, + "start_time": self.server_start_time.isoformat(), + "uptime_seconds": (datetime.now() - self.server_start_time).total_seconds(), + "websocket_connections": len(self.websocket_manager.active_connections) + } diff --git a/usda_vision_system/camera/__init__.py b/usda_vision_system/camera/__init__.py new file mode 100644 index 0000000..205313d --- /dev/null +++ b/usda_vision_system/camera/__init__.py @@ -0,0 +1,12 @@ +""" +Camera module for the USDA Vision Camera System. + +This module handles GigE camera discovery, management, monitoring, and recording +using the python demo library (mvsdk). +""" + +from .manager import CameraManager +from .recorder import CameraRecorder +from .monitor import CameraMonitor + +__all__ = ["CameraManager", "CameraRecorder", "CameraMonitor"] diff --git a/usda_vision_system/camera/__pycache__/__init__.cpython-311.pyc b/usda_vision_system/camera/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..372f908080b73bb7e6f3876da8fc56111201e478 GIT binary patch literal 604 zcmY*Vv2NQi5G5tqGKAXSKr|gHAhEViL4cqwGNnV^X3$WAFT&AAi}Mpof#HFZ{L(O xfUf^fcLuOcF~-|ufU_;~FhO{4jb1*DJM?P(Jno(ofc(2ST8FsG{byf*^nYh*w8j7c literal 0 HcmV?d00001 diff --git a/usda_vision_system/camera/__pycache__/manager.cpython-311.pyc b/usda_vision_system/camera/__pycache__/manager.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdef056a103a2a22a1dae6c7bd722728980837b1 GIT binary patch literal 16691 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 literal 0 HcmV?d00001 diff --git a/usda_vision_system/camera/__pycache__/monitor.cpython-311.pyc b/usda_vision_system/camera/__pycache__/monitor.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66df48176df7d27fafd513f53283dcd80bfb18e7 GIT binary patch literal 13505 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 literal 0 HcmV?d00001 diff --git a/usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc b/usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64c662962fd35ef4a42a9464e1ff837b09449366 GIT binary patch literal 20089 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 hVN&uB-5F*?&UbGYTyfa|`|^k1ko@m recorder + self.camera_monitor: Optional[CameraMonitor] = None + + # Threading + self._lock = threading.RLock() + self.running = False + + # Subscribe to machine state changes + self.event_system.subscribe(EventType.MACHINE_STATE_CHANGED, self._on_machine_state_changed) + + # Initialize camera discovery + self._discover_cameras() + + # Create camera monitor + self.camera_monitor = CameraMonitor( + config=config, + state_manager=state_manager, + event_system=event_system, + camera_manager=self + ) + + def start(self) -> bool: + """Start the camera manager""" + if self.running: + self.logger.warning("Camera manager is already running") + return True + + self.logger.info("Starting camera manager...") + self.running = True + + # Start camera monitor + if self.camera_monitor: + self.camera_monitor.start() + + # Initialize camera recorders + self._initialize_recorders() + + self.logger.info("Camera manager started successfully") + return True + + def stop(self) -> None: + """Stop the camera manager""" + if not self.running: + return + + self.logger.info("Stopping camera manager...") + self.running = False + + # Stop camera monitor + if self.camera_monitor: + self.camera_monitor.stop() + + # Stop all active recordings + with self._lock: + for recorder in self.camera_recorders.values(): + if recorder.is_recording(): + recorder.stop_recording() + recorder.cleanup() + + self.logger.info("Camera manager stopped") + + def _discover_cameras(self) -> None: + """Discover available GigE cameras""" + try: + self.logger.info("Discovering GigE cameras...") + + # Enumerate cameras using mvsdk + device_list = mvsdk.CameraEnumerateDevice() + self.available_cameras = device_list + + self.logger.info(f"Found {len(device_list)} camera(s)") + + for i, dev_info in enumerate(device_list): + try: + name = dev_info.GetFriendlyName() + port_type = dev_info.GetPortType() + serial = getattr(dev_info, 'acSn', 'Unknown') + + self.logger.info(f" Camera {i}: {name} ({port_type}) - Serial: {serial}") + + # Update state manager with discovered camera + camera_name = f"camera{i+1}" # Default naming + self.state_manager.update_camera_status( + name=camera_name, + status="available", + device_info={ + "friendly_name": name, + "port_type": port_type, + "serial_number": serial, + "device_index": i + } + ) + + except Exception as e: + self.logger.error(f"Error processing camera {i}: {e}") + + except Exception as e: + self.logger.error(f"Error discovering cameras: {e}") + self.available_cameras = [] + + def _initialize_recorders(self) -> None: + """Initialize camera recorders for configured cameras""" + with self._lock: + for camera_config in self.config.cameras: + if not camera_config.enabled: + continue + + try: + # Find matching physical camera + device_info = self._find_camera_device(camera_config.name) + if device_info is None: + self.logger.warning(f"No physical camera found for configured camera: {camera_config.name}") + continue + + # Create recorder + recorder = CameraRecorder( + camera_config=camera_config, + device_info=device_info, + state_manager=self.state_manager, + event_system=self.event_system + ) + + self.camera_recorders[camera_config.name] = recorder + self.logger.info(f"Initialized recorder for camera: {camera_config.name}") + + except Exception as e: + self.logger.error(f"Error initializing recorder for {camera_config.name}: {e}") + + def _find_camera_device(self, camera_name: str) -> Optional[Any]: + """Find physical camera device for a configured camera""" + # For now, use simple mapping: camera1 -> device 0, camera2 -> device 1, etc. + # This could be enhanced to use serial numbers or other identifiers + + camera_index_map = { + "camera1": 0, + "camera2": 1, + "camera3": 2, + "camera4": 3 + } + + device_index = camera_index_map.get(camera_name) + if device_index is not None and device_index < len(self.available_cameras): + return self.available_cameras[device_index] + + return None + + def _on_machine_state_changed(self, event: Event) -> None: + """Handle machine state change events""" + try: + machine_name = event.data.get("machine_name") + new_state = event.data.get("state") + + if not machine_name or not new_state: + return + + self.logger.info(f"Handling machine state change: {machine_name} -> {new_state}") + + # Find camera associated with this machine + camera_config = None + for config in self.config.cameras: + if config.machine_topic == machine_name: + camera_config = config + break + + if not camera_config: + self.logger.warning(f"No camera configured for machine: {machine_name}") + return + + # Get the recorder for this camera + recorder = self.camera_recorders.get(camera_config.name) + if not recorder: + self.logger.warning(f"No recorder found for camera: {camera_config.name}") + return + + # Handle state change + if new_state == "on": + self._start_recording(camera_config.name, recorder) + elif new_state in ["off", "error"]: + self._stop_recording(camera_config.name, recorder) + + except Exception as e: + self.logger.error(f"Error handling machine state change: {e}") + + def _start_recording(self, camera_name: str, recorder: CameraRecorder) -> None: + """Start recording for a camera""" + try: + if recorder.is_recording(): + self.logger.info(f"Camera {camera_name} is already recording") + return + + # Generate filename with Atlanta timezone timestamp + timestamp = format_filename_timestamp() + filename = f"{camera_name}_recording_{timestamp}.avi" + + # Start recording + success = recorder.start_recording(filename) + if success: + self.logger.info(f"Started recording for camera {camera_name}: {filename}") + else: + self.logger.error(f"Failed to start recording for camera {camera_name}") + + except Exception as e: + self.logger.error(f"Error starting recording for {camera_name}: {e}") + + def _stop_recording(self, camera_name: str, recorder: CameraRecorder) -> None: + """Stop recording for a camera""" + try: + if not recorder.is_recording(): + self.logger.info(f"Camera {camera_name} is not recording") + return + + # Stop recording + success = recorder.stop_recording() + if success: + self.logger.info(f"Stopped recording for camera {camera_name}") + else: + self.logger.error(f"Failed to stop recording for camera {camera_name}") + + except Exception as e: + self.logger.error(f"Error stopping recording for {camera_name}: {e}") + + def get_camera_status(self, camera_name: str) -> Optional[Dict[str, Any]]: + """Get status of a specific camera""" + recorder = self.camera_recorders.get(camera_name) + if not recorder: + return None + + return recorder.get_status() + + def get_all_camera_status(self) -> Dict[str, Dict[str, Any]]: + """Get status of all cameras""" + status = {} + with self._lock: + for camera_name, recorder in self.camera_recorders.items(): + status[camera_name] = recorder.get_status() + return status + + def manual_start_recording(self, camera_name: str, filename: Optional[str] = None) -> bool: + """Manually start recording for a camera""" + recorder = self.camera_recorders.get(camera_name) + if not recorder: + self.logger.error(f"Camera not found: {camera_name}") + return False + + if not filename: + timestamp = format_filename_timestamp() + filename = f"{camera_name}_manual_{timestamp}.avi" + + return recorder.start_recording(filename) + + def manual_stop_recording(self, camera_name: str) -> bool: + """Manually stop recording for a camera""" + recorder = self.camera_recorders.get(camera_name) + if not recorder: + self.logger.error(f"Camera not found: {camera_name}") + return False + + return recorder.stop_recording() + + def get_available_cameras(self) -> List[Dict[str, Any]]: + """Get list of available physical cameras""" + cameras = [] + for i, dev_info in enumerate(self.available_cameras): + try: + cameras.append({ + "index": i, + "name": dev_info.GetFriendlyName(), + "port_type": dev_info.GetPortType(), + "serial_number": getattr(dev_info, 'acSn', 'Unknown') + }) + except Exception as e: + self.logger.error(f"Error getting info for camera {i}: {e}") + + return cameras + + def refresh_camera_discovery(self) -> int: + """Refresh camera discovery and return number of cameras found""" + self._discover_cameras() + return len(self.available_cameras) + + def is_running(self) -> bool: + """Check if camera manager is running""" + return self.running diff --git a/usda_vision_system/camera/monitor.py b/usda_vision_system/camera/monitor.py new file mode 100644 index 0000000..e4b5515 --- /dev/null +++ b/usda_vision_system/camera/monitor.py @@ -0,0 +1,267 @@ +""" +Camera Monitor for the USDA Vision Camera System. + +This module monitors camera status and availability at regular intervals. +""" + +import sys +import os +import threading +import time +import logging +from typing import Dict, List, Optional, Any + +# Add python demo to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo')) +import mvsdk + +from ..core.config import Config +from ..core.state_manager import StateManager, CameraStatus +from ..core.events import EventSystem, publish_camera_status_changed + + +class CameraMonitor: + """Monitors camera status and availability""" + + def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager=None): + self.config = config + self.state_manager = state_manager + self.event_system = event_system + self.camera_manager = camera_manager # Reference to camera manager + self.logger = logging.getLogger(__name__) + + # Monitoring settings + self.check_interval = config.system.camera_check_interval_seconds + + # Threading + self.running = False + self._thread: Optional[threading.Thread] = None + self._stop_event = threading.Event() + + # Status tracking + self.last_check_time: Optional[float] = None + self.check_count = 0 + self.error_count = 0 + + def start(self) -> bool: + """Start camera monitoring""" + if self.running: + self.logger.warning("Camera monitor is already running") + return True + + self.logger.info(f"Starting camera monitor (check interval: {self.check_interval}s)") + self.running = True + self._stop_event.clear() + + # Start monitoring thread + self._thread = threading.Thread(target=self._monitoring_loop, daemon=True) + self._thread.start() + + return True + + def stop(self) -> None: + """Stop camera monitoring""" + if not self.running: + return + + self.logger.info("Stopping camera monitor...") + self.running = False + self._stop_event.set() + + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=5) + + self.logger.info("Camera monitor stopped") + + def _monitoring_loop(self) -> None: + """Main monitoring loop""" + self.logger.info("Camera monitoring loop started") + + while self.running and not self._stop_event.is_set(): + try: + self.last_check_time = time.time() + self.check_count += 1 + + # Check all configured cameras + self._check_all_cameras() + + # Wait for next check + if self._stop_event.wait(self.check_interval): + break + + except Exception as e: + self.error_count += 1 + self.logger.error(f"Error in camera monitoring loop: {e}") + + # Wait a bit before retrying + if self._stop_event.wait(min(self.check_interval, 10)): + break + + self.logger.info("Camera monitoring loop ended") + + def _check_all_cameras(self) -> None: + """Check status of all configured cameras""" + for camera_config in self.config.cameras: + if not camera_config.enabled: + continue + + try: + self._check_camera_status(camera_config.name) + except Exception as e: + self.logger.error(f"Error checking camera {camera_config.name}: {e}") + + def _check_camera_status(self, camera_name: str) -> None: + """Check status of a specific camera""" + try: + # Get current status from state manager + current_info = self.state_manager.get_camera_status(camera_name) + + # Perform actual camera check + status, details, device_info = self._perform_camera_check(camera_name) + + # Update state if changed + old_status = current_info.status.value if current_info else "unknown" + if old_status != status: + self.state_manager.update_camera_status( + name=camera_name, + status=status, + error=details if status == "error" else None, + device_info=device_info + ) + + # Publish status change event + publish_camera_status_changed( + camera_name=camera_name, + status=status, + details=details + ) + + self.logger.info(f"Camera {camera_name} status changed: {old_status} -> {status}") + + except Exception as e: + self.logger.error(f"Error checking camera {camera_name}: {e}") + + # Update to error state + self.state_manager.update_camera_status( + name=camera_name, + status="error", + error=str(e) + ) + + def _perform_camera_check(self, camera_name: str) -> tuple[str, str, Optional[Dict[str, Any]]]: + """Perform actual camera availability check""" + try: + # Get camera device info from camera manager + if not self.camera_manager: + return "error", "Camera manager not available", None + + device_info = self.camera_manager._find_camera_device(camera_name) + if not device_info: + return "disconnected", "Camera device not found", None + + # Check if camera is already opened by another process + if mvsdk.CameraIsOpened(device_info): + # Camera is opened - check if it's our recorder + recorder = self.camera_manager.camera_recorders.get(camera_name) + if recorder and recorder.hCamera: + return "available", "Camera initialized and ready", self._get_device_info_dict(device_info) + else: + return "busy", "Camera opened by another process", self._get_device_info_dict(device_info) + + # Try to initialize camera briefly to test availability + try: + hCamera = mvsdk.CameraInit(device_info, -1, -1) + + # Quick test - try to get one frame + try: + mvsdk.CameraSetTriggerMode(hCamera, 0) + mvsdk.CameraPlay(hCamera) + + # Try to capture with short timeout + pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, 500) + mvsdk.CameraReleaseImageBuffer(hCamera, pRawData) + + # Success - camera is available + mvsdk.CameraUnInit(hCamera) + return "available", "Camera test successful", self._get_device_info_dict(device_info) + + except mvsdk.CameraException as e: + mvsdk.CameraUnInit(hCamera) + if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT: + return "available", "Camera available but slow response", self._get_device_info_dict(device_info) + else: + return "error", f"Camera test failed: {e.message}", self._get_device_info_dict(device_info) + + except mvsdk.CameraException as e: + return "error", f"Camera initialization failed: {e.message}", self._get_device_info_dict(device_info) + + except Exception as e: + return "error", f"Camera check failed: {str(e)}", None + + def _get_device_info_dict(self, device_info) -> Dict[str, Any]: + """Convert device info to dictionary""" + try: + return { + "friendly_name": device_info.GetFriendlyName(), + "port_type": device_info.GetPortType(), + "serial_number": getattr(device_info, 'acSn', 'Unknown'), + "last_checked": time.time() + } + except Exception as e: + self.logger.error(f"Error getting device info: {e}") + return {"error": str(e)} + + def check_camera_now(self, camera_name: str) -> Dict[str, Any]: + """Manually check a specific camera status""" + try: + status, details, device_info = self._perform_camera_check(camera_name) + + # Update state + self.state_manager.update_camera_status( + name=camera_name, + status=status, + error=details if status == "error" else None, + device_info=device_info + ) + + return { + "camera_name": camera_name, + "status": status, + "details": details, + "device_info": device_info, + "check_time": time.time() + } + + except Exception as e: + error_msg = f"Manual camera check failed: {e}" + self.logger.error(error_msg) + return { + "camera_name": camera_name, + "status": "error", + "details": error_msg, + "device_info": None, + "check_time": time.time() + } + + def check_all_cameras_now(self) -> Dict[str, Dict[str, Any]]: + """Manually check all cameras""" + results = {} + for camera_config in self.config.cameras: + if camera_config.enabled: + results[camera_config.name] = self.check_camera_now(camera_config.name) + return results + + def get_monitoring_stats(self) -> Dict[str, Any]: + """Get monitoring statistics""" + return { + "running": self.running, + "check_interval_seconds": self.check_interval, + "total_checks": self.check_count, + "error_count": self.error_count, + "last_check_time": self.last_check_time, + "success_rate": (self.check_count - self.error_count) / max(self.check_count, 1) * 100 + } + + def is_running(self) -> bool: + """Check if monitor is running""" + return self.running diff --git a/usda_vision_system/camera/recorder.py b/usda_vision_system/camera/recorder.py new file mode 100644 index 0000000..1c9eaa7 --- /dev/null +++ b/usda_vision_system/camera/recorder.py @@ -0,0 +1,372 @@ +""" +Camera Recorder for the USDA Vision Camera System. + +This module handles video recording from GigE cameras using the python demo library (mvsdk). +""" + +import sys +import os +import threading +import time +import logging +import cv2 +import numpy as np +from typing import Optional, Dict, Any +from datetime import datetime +from pathlib import Path + +# Add python demo to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo')) +import mvsdk + +from ..core.config import CameraConfig +from ..core.state_manager import StateManager +from ..core.events import EventSystem, publish_recording_started, publish_recording_stopped, publish_recording_error +from ..core.timezone_utils import now_atlanta, format_filename_timestamp + + +class CameraRecorder: + """Handles video recording for a single camera""" + + def __init__(self, camera_config: CameraConfig, device_info: Any, state_manager: StateManager, event_system: EventSystem): + self.camera_config = camera_config + self.device_info = device_info + self.state_manager = state_manager + self.event_system = event_system + self.logger = logging.getLogger(f"{__name__}.{camera_config.name}") + + # Camera handle and properties + self.hCamera: Optional[int] = None + self.cap = None + self.monoCamera = False + self.frame_buffer = None + self.frame_buffer_size = 0 + + # Recording state + self.recording = False + self.video_writer: Optional[cv2.VideoWriter] = None + self.output_filename: Optional[str] = None + self.frame_count = 0 + self.start_time: Optional[datetime] = None + + # Threading + self._recording_thread: Optional[threading.Thread] = None + self._stop_recording_event = threading.Event() + self._lock = threading.RLock() + + # Initialize camera + self._initialize_camera() + + def _initialize_camera(self) -> bool: + """Initialize the camera with configured settings""" + try: + self.logger.info(f"Initializing camera: {self.camera_config.name}") + + # Initialize camera + self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1) + self.logger.info("Camera initialized successfully") + + # Get camera capabilities + self.cap = mvsdk.CameraGetCapability(self.hCamera) + self.monoCamera = self.cap.sIspCapacity.bMonoSensor != 0 + self.logger.info(f"Camera type: {'Monochrome' if self.monoCamera else 'Color'}") + + # Set output format + if self.monoCamera: + mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8) + else: + mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8) + + # Configure camera settings + self._configure_camera_settings() + + # Allocate frame buffer + self.frame_buffer_size = (self.cap.sResolutionRange.iWidthMax * + self.cap.sResolutionRange.iHeightMax * + (1 if self.monoCamera else 3)) + self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16) + + # Start camera + mvsdk.CameraPlay(self.hCamera) + self.logger.info("Camera started successfully") + + return True + + except mvsdk.CameraException as e: + self.logger.error(f"Camera initialization failed({e.error_code}): {e.message}") + return False + except Exception as e: + self.logger.error(f"Unexpected error during camera initialization: {e}") + return False + + def _configure_camera_settings(self) -> None: + """Configure camera settings from config""" + try: + # Set trigger mode (continuous acquisition) + mvsdk.CameraSetTriggerMode(self.hCamera, 0) + + # Set manual exposure + mvsdk.CameraSetAeState(self.hCamera, 0) # Disable auto exposure + exposure_us = int(self.camera_config.exposure_ms * 1000) # Convert ms to microseconds + mvsdk.CameraSetExposureTime(self.hCamera, exposure_us) + + # Set analog gain + gain_value = int(self.camera_config.gain * 100) # Convert to camera units + mvsdk.CameraSetAnalogGain(self.hCamera, gain_value) + + self.logger.info(f"Camera settings configured - Exposure: {exposure_us}μs, Gain: {gain_value}") + + except Exception as e: + self.logger.warning(f"Error configuring camera settings: {e}") + + def start_recording(self, filename: str) -> bool: + """Start video recording""" + with self._lock: + if self.recording: + self.logger.warning("Already recording!") + return False + + if not self.hCamera: + self.logger.error("Camera not initialized") + return False + + try: + # Prepare output path + output_path = os.path.join(self.camera_config.storage_path, filename) + Path(self.camera_config.storage_path).mkdir(parents=True, exist_ok=True) + + # Test camera capture before starting recording + if not self._test_camera_capture(): + self.logger.error("Camera capture test failed") + return False + + # Initialize recording state + self.output_filename = output_path + self.frame_count = 0 + self.start_time = now_atlanta() # Use Atlanta timezone + self._stop_recording_event.clear() + + # Start recording thread + self._recording_thread = threading.Thread(target=self._recording_loop, daemon=True) + self._recording_thread.start() + + # Update state + self.recording = True + recording_id = self.state_manager.start_recording(self.camera_config.name, output_path) + + # Publish event + publish_recording_started(self.camera_config.name, output_path) + + self.logger.info(f"Started recording to: {output_path}") + return True + + except Exception as e: + self.logger.error(f"Error starting recording: {e}") + publish_recording_error(self.camera_config.name, str(e)) + return False + + def _test_camera_capture(self) -> bool: + """Test if camera can capture frames""" + try: + # Try to capture one frame + pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000) # 1 second timeout + mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead) + mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData) + return True + except Exception as e: + self.logger.error(f"Camera capture test failed: {e}") + return False + + def stop_recording(self) -> bool: + """Stop video recording""" + with self._lock: + if not self.recording: + self.logger.warning("Not currently recording") + return False + + try: + # Signal recording thread to stop + self._stop_recording_event.set() + + # Wait for recording thread to finish + if self._recording_thread and self._recording_thread.is_alive(): + self._recording_thread.join(timeout=5) + + # Update state + self.recording = False + + # Calculate duration and file size + duration = 0 + file_size = 0 + if self.start_time: + duration = (now_atlanta() - self.start_time).total_seconds() + + if self.output_filename and os.path.exists(self.output_filename): + file_size = os.path.getsize(self.output_filename) + + # Update state manager + if self.output_filename: + self.state_manager.stop_recording(self.output_filename, file_size, self.frame_count) + + # Publish event + publish_recording_stopped( + self.camera_config.name, + self.output_filename or "unknown", + duration + ) + + self.logger.info(f"Stopped recording - Duration: {duration:.1f}s, Frames: {self.frame_count}") + return True + + except Exception as e: + self.logger.error(f"Error stopping recording: {e}") + return False + + def _recording_loop(self) -> None: + """Main recording loop running in separate thread""" + try: + # Initialize video writer + if not self._initialize_video_writer(): + self.logger.error("Failed to initialize video writer") + return + + self.logger.info("Recording loop started") + + while not self._stop_recording_event.is_set(): + try: + # Capture frame + pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 200) # 200ms timeout + + # Process frame + mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead) + + # Convert to OpenCV format + frame = self._convert_frame_to_opencv(FrameHead) + + # Write frame to video + if frame is not None and self.video_writer: + self.video_writer.write(frame) + self.frame_count += 1 + + # Release buffer + mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData) + + # Control frame rate + time.sleep(1.0 / self.camera_config.target_fps) + + except mvsdk.CameraException as e: + if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT: + continue # Timeout is normal, continue + else: + self.logger.error(f"Camera error during recording: {e.message}") + break + except Exception as e: + self.logger.error(f"Error in recording loop: {e}") + break + + self.logger.info("Recording loop ended") + + except Exception as e: + self.logger.error(f"Fatal error in recording loop: {e}") + publish_recording_error(self.camera_config.name, str(e)) + finally: + self._cleanup_recording() + + def _initialize_video_writer(self) -> bool: + """Initialize OpenCV video writer""" + try: + # Get frame dimensions by capturing a test frame + pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000) + mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead) + mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData) + + # Set up video writer + fourcc = cv2.VideoWriter_fourcc(*'XVID') + frame_size = (FrameHead.iWidth, FrameHead.iHeight) + + self.video_writer = cv2.VideoWriter( + self.output_filename, + fourcc, + self.camera_config.target_fps, + frame_size + ) + + if not self.video_writer.isOpened(): + self.logger.error(f"Failed to open video writer for {self.output_filename}") + return False + + self.logger.info(f"Video writer initialized - Size: {frame_size}, FPS: {self.camera_config.target_fps}") + return True + + except Exception as e: + self.logger.error(f"Error initializing video writer: {e}") + return False + + def _convert_frame_to_opencv(self, frame_head) -> Optional[np.ndarray]: + """Convert camera frame to OpenCV format""" + try: + if self.monoCamera: + # Monochrome camera - convert to BGR + frame_data = np.frombuffer(self.frame_buffer, dtype=np.uint8) + frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth)) + frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR) + else: + # Color camera - already in BGR format + frame_data = np.frombuffer(self.frame_buffer, dtype=np.uint8) + frame_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3)) + + return frame_bgr + + except Exception as e: + self.logger.error(f"Error converting frame: {e}") + return None + + def _cleanup_recording(self) -> None: + """Clean up recording resources""" + try: + if self.video_writer: + self.video_writer.release() + self.video_writer = None + + self.recording = False + + except Exception as e: + self.logger.error(f"Error during recording cleanup: {e}") + + def cleanup(self) -> None: + """Clean up camera resources""" + try: + # Stop recording if active + if self.recording: + self.stop_recording() + + # Clean up camera + if self.hCamera: + mvsdk.CameraUnInit(self.hCamera) + self.hCamera = None + + # Free frame buffer + if self.frame_buffer: + mvsdk.CameraAlignFree(self.frame_buffer) + self.frame_buffer = None + + self.logger.info("Camera resources cleaned up") + + except Exception as e: + self.logger.error(f"Error during cleanup: {e}") + + def is_recording(self) -> bool: + """Check if currently recording""" + return self.recording + + def get_status(self) -> Dict[str, Any]: + """Get recorder status""" + return { + "camera_name": self.camera_config.name, + "is_recording": self.recording, + "current_file": self.output_filename, + "frame_count": self.frame_count, + "start_time": self.start_time.isoformat() if self.start_time else None, + "camera_initialized": self.hCamera is not None, + "storage_path": self.camera_config.storage_path + } diff --git a/usda_vision_system/core/__init__.py b/usda_vision_system/core/__init__.py new file mode 100644 index 0000000..3539c97 --- /dev/null +++ b/usda_vision_system/core/__init__.py @@ -0,0 +1,15 @@ +""" +USDA Vision Camera System - Core Module + +This module contains the core functionality for the USDA vision camera system, +including configuration management, state management, and event handling. +""" + +__version__ = "1.0.0" +__author__ = "USDA Vision Team" + +from .config import Config +from .state_manager import StateManager +from .events import EventSystem + +__all__ = ["Config", "StateManager", "EventSystem"] diff --git a/usda_vision_system/core/__pycache__/__init__.cpython-311.pyc b/usda_vision_system/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c3241c138086a70086cdadd1992f53c30a5a988 GIT binary patch literal 686 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 tg>6Jh}h? literal 0 HcmV?d00001 diff --git a/usda_vision_system/core/__pycache__/logging_config.cpython-311.pyc b/usda_vision_system/core/__pycache__/logging_config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c167a304e5dc53c9bc99e007f59d1c1cae1e520f GIT binary patch literal 13147 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 Wf}o2ZW=bJHe0!cUhaU?Vz5E~JlB%Hq literal 0 HcmV?d00001 diff --git a/usda_vision_system/core/__pycache__/state_manager.cpython-311.pyc b/usda_vision_system/core/__pycache__/state_manager.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1ca18465ba390332ddd46fb4ddb88728a1c573f GIT binary patch literal 21503 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 Rfb+`3XDt22I#|Yo{|BA1wIcuk literal 0 HcmV?d00001 diff --git a/usda_vision_system/core/__pycache__/timezone_utils.cpython-311.pyc b/usda_vision_system/core/__pycache__/timezone_utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e230f44a92417a22fcf983f17a28120471592498 GIT binary patch literal 12297 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 None: + """Load configuration from file""" + config_path = Path(self.config_file) + + if config_path.exists(): + try: + with open(config_path, 'r') as f: + config_data = json.load(f) + + # Load MQTT config + if 'mqtt' in config_data: + mqtt_data = config_data['mqtt'] + self.mqtt = MQTTConfig(**mqtt_data) + + # Load storage config + if 'storage' in config_data: + storage_data = config_data['storage'] + self.storage = StorageConfig(**storage_data) + + # Load system config + if 'system' in config_data: + system_data = config_data['system'] + self.system = SystemConfig(**system_data) + + # Load camera configs + if 'cameras' in config_data: + self.cameras = [ + CameraConfig(**cam_data) + for cam_data in config_data['cameras'] + ] + else: + self._create_default_camera_configs() + + self.logger.info(f"Configuration loaded from {config_path}") + + except Exception as e: + self.logger.error(f"Error loading config from {config_path}: {e}") + self._create_default_camera_configs() + else: + self.logger.info(f"Config file {config_path} not found, using defaults") + self._create_default_camera_configs() + self.save_config() # Save default config + + def _create_default_camera_configs(self) -> None: + """Create default camera configurations""" + self.cameras = [ + CameraConfig( + name="camera1", + machine_topic="vibratory_conveyor", + storage_path=os.path.join(self.storage.base_path, "camera1") + ), + CameraConfig( + name="camera2", + machine_topic="blower_separator", + storage_path=os.path.join(self.storage.base_path, "camera2") + ) + ] + + def save_config(self) -> None: + """Save current configuration to file""" + config_data = { + 'mqtt': asdict(self.mqtt), + 'storage': asdict(self.storage), + 'system': asdict(self.system), + 'cameras': [asdict(cam) for cam in self.cameras] + } + + try: + with open(self.config_file, 'w') as f: + json.dump(config_data, f, indent=2) + self.logger.info(f"Configuration saved to {self.config_file}") + except Exception as e: + self.logger.error(f"Error saving config to {self.config_file}: {e}") + + def _ensure_storage_directories(self) -> None: + """Ensure all storage directories exist""" + try: + # Create base storage directory + Path(self.storage.base_path).mkdir(parents=True, exist_ok=True) + + # Create camera-specific directories + for camera in self.cameras: + Path(camera.storage_path).mkdir(parents=True, exist_ok=True) + + self.logger.info("Storage directories verified/created") + except Exception as e: + self.logger.error(f"Error creating storage directories: {e}") + + def get_camera_by_topic(self, topic: str) -> Optional[CameraConfig]: + """Get camera configuration by MQTT topic""" + for camera in self.cameras: + if camera.machine_topic == topic: + return camera + return None + + def get_camera_by_name(self, name: str) -> Optional[CameraConfig]: + """Get camera configuration by name""" + for camera in self.cameras: + if camera.name == name: + return camera + return None + + def update_camera_config(self, name: str, **kwargs) -> bool: + """Update camera configuration""" + camera = self.get_camera_by_name(name) + if camera: + for key, value in kwargs.items(): + if hasattr(camera, key): + setattr(camera, key, value) + self.save_config() + return True + return False + + def to_dict(self) -> Dict[str, Any]: + """Convert configuration to dictionary""" + return { + 'mqtt': asdict(self.mqtt), + 'storage': asdict(self.storage), + 'system': asdict(self.system), + 'cameras': [asdict(cam) for cam in self.cameras] + } diff --git a/usda_vision_system/core/events.py b/usda_vision_system/core/events.py new file mode 100644 index 0000000..a1f0b44 --- /dev/null +++ b/usda_vision_system/core/events.py @@ -0,0 +1,195 @@ +""" +Event system for the USDA Vision Camera System. + +This module provides a thread-safe event system for communication between +different components of the system (MQTT, cameras, recording, etc.). +""" + +import threading +import logging +from typing import Dict, List, Callable, Any, Optional +from dataclasses import dataclass +from datetime import datetime +from enum import Enum + + +class EventType(Enum): + """Event types for the system""" + MACHINE_STATE_CHANGED = "machine_state_changed" + CAMERA_STATUS_CHANGED = "camera_status_changed" + RECORDING_STARTED = "recording_started" + RECORDING_STOPPED = "recording_stopped" + RECORDING_ERROR = "recording_error" + MQTT_CONNECTED = "mqtt_connected" + MQTT_DISCONNECTED = "mqtt_disconnected" + SYSTEM_SHUTDOWN = "system_shutdown" + + +@dataclass +class Event: + """Event data structure""" + event_type: EventType + source: str + data: Dict[str, Any] + timestamp: datetime + + def __post_init__(self): + if not isinstance(self.timestamp, datetime): + self.timestamp = datetime.now() + + +class EventSystem: + """Thread-safe event system for inter-component communication""" + + def __init__(self): + self.logger = logging.getLogger(__name__) + self._subscribers: Dict[EventType, List[Callable]] = {} + self._lock = threading.RLock() + self._event_history: List[Event] = [] + self._max_history = 1000 # Keep last 1000 events + + def subscribe(self, event_type: EventType, callback: Callable[[Event], None]) -> None: + """Subscribe to an event type""" + with self._lock: + if event_type not in self._subscribers: + self._subscribers[event_type] = [] + + if callback not in self._subscribers[event_type]: + self._subscribers[event_type].append(callback) + self.logger.debug(f"Subscribed to {event_type.value}") + + def unsubscribe(self, event_type: EventType, callback: Callable[[Event], None]) -> None: + """Unsubscribe from an event type""" + with self._lock: + if event_type in self._subscribers: + try: + self._subscribers[event_type].remove(callback) + self.logger.debug(f"Unsubscribed from {event_type.value}") + except ValueError: + pass # Callback wasn't subscribed + + def publish(self, event_type: EventType, source: str, data: Optional[Dict[str, Any]] = None) -> None: + """Publish an event""" + if data is None: + data = {} + + event = Event( + event_type=event_type, + source=source, + data=data, + timestamp=datetime.now() + ) + + # Add to history + with self._lock: + self._event_history.append(event) + if len(self._event_history) > self._max_history: + self._event_history.pop(0) + + # Notify subscribers + self._notify_subscribers(event) + + def _notify_subscribers(self, event: Event) -> None: + """Notify all subscribers of an event""" + with self._lock: + subscribers = self._subscribers.get(event.event_type, []).copy() + + for callback in subscribers: + try: + callback(event) + except Exception as e: + self.logger.error(f"Error in event callback for {event.event_type.value}: {e}") + + def get_recent_events(self, event_type: Optional[EventType] = None, limit: int = 100) -> List[Event]: + """Get recent events, optionally filtered by type""" + with self._lock: + events = self._event_history.copy() + + if event_type: + events = [e for e in events if e.event_type == event_type] + + return events[-limit:] if limit else events + + def clear_history(self) -> None: + """Clear event history""" + with self._lock: + self._event_history.clear() + self.logger.info("Event history cleared") + + def get_subscriber_count(self, event_type: EventType) -> int: + """Get number of subscribers for an event type""" + with self._lock: + return len(self._subscribers.get(event_type, [])) + + def get_all_event_types(self) -> List[EventType]: + """Get all event types that have subscribers""" + with self._lock: + return list(self._subscribers.keys()) + + +# Global event system instance +event_system = EventSystem() + + +# Convenience functions for common events +def publish_machine_state_changed(machine_name: str, state: str, source: str = "mqtt") -> None: + """Publish machine state change event""" + event_system.publish( + EventType.MACHINE_STATE_CHANGED, + source, + { + "machine_name": machine_name, + "state": state, + "previous_state": None # Could be enhanced to track previous state + } + ) + + +def publish_camera_status_changed(camera_name: str, status: str, details: str = "", source: str = "camera_monitor") -> None: + """Publish camera status change event""" + event_system.publish( + EventType.CAMERA_STATUS_CHANGED, + source, + { + "camera_name": camera_name, + "status": status, + "details": details + } + ) + + +def publish_recording_started(camera_name: str, filename: str, source: str = "recorder") -> None: + """Publish recording started event""" + event_system.publish( + EventType.RECORDING_STARTED, + source, + { + "camera_name": camera_name, + "filename": filename + } + ) + + +def publish_recording_stopped(camera_name: str, filename: str, duration_seconds: float, source: str = "recorder") -> None: + """Publish recording stopped event""" + event_system.publish( + EventType.RECORDING_STOPPED, + source, + { + "camera_name": camera_name, + "filename": filename, + "duration_seconds": duration_seconds + } + ) + + +def publish_recording_error(camera_name: str, error_message: str, source: str = "recorder") -> None: + """Publish recording error event""" + event_system.publish( + EventType.RECORDING_ERROR, + source, + { + "camera_name": camera_name, + "error_message": error_message + } + ) diff --git a/usda_vision_system/core/logging_config.py b/usda_vision_system/core/logging_config.py new file mode 100644 index 0000000..d4afe75 --- /dev/null +++ b/usda_vision_system/core/logging_config.py @@ -0,0 +1,260 @@ +""" +Logging configuration for the USDA Vision Camera System. + +This module provides comprehensive logging setup with rotation, formatting, +and different log levels for different components. +""" + +import logging +import logging.handlers +import os +import sys +from typing import Optional +from datetime import datetime + + +class ColoredFormatter(logging.Formatter): + """Colored formatter for console output""" + + # ANSI color codes + COLORS = { + 'DEBUG': '\033[36m', # Cyan + 'INFO': '\033[32m', # Green + 'WARNING': '\033[33m', # Yellow + 'ERROR': '\033[31m', # Red + 'CRITICAL': '\033[35m', # Magenta + 'RESET': '\033[0m' # Reset + } + + def format(self, record): + # Add color to levelname + if record.levelname in self.COLORS: + record.levelname = f"{self.COLORS[record.levelname]}{record.levelname}{self.COLORS['RESET']}" + + return super().format(record) + + +class USDAVisionLogger: + """Custom logger setup for the USDA Vision Camera System""" + + def __init__(self, log_level: str = "INFO", log_file: Optional[str] = None, + enable_console: bool = True, enable_rotation: bool = True): + self.log_level = log_level.upper() + self.log_file = log_file + self.enable_console = enable_console + self.enable_rotation = enable_rotation + + # Setup logging + self._setup_logging() + + def _setup_logging(self) -> None: + """Setup comprehensive logging configuration""" + + # Get root logger + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, self.log_level)) + + # Clear existing handlers + root_logger.handlers.clear() + + # Create formatters + detailed_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s' + ) + + simple_formatter = logging.Formatter( + '%(asctime)s - %(levelname)s - %(message)s' + ) + + colored_formatter = ColoredFormatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Console handler + if self.enable_console: + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(getattr(logging, self.log_level)) + console_handler.setFormatter(colored_formatter) + root_logger.addHandler(console_handler) + + # File handler + if self.log_file: + try: + # Create log directory if it doesn't exist + log_dir = os.path.dirname(self.log_file) + if log_dir and not os.path.exists(log_dir): + os.makedirs(log_dir) + + if self.enable_rotation: + # Rotating file handler (10MB max, keep 5 backups) + file_handler = logging.handlers.RotatingFileHandler( + self.log_file, + maxBytes=10*1024*1024, # 10MB + backupCount=5 + ) + else: + file_handler = logging.FileHandler(self.log_file) + + file_handler.setLevel(logging.DEBUG) # File gets all messages + file_handler.setFormatter(detailed_formatter) + root_logger.addHandler(file_handler) + + except Exception as e: + print(f"Warning: Could not setup file logging: {e}") + + # Setup specific logger levels for different components + self._setup_component_loggers() + + # Log the logging setup + logger = logging.getLogger(__name__) + logger.info(f"Logging initialized - Level: {self.log_level}, File: {self.log_file}") + + def _setup_component_loggers(self) -> None: + """Setup specific log levels for different components""" + + # MQTT client - can be verbose + mqtt_logger = logging.getLogger('usda_vision_system.mqtt') + if self.log_level == 'DEBUG': + mqtt_logger.setLevel(logging.DEBUG) + else: + mqtt_logger.setLevel(logging.INFO) + + # Camera components - important for debugging + camera_logger = logging.getLogger('usda_vision_system.camera') + camera_logger.setLevel(logging.INFO) + + # API server - can be noisy + api_logger = logging.getLogger('usda_vision_system.api') + if self.log_level == 'DEBUG': + api_logger.setLevel(logging.DEBUG) + else: + api_logger.setLevel(logging.INFO) + + # Uvicorn - reduce noise unless debugging + uvicorn_logger = logging.getLogger('uvicorn') + if self.log_level == 'DEBUG': + uvicorn_logger.setLevel(logging.INFO) + else: + uvicorn_logger.setLevel(logging.WARNING) + + # FastAPI - reduce noise + fastapi_logger = logging.getLogger('fastapi') + fastapi_logger.setLevel(logging.WARNING) + + @staticmethod + def setup_exception_logging(): + """Setup logging for uncaught exceptions""" + + def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + # Don't log keyboard interrupts + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + logger = logging.getLogger("uncaught_exception") + logger.critical( + "Uncaught exception", + exc_info=(exc_type, exc_value, exc_traceback) + ) + + sys.excepthook = handle_exception + + +class PerformanceLogger: + """Logger for performance monitoring""" + + def __init__(self, name: str): + self.logger = logging.getLogger(f"performance.{name}") + self.start_time: Optional[float] = None + + def start_timer(self, operation: str) -> None: + """Start timing an operation""" + import time + self.start_time = time.time() + self.logger.debug(f"Started: {operation}") + + def end_timer(self, operation: str) -> float: + """End timing an operation and log duration""" + import time + if self.start_time is None: + self.logger.warning(f"Timer not started for: {operation}") + return 0.0 + + duration = time.time() - self.start_time + self.logger.info(f"Completed: {operation} in {duration:.3f}s") + self.start_time = None + return duration + + def log_metric(self, metric_name: str, value: float, unit: str = "") -> None: + """Log a performance metric""" + self.logger.info(f"Metric: {metric_name} = {value} {unit}") + + +class ErrorTracker: + """Track and log errors with context""" + + def __init__(self, component_name: str): + self.component_name = component_name + self.logger = logging.getLogger(f"errors.{component_name}") + self.error_count = 0 + self.last_error_time: Optional[datetime] = None + + def log_error(self, error: Exception, context: str = "", + additional_data: Optional[dict] = None) -> None: + """Log an error with context and tracking""" + self.error_count += 1 + self.last_error_time = datetime.now() + + error_msg = f"Error in {self.component_name}" + if context: + error_msg += f" ({context})" + error_msg += f": {str(error)}" + + if additional_data: + error_msg += f" | Data: {additional_data}" + + self.logger.error(error_msg, exc_info=True) + + def log_warning(self, message: str, context: str = "") -> None: + """Log a warning with context""" + warning_msg = f"Warning in {self.component_name}" + if context: + warning_msg += f" ({context})" + warning_msg += f": {message}" + + self.logger.warning(warning_msg) + + def get_error_stats(self) -> dict: + """Get error statistics""" + return { + "component": self.component_name, + "error_count": self.error_count, + "last_error_time": self.last_error_time.isoformat() if self.last_error_time else None + } + + +def setup_logging(log_level: str = "INFO", log_file: Optional[str] = None) -> USDAVisionLogger: + """Setup logging for the entire application""" + + # Setup main logging + logger_setup = USDAVisionLogger( + log_level=log_level, + log_file=log_file, + enable_console=True, + enable_rotation=True + ) + + # Setup exception logging + USDAVisionLogger.setup_exception_logging() + + return logger_setup + + +def get_performance_logger(component_name: str) -> PerformanceLogger: + """Get a performance logger for a component""" + return PerformanceLogger(component_name) + + +def get_error_tracker(component_name: str) -> ErrorTracker: + """Get an error tracker for a component""" + return ErrorTracker(component_name) diff --git a/usda_vision_system/core/state_manager.py b/usda_vision_system/core/state_manager.py new file mode 100644 index 0000000..b308727 --- /dev/null +++ b/usda_vision_system/core/state_manager.py @@ -0,0 +1,328 @@ +""" +State management for the USDA Vision Camera System. + +This module manages the current state of machines, cameras, and recordings +in a thread-safe manner. +""" + +import threading +import logging +from typing import Dict, Optional, List, Any +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum + + +class MachineState(Enum): + """Machine states""" + UNKNOWN = "unknown" + ON = "on" + OFF = "off" + ERROR = "error" + + +class CameraStatus(Enum): + """Camera status""" + UNKNOWN = "unknown" + AVAILABLE = "available" + BUSY = "busy" + ERROR = "error" + DISCONNECTED = "disconnected" + + +class RecordingState(Enum): + """Recording states""" + IDLE = "idle" + RECORDING = "recording" + STOPPING = "stopping" + ERROR = "error" + + +@dataclass +class MachineInfo: + """Machine state information""" + name: str + state: MachineState = MachineState.UNKNOWN + last_updated: datetime = field(default_factory=datetime.now) + last_message: Optional[str] = None + mqtt_topic: Optional[str] = None + + +@dataclass +class CameraInfo: + """Camera state information""" + name: str + status: CameraStatus = CameraStatus.UNKNOWN + last_checked: datetime = field(default_factory=datetime.now) + last_error: Optional[str] = None + device_info: Optional[Dict[str, Any]] = None + is_recording: bool = False + current_recording_file: Optional[str] = None + recording_start_time: Optional[datetime] = None + + +@dataclass +class RecordingInfo: + """Recording session information""" + camera_name: str + filename: str + start_time: datetime + state: RecordingState = RecordingState.RECORDING + end_time: Optional[datetime] = None + file_size_bytes: Optional[int] = None + frame_count: Optional[int] = None + error_message: Optional[str] = None + + +class StateManager: + """Thread-safe state manager for the entire system""" + + def __init__(self): + self.logger = logging.getLogger(__name__) + self._lock = threading.RLock() + + # State dictionaries + self._machines: Dict[str, MachineInfo] = {} + self._cameras: Dict[str, CameraInfo] = {} + self._recordings: Dict[str, RecordingInfo] = {} # Key: recording_id (filename) + + # System state + self._mqtt_connected = False + self._system_started = False + self._last_mqtt_message_time: Optional[datetime] = None + + # Machine state management + def update_machine_state(self, name: str, state: str, message: Optional[str] = None, topic: Optional[str] = None) -> bool: + """Update machine state""" + try: + machine_state = MachineState(state.lower()) + except ValueError: + self.logger.warning(f"Invalid machine state: {state}") + machine_state = MachineState.UNKNOWN + + with self._lock: + if name not in self._machines: + self._machines[name] = MachineInfo(name=name, mqtt_topic=topic) + + machine = self._machines[name] + old_state = machine.state + machine.state = machine_state + machine.last_updated = datetime.now() + machine.last_message = message + if topic: + machine.mqtt_topic = topic + + self.logger.info(f"Machine {name} state: {old_state.value} -> {machine_state.value}") + return old_state != machine_state + + def get_machine_state(self, name: str) -> Optional[MachineInfo]: + """Get machine state""" + with self._lock: + return self._machines.get(name) + + def get_all_machines(self) -> Dict[str, MachineInfo]: + """Get all machine states""" + with self._lock: + return self._machines.copy() + + # Camera state management + def update_camera_status(self, name: str, status: str, error: Optional[str] = None, device_info: Optional[Dict] = None) -> bool: + """Update camera status""" + try: + camera_status = CameraStatus(status.lower()) + except ValueError: + self.logger.warning(f"Invalid camera status: {status}") + camera_status = CameraStatus.UNKNOWN + + with self._lock: + if name not in self._cameras: + self._cameras[name] = CameraInfo(name=name) + + camera = self._cameras[name] + old_status = camera.status + camera.status = camera_status + camera.last_checked = datetime.now() + camera.last_error = error + if device_info: + camera.device_info = device_info + + if old_status != camera_status: + self.logger.info(f"Camera {name} status: {old_status.value} -> {camera_status.value}") + return True + return False + + def set_camera_recording(self, name: str, recording: bool, filename: Optional[str] = None) -> None: + """Set camera recording state""" + with self._lock: + if name not in self._cameras: + self._cameras[name] = CameraInfo(name=name) + + camera = self._cameras[name] + camera.is_recording = recording + camera.current_recording_file = filename + + if recording and filename: + camera.recording_start_time = datetime.now() + self.logger.info(f"Camera {name} started recording: {filename}") + elif not recording: + camera.recording_start_time = None + self.logger.info(f"Camera {name} stopped recording") + + def get_camera_status(self, name: str) -> Optional[CameraInfo]: + """Get camera status""" + with self._lock: + return self._cameras.get(name) + + def get_all_cameras(self) -> Dict[str, CameraInfo]: + """Get all camera statuses""" + with self._lock: + return self._cameras.copy() + + # Recording management + def start_recording(self, camera_name: str, filename: str) -> str: + """Start a new recording session""" + recording_id = filename # Use filename as recording ID + + with self._lock: + recording = RecordingInfo( + camera_name=camera_name, + filename=filename, + start_time=datetime.now() + ) + self._recordings[recording_id] = recording + + # Update camera state + self.set_camera_recording(camera_name, True, filename) + + self.logger.info(f"Started recording session: {recording_id}") + return recording_id + + def stop_recording(self, recording_id: str, file_size: Optional[int] = None, frame_count: Optional[int] = None) -> bool: + """Stop a recording session""" + with self._lock: + if recording_id not in self._recordings: + self.logger.warning(f"Recording session not found: {recording_id}") + return False + + recording = self._recordings[recording_id] + recording.state = RecordingState.IDLE + recording.end_time = datetime.now() + recording.file_size_bytes = file_size + recording.frame_count = frame_count + + # Update camera state + self.set_camera_recording(recording.camera_name, False) + + duration = (recording.end_time - recording.start_time).total_seconds() + self.logger.info(f"Stopped recording session: {recording_id} (duration: {duration:.1f}s)") + return True + + def set_recording_error(self, recording_id: str, error_message: str) -> bool: + """Set recording error state""" + with self._lock: + if recording_id not in self._recordings: + return False + + recording = self._recordings[recording_id] + recording.state = RecordingState.ERROR + recording.error_message = error_message + recording.end_time = datetime.now() + + # Update camera state + self.set_camera_recording(recording.camera_name, False) + + self.logger.error(f"Recording error for {recording_id}: {error_message}") + return True + + def get_recording(self, recording_id: str) -> Optional[RecordingInfo]: + """Get recording information""" + with self._lock: + return self._recordings.get(recording_id) + + def get_all_recordings(self) -> Dict[str, RecordingInfo]: + """Get all recording sessions""" + with self._lock: + return self._recordings.copy() + + def get_active_recordings(self) -> Dict[str, RecordingInfo]: + """Get currently active recordings""" + with self._lock: + return { + rid: recording for rid, recording in self._recordings.items() + if recording.state == RecordingState.RECORDING + } + + # System state management + def set_mqtt_connected(self, connected: bool) -> None: + """Set MQTT connection state""" + with self._lock: + old_state = self._mqtt_connected + self._mqtt_connected = connected + if connected: + self._last_mqtt_message_time = datetime.now() + + if old_state != connected: + self.logger.info(f"MQTT connection: {'connected' if connected else 'disconnected'}") + + def is_mqtt_connected(self) -> bool: + """Check if MQTT is connected""" + with self._lock: + return self._mqtt_connected + + def update_mqtt_activity(self) -> None: + """Update last MQTT message time""" + with self._lock: + self._last_mqtt_message_time = datetime.now() + + def set_system_started(self, started: bool) -> None: + """Set system started state""" + with self._lock: + self._system_started = started + self.logger.info(f"System {'started' if started else 'stopped'}") + + def is_system_started(self) -> bool: + """Check if system is started""" + with self._lock: + return self._system_started + + # Utility methods + def get_system_summary(self) -> Dict[str, Any]: + """Get a summary of the entire system state""" + with self._lock: + return { + "system_started": self._system_started, + "mqtt_connected": self._mqtt_connected, + "last_mqtt_message": self._last_mqtt_message_time.isoformat() if self._last_mqtt_message_time else None, + "machines": {name: { + "state": machine.state.value, + "last_updated": machine.last_updated.isoformat() + } for name, machine in self._machines.items()}, + "cameras": {name: { + "status": camera.status.value, + "is_recording": camera.is_recording, + "last_checked": camera.last_checked.isoformat() + } for name, camera in self._cameras.items()}, + "active_recordings": len(self.get_active_recordings()), + "total_recordings": len(self._recordings) + } + + def cleanup_old_recordings(self, max_age_hours: int = 24) -> int: + """Clean up old recording entries from memory""" + cutoff_time = datetime.now() - datetime.timedelta(hours=max_age_hours) + removed_count = 0 + + with self._lock: + to_remove = [] + for recording_id, recording in self._recordings.items(): + if (recording.state != RecordingState.RECORDING and + recording.end_time and recording.end_time < cutoff_time): + to_remove.append(recording_id) + + for recording_id in to_remove: + del self._recordings[recording_id] + removed_count += 1 + + if removed_count > 0: + self.logger.info(f"Cleaned up {removed_count} old recording entries") + + return removed_count diff --git a/usda_vision_system/core/timezone_utils.py b/usda_vision_system/core/timezone_utils.py new file mode 100644 index 0000000..c831e75 --- /dev/null +++ b/usda_vision_system/core/timezone_utils.py @@ -0,0 +1,225 @@ +""" +Timezone utilities for the USDA Vision Camera System. + +This module provides timezone-aware datetime handling for Atlanta, Georgia. +""" + +import datetime +import pytz +import logging +from typing import Optional + + +class TimezoneManager: + """Manages timezone-aware datetime operations""" + + def __init__(self, timezone_name: str = "America/New_York"): + self.timezone_name = timezone_name + self.timezone = pytz.timezone(timezone_name) + self.logger = logging.getLogger(__name__) + + # Log timezone information + self.logger.info(f"Timezone manager initialized for {timezone_name}") + self._log_timezone_info() + + def _log_timezone_info(self) -> None: + """Log current timezone information""" + now = self.now() + self.logger.info(f"Current local time: {now}") + self.logger.info(f"Current UTC time: {self.to_utc(now)}") + self.logger.info(f"Timezone: {now.tzname()} (UTC{now.strftime('%z')})") + + def now(self) -> datetime.datetime: + """Get current time in the configured timezone""" + return datetime.datetime.now(self.timezone) + + def utc_now(self) -> datetime.datetime: + """Get current UTC time""" + return datetime.datetime.now(pytz.UTC) + + def to_local(self, dt: datetime.datetime) -> datetime.datetime: + """Convert datetime to local timezone""" + if dt.tzinfo is None: + # Assume UTC if no timezone info + dt = pytz.UTC.localize(dt) + return dt.astimezone(self.timezone) + + def to_utc(self, dt: datetime.datetime) -> datetime.datetime: + """Convert datetime to UTC""" + if dt.tzinfo is None: + # Assume local timezone if no timezone info + dt = self.timezone.localize(dt) + return dt.astimezone(pytz.UTC) + + def localize(self, dt: datetime.datetime) -> datetime.datetime: + """Add timezone info to naive datetime (assumes local timezone)""" + if dt.tzinfo is not None: + return dt + return self.timezone.localize(dt) + + def format_timestamp(self, dt: Optional[datetime.datetime] = None, + include_timezone: bool = True) -> str: + """Format datetime as timestamp string""" + if dt is None: + dt = self.now() + + if dt.tzinfo is None: + dt = self.localize(dt) + + if include_timezone: + return dt.strftime("%Y-%m-%d %H:%M:%S %Z") + else: + return dt.strftime("%Y-%m-%d %H:%M:%S") + + def format_filename_timestamp(self, dt: Optional[datetime.datetime] = None) -> str: + """Format datetime for use in filenames (no special characters)""" + if dt is None: + dt = self.now() + + if dt.tzinfo is None: + dt = self.localize(dt) + + return dt.strftime("%Y%m%d_%H%M%S") + + def parse_timestamp(self, timestamp_str: str) -> datetime.datetime: + """Parse timestamp string to datetime""" + try: + # Try parsing with timezone info + return datetime.datetime.fromisoformat(timestamp_str) + except ValueError: + try: + # Try parsing without timezone (assume local) + dt = datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S") + return self.localize(dt) + except ValueError: + try: + # Try parsing filename format + dt = datetime.datetime.strptime(timestamp_str, "%Y%m%d_%H%M%S") + return self.localize(dt) + except ValueError: + raise ValueError(f"Unable to parse timestamp: {timestamp_str}") + + def is_dst(self, dt: Optional[datetime.datetime] = None) -> bool: + """Check if datetime is during daylight saving time""" + if dt is None: + dt = self.now() + + if dt.tzinfo is None: + dt = self.localize(dt) + + return bool(dt.dst()) + + def get_timezone_offset(self, dt: Optional[datetime.datetime] = None) -> str: + """Get timezone offset string (e.g., '-0500' or '-0400')""" + if dt is None: + dt = self.now() + + if dt.tzinfo is None: + dt = self.localize(dt) + + return dt.strftime('%z') + + def get_timezone_name(self, dt: Optional[datetime.datetime] = None) -> str: + """Get timezone name (e.g., 'EST' or 'EDT')""" + if dt is None: + dt = self.now() + + if dt.tzinfo is None: + dt = self.localize(dt) + + return dt.tzname() + + +# Global timezone manager instance for Atlanta, Georgia +atlanta_tz = TimezoneManager("America/New_York") + + +# Convenience functions +def now_atlanta() -> datetime.datetime: + """Get current Atlanta time""" + return atlanta_tz.now() + + +def format_atlanta_timestamp(dt: Optional[datetime.datetime] = None) -> str: + """Format timestamp in Atlanta timezone""" + return atlanta_tz.format_timestamp(dt) + + +def format_filename_timestamp(dt: Optional[datetime.datetime] = None) -> str: + """Format timestamp for filenames""" + return atlanta_tz.format_filename_timestamp(dt) + + +def to_atlanta_time(dt: datetime.datetime) -> datetime.datetime: + """Convert any datetime to Atlanta time""" + return atlanta_tz.to_local(dt) + + +def check_time_sync() -> dict: + """Check if system time appears to be synchronized""" + import requests + + result = { + "system_time": now_atlanta(), + "timezone": atlanta_tz.get_timezone_name(), + "offset": atlanta_tz.get_timezone_offset(), + "dst": atlanta_tz.is_dst(), + "sync_status": "unknown", + "time_diff_seconds": None, + "error": None + } + + try: + # Check against world time API + response = requests.get( + "http://worldtimeapi.org/api/timezone/America/New_York", + timeout=5 + ) + + if response.status_code == 200: + data = response.json() + api_time = datetime.datetime.fromisoformat(data['datetime']) + + # Convert to same timezone for comparison + system_time = atlanta_tz.now() + time_diff = abs((system_time.replace(tzinfo=None) - + api_time.replace(tzinfo=None)).total_seconds()) + + result["api_time"] = api_time + result["time_diff_seconds"] = time_diff + + if time_diff < 5: + result["sync_status"] = "synchronized" + elif time_diff < 30: + result["sync_status"] = "minor_drift" + else: + result["sync_status"] = "out_of_sync" + else: + result["error"] = f"API returned status {response.status_code}" + + except Exception as e: + result["error"] = str(e) + + return result + + +def log_time_info(logger: Optional[logging.Logger] = None) -> None: + """Log comprehensive time information""" + if logger is None: + logger = logging.getLogger(__name__) + + sync_info = check_time_sync() + + logger.info("=== TIME SYNCHRONIZATION STATUS ===") + logger.info(f"System time: {sync_info['system_time']}") + logger.info(f"Timezone: {sync_info['timezone']} ({sync_info['offset']})") + logger.info(f"Daylight Saving: {'Yes' if sync_info['dst'] else 'No'}") + logger.info(f"Sync status: {sync_info['sync_status']}") + + if sync_info.get('time_diff_seconds') is not None: + logger.info(f"Time difference: {sync_info['time_diff_seconds']:.2f} seconds") + + if sync_info.get('error'): + logger.warning(f"Time sync check error: {sync_info['error']}") + + logger.info("=====================================") diff --git a/usda_vision_system/main.py b/usda_vision_system/main.py new file mode 100644 index 0000000..4144d8c --- /dev/null +++ b/usda_vision_system/main.py @@ -0,0 +1,288 @@ +""" +Main Application Coordinator for the USDA Vision Camera System. + +This module coordinates all system components and provides graceful startup/shutdown. +""" + +import signal +import time +import logging +import sys +from typing import Optional +from datetime import datetime + +from .core.config import Config +from .core.state_manager import StateManager +from .core.events import EventSystem, EventType +from .core.logging_config import setup_logging, get_error_tracker, get_performance_logger +from .core.timezone_utils import log_time_info, check_time_sync +from .mqtt.client import MQTTClient +from .camera.manager import CameraManager +from .storage.manager import StorageManager +from .api.server import APIServer + + +class USDAVisionSystem: + """Main application coordinator for the USDA Vision Camera System""" + + def __init__(self, config_file: Optional[str] = None): + # Load configuration first (basic logging will be used initially) + self.config = Config(config_file) + + # Setup comprehensive logging + self.logger_setup = setup_logging( + log_level=self.config.system.log_level, + log_file=self.config.system.log_file + ) + self.logger = logging.getLogger(__name__) + + # Setup error tracking and performance monitoring + self.error_tracker = get_error_tracker("main_system") + self.performance_logger = get_performance_logger("main_system") + + # Initialize core components + self.state_manager = StateManager() + self.event_system = EventSystem() + + # Initialize system components + self.storage_manager = StorageManager(self.config, self.state_manager) + self.mqtt_client = MQTTClient(self.config, self.state_manager, self.event_system) + self.camera_manager = CameraManager(self.config, self.state_manager, self.event_system) + self.api_server = APIServer( + self.config, self.state_manager, self.event_system, + self.camera_manager, self.mqtt_client, self.storage_manager + ) + + # System state + self.running = False + self.start_time: Optional[datetime] = None + + # Setup signal handlers for graceful shutdown + self._setup_signal_handlers() + + self.logger.info("USDA Vision Camera System initialized") + + def _setup_signal_handlers(self) -> None: + """Setup signal handlers for graceful shutdown""" + def signal_handler(signum, frame): + self.logger.info(f"Received signal {signum}, initiating graceful shutdown...") + self.stop() + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + def start(self) -> bool: + """Start the entire system""" + if self.running: + self.logger.warning("System is already running") + return True + + self.logger.info("Starting USDA Vision Camera System...") + self.performance_logger.start_timer("system_startup") + self.start_time = datetime.now() + + # Check time synchronization + self.logger.info("Checking time synchronization...") + log_time_info(self.logger) + sync_info = check_time_sync() + if sync_info["sync_status"] == "out_of_sync": + self.error_tracker.log_warning( + f"System time may be out of sync (difference: {sync_info.get('time_diff_seconds', 'unknown')}s)", + "time_sync_check" + ) + elif sync_info["sync_status"] == "synchronized": + self.logger.info("āœ… System time is synchronized") + + try: + # Start storage manager (no background tasks) + self.logger.info("Initializing storage manager...") + try: + # Verify storage integrity + integrity_report = self.storage_manager.verify_storage_integrity() + if integrity_report.get("fixed_issues", 0) > 0: + self.logger.info(f"Fixed {integrity_report['fixed_issues']} storage integrity issues") + self.logger.info("Storage manager ready") + except Exception as e: + self.error_tracker.log_error(e, "storage_manager_init") + self.logger.error("Failed to initialize storage manager") + return False + + # Start MQTT client + self.logger.info("Starting MQTT client...") + try: + if not self.mqtt_client.start(): + self.error_tracker.log_error(Exception("MQTT client failed to start"), "mqtt_startup") + return False + self.logger.info("MQTT client started successfully") + except Exception as e: + self.error_tracker.log_error(e, "mqtt_startup") + return False + + # Start camera manager + self.logger.info("Starting camera manager...") + try: + if not self.camera_manager.start(): + self.error_tracker.log_error(Exception("Camera manager failed to start"), "camera_startup") + self.mqtt_client.stop() + return False + self.logger.info("Camera manager started successfully") + except Exception as e: + self.error_tracker.log_error(e, "camera_startup") + self.mqtt_client.stop() + return False + + # Start API server + self.logger.info("Starting API server...") + try: + if not self.api_server.start(): + self.error_tracker.log_warning("Failed to start API server", "api_startup") + else: + self.logger.info("API server started successfully") + except Exception as e: + self.error_tracker.log_error(e, "api_startup") + self.logger.warning("API server failed to start (continuing without API)") + + # Update system state + self.running = True + self.state_manager.set_system_started(True) + + # Publish system started event + self.event_system.publish( + EventType.SYSTEM_SHUTDOWN, # We don't have SYSTEM_STARTED, using closest + "main_system", + {"action": "started", "timestamp": self.start_time.isoformat()} + ) + + startup_time = self.performance_logger.end_timer("system_startup") + self.logger.info(f"USDA Vision Camera System started successfully in {startup_time:.2f}s") + return True + + except Exception as e: + self.error_tracker.log_error(e, "system_startup") + self.stop() + return False + + def stop(self) -> None: + """Stop the entire system gracefully""" + if not self.running: + return + + self.logger.info("Stopping USDA Vision Camera System...") + self.running = False + + try: + # Update system state + self.state_manager.set_system_started(False) + + # Publish system shutdown event + self.event_system.publish( + EventType.SYSTEM_SHUTDOWN, + "main_system", + {"action": "stopping", "timestamp": datetime.now().isoformat()} + ) + + # Stop API server + self.api_server.stop() + + # Stop camera manager (this will stop all recordings) + self.camera_manager.stop() + + # Stop MQTT client + self.mqtt_client.stop() + + # Final cleanup + if self.start_time: + uptime = (datetime.now() - self.start_time).total_seconds() + self.logger.info(f"System uptime: {uptime:.1f} seconds") + + self.logger.info("USDA Vision Camera System stopped") + + except Exception as e: + self.logger.error(f"Error during system shutdown: {e}") + + def run(self) -> None: + """Run the system (blocking call)""" + if not self.start(): + self.logger.error("Failed to start system") + return + + try: + self.logger.info("System running... Press Ctrl+C to stop") + + # Main loop - just keep the system alive + while self.running: + time.sleep(1) + + # Periodic maintenance tasks could go here + # For example: cleanup old recordings, health checks, etc. + + except KeyboardInterrupt: + self.logger.info("Keyboard interrupt received") + except Exception as e: + self.logger.error(f"Unexpected error in main loop: {e}") + finally: + self.stop() + + def get_system_status(self) -> dict: + """Get comprehensive system status""" + return { + "running": self.running, + "start_time": self.start_time.isoformat() if self.start_time else None, + "uptime_seconds": (datetime.now() - self.start_time).total_seconds() if self.start_time else 0, + "components": { + "mqtt_client": { + "running": self.mqtt_client.is_running(), + "connected": self.mqtt_client.is_connected() + }, + "camera_manager": { + "running": self.camera_manager.is_running() + }, + "api_server": { + "running": self.api_server.is_running() + } + }, + "state_summary": self.state_manager.get_system_summary() + } + + def is_running(self) -> bool: + """Check if system is running""" + return self.running + + +def main(): + """Main entry point for the application""" + import argparse + + parser = argparse.ArgumentParser(description="USDA Vision Camera System") + parser.add_argument( + "--config", + type=str, + help="Path to configuration file", + default="config.json" + ) + parser.add_argument( + "--log-level", + type=str, + choices=["DEBUG", "INFO", "WARNING", "ERROR"], + help="Override log level", + default=None + ) + + args = parser.parse_args() + + # Create and run system + system = USDAVisionSystem(args.config) + + # Override log level if specified + if args.log_level: + logging.getLogger().setLevel(getattr(logging, args.log_level)) + + try: + system.run() + except Exception as e: + logging.error(f"Fatal error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/usda_vision_system/mqtt/__init__.py b/usda_vision_system/mqtt/__init__.py new file mode 100644 index 0000000..a80b192 --- /dev/null +++ b/usda_vision_system/mqtt/__init__.py @@ -0,0 +1,11 @@ +""" +MQTT module for the USDA Vision Camera System. + +This module handles MQTT communication for receiving machine state updates +and triggering camera recording based on machine states. +""" + +from .client import MQTTClient +from .handlers import MQTTMessageHandler + +__all__ = ["MQTTClient", "MQTTMessageHandler"] diff --git a/usda_vision_system/mqtt/__pycache__/__init__.cpython-311.pyc b/usda_vision_system/mqtt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0db93751a4ba88317a60571d8591606f33634bea GIT binary patch literal 544 zcmY*XJxc>I7*6i4ADq^~MO-wa722EJL=Y>uRFGDOTSC0VyTIkHCb>d83H}Iwg7^dc zA04HW;N(^)E}cy7EZ8Kxd6PWPnZMA%0TK4mtoIn7+>|))VzhrrAk{3Wh2q>gZ z3WXCo(B^LH3NQ3b?$KK8gf&_pfOcd0?)PtoLnLxqqzpae66u7Y+d=Of-Eqb93|$bx zBte6T(oA%Hf0%H!#wR4BDO0Gd5#>S@8IOpzG+Roti19JcVkAhE@Qfj)iDsx6Q8Ovu zu#uKLj+wNesJh9pa!KvRJyDDzQ*48-y8gE@0JPz}6n~L&mT9xvwKDolDH5}*Qkj&l z0ZPw=>vnzKz?kL{#`Ahqa>~Y1=`A%qL32E7HaVF$p& c894YqKETmzb9`>?&GtJpzx&j%UD}WF3-3Ln$p8QV literal 0 HcmV?d00001 diff --git a/usda_vision_system/mqtt/__pycache__/client.cpython-311.pyc b/usda_vision_system/mqtt/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..738a8a7ee62cc0ff7f66e91a7da6928d84d9ec96 GIT binary patch literal 13859 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 machine_name) + self.topic_to_machine = { + topic: machine_name + for machine_name, topic in self.mqtt_config.topics.items() + } + + def start(self) -> bool: + """Start the MQTT client in a separate thread""" + if self.running: + self.logger.warning("MQTT client is already running") + return True + + self.logger.info("Starting MQTT client...") + self.running = True + self._stop_event.clear() + + # Start in separate thread + self._thread = threading.Thread(target=self._run_loop, daemon=True) + self._thread.start() + + # Wait a moment to see if connection succeeds + time.sleep(2) + return self.connected + + def stop(self) -> None: + """Stop the MQTT client""" + if not self.running: + return + + self.logger.info("Stopping MQTT client...") + self.running = False + self._stop_event.set() + + if self.client and self.connected: + self.client.disconnect() + + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=5) + + self.logger.info("MQTT client stopped") + + def _run_loop(self) -> None: + """Main MQTT client loop""" + reconnect_attempts = 0 + + while self.running and not self._stop_event.is_set(): + try: + if not self.connected: + if self._connect(): + reconnect_attempts = 0 + self._subscribe_to_topics() + else: + reconnect_attempts += 1 + if reconnect_attempts >= self.max_reconnect_attempts: + self.logger.error(f"Max reconnection attempts ({self.max_reconnect_attempts}) reached") + break + + self.logger.warning(f"Reconnection attempt {reconnect_attempts}/{self.max_reconnect_attempts} in {self.reconnect_delay}s") + if self._stop_event.wait(self.reconnect_delay): + break + continue + + # Process MQTT messages + if self.client: + self.client.loop(timeout=1.0) + + # Small delay to prevent busy waiting + if self._stop_event.wait(0.1): + break + + except Exception as e: + self.logger.error(f"Error in MQTT loop: {e}") + self.connected = False + if self._stop_event.wait(self.reconnect_delay): + break + + self.running = False + self.logger.info("MQTT client loop ended") + + def _connect(self) -> bool: + """Connect to MQTT broker""" + try: + # Create new client instance + self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) + + # Set callbacks + self.client.on_connect = self._on_connect + self.client.on_disconnect = self._on_disconnect + self.client.on_message = self._on_message + + # Set authentication if provided + if self.mqtt_config.username and self.mqtt_config.password: + self.client.username_pw_set( + self.mqtt_config.username, + self.mqtt_config.password + ) + + # Connect to broker + self.logger.info(f"Connecting to MQTT broker at {self.mqtt_config.broker_host}:{self.mqtt_config.broker_port}") + self.client.connect( + self.mqtt_config.broker_host, + self.mqtt_config.broker_port, + 60 + ) + + return True + + except Exception as e: + self.logger.error(f"Failed to connect to MQTT broker: {e}") + return False + + def _subscribe_to_topics(self) -> None: + """Subscribe to all configured topics""" + if not self.client or not self.connected: + return + + for machine_name, topic in self.mqtt_config.topics.items(): + try: + result, mid = self.client.subscribe(topic) + if result == mqtt.MQTT_ERR_SUCCESS: + self.logger.info(f"Subscribed to topic: {topic} (machine: {machine_name})") + else: + self.logger.error(f"Failed to subscribe to topic: {topic}") + except Exception as e: + self.logger.error(f"Error subscribing to topic {topic}: {e}") + + def _on_connect(self, client, userdata, flags, rc) -> None: + """Callback for when the client connects to the broker""" + if rc == 0: + self.connected = True + self.state_manager.set_mqtt_connected(True) + self.event_system.publish(EventType.MQTT_CONNECTED, "mqtt_client") + self.logger.info("Successfully connected to MQTT broker") + else: + self.connected = False + self.logger.error(f"Failed to connect to MQTT broker, return code {rc}") + + def _on_disconnect(self, client, userdata, rc) -> None: + """Callback for when the client disconnects from the broker""" + self.connected = False + self.state_manager.set_mqtt_connected(False) + self.event_system.publish(EventType.MQTT_DISCONNECTED, "mqtt_client") + + if rc != 0: + self.logger.warning(f"Unexpected MQTT disconnection (rc: {rc})") + else: + self.logger.info("MQTT client disconnected") + + def _on_message(self, client, userdata, msg) -> None: + """Callback for when a message is received""" + try: + topic = msg.topic + payload = msg.payload.decode('utf-8').strip() + + self.logger.debug(f"MQTT message received - Topic: {topic}, Payload: {payload}") + + # Update MQTT activity + self.state_manager.update_mqtt_activity() + + # Get machine name from topic + machine_name = self.topic_to_machine.get(topic) + if not machine_name: + self.logger.warning(f"Unknown topic: {topic}") + return + + # Handle the message + self.message_handler.handle_message(machine_name, topic, payload) + + except Exception as e: + self.logger.error(f"Error processing MQTT message: {e}") + + def publish_message(self, topic: str, payload: str, qos: int = 0, retain: bool = False) -> bool: + """Publish a message to MQTT broker""" + if not self.client or not self.connected: + self.logger.warning("Cannot publish: MQTT client not connected") + return False + + try: + result = self.client.publish(topic, payload, qos, retain) + if result.rc == mqtt.MQTT_ERR_SUCCESS: + self.logger.debug(f"Published message to {topic}: {payload}") + return True + else: + self.logger.error(f"Failed to publish message to {topic}") + return False + except Exception as e: + self.logger.error(f"Error publishing message: {e}") + return False + + def get_status(self) -> Dict[str, any]: + """Get MQTT client status""" + return { + "connected": self.connected, + "running": self.running, + "broker_host": self.mqtt_config.broker_host, + "broker_port": self.mqtt_config.broker_port, + "subscribed_topics": list(self.mqtt_config.topics.values()), + "topic_mappings": self.topic_to_machine + } + + def is_connected(self) -> bool: + """Check if MQTT client is connected""" + return self.connected + + def is_running(self) -> bool: + """Check if MQTT client is running""" + return self.running diff --git a/usda_vision_system/mqtt/handlers.py b/usda_vision_system/mqtt/handlers.py new file mode 100644 index 0000000..8e2330f --- /dev/null +++ b/usda_vision_system/mqtt/handlers.py @@ -0,0 +1,153 @@ +""" +MQTT Message Handlers for the USDA Vision Camera System. + +This module handles processing of MQTT messages and triggering appropriate actions. +""" + +import logging +from typing import Dict, Optional +from datetime import datetime + +from ..core.state_manager import StateManager, MachineState +from ..core.events import EventSystem, publish_machine_state_changed + + +class MQTTMessageHandler: + """Handles MQTT messages and triggers appropriate system actions""" + + def __init__(self, state_manager: StateManager, event_system: EventSystem): + self.state_manager = state_manager + self.event_system = event_system + self.logger = logging.getLogger(__name__) + + # Message processing statistics + self.message_count = 0 + self.last_message_time: Optional[datetime] = None + self.error_count = 0 + + def handle_message(self, machine_name: str, topic: str, payload: str) -> None: + """Handle an incoming MQTT message""" + try: + self.message_count += 1 + self.last_message_time = datetime.now() + + self.logger.info(f"Processing MQTT message - Machine: {machine_name}, Topic: {topic}, Payload: {payload}") + + # Normalize payload + normalized_payload = self._normalize_payload(payload) + + # Update machine state + state_changed = self.state_manager.update_machine_state( + name=machine_name, + state=normalized_payload, + message=payload, + topic=topic + ) + + # Publish state change event if state actually changed + if state_changed: + publish_machine_state_changed( + machine_name=machine_name, + state=normalized_payload, + source="mqtt_handler" + ) + + self.logger.info(f"Machine {machine_name} state changed to: {normalized_payload}") + + # Log the message for debugging + self._log_message_details(machine_name, topic, payload, normalized_payload) + + except Exception as e: + self.error_count += 1 + self.logger.error(f"Error handling MQTT message for {machine_name}: {e}") + + def _normalize_payload(self, payload: str) -> str: + """Normalize payload to standard machine states""" + payload_lower = payload.lower().strip() + + # Map various possible payloads to standard states + if payload_lower in ['on', 'true', '1', 'start', 'running', 'active']: + return 'on' + elif payload_lower in ['off', 'false', '0', 'stop', 'stopped', 'inactive']: + return 'off' + elif payload_lower in ['error', 'fault', 'alarm']: + return 'error' + else: + # For unknown payloads, log and return as-is + self.logger.warning(f"Unknown payload format: '{payload}', treating as raw state") + return payload_lower + + def _log_message_details(self, machine_name: str, topic: str, original_payload: str, normalized_payload: str) -> None: + """Log detailed message information""" + self.logger.debug(f"MQTT Message Details:") + self.logger.debug(f" Machine: {machine_name}") + self.logger.debug(f" Topic: {topic}") + self.logger.debug(f" Original Payload: '{original_payload}'") + self.logger.debug(f" Normalized Payload: '{normalized_payload}'") + self.logger.debug(f" Timestamp: {self.last_message_time}") + self.logger.debug(f" Total Messages Processed: {self.message_count}") + + def get_statistics(self) -> Dict[str, any]: + """Get message processing statistics""" + return { + "total_messages": self.message_count, + "error_count": self.error_count, + "last_message_time": self.last_message_time.isoformat() if self.last_message_time else None, + "success_rate": (self.message_count - self.error_count) / max(self.message_count, 1) * 100 + } + + def reset_statistics(self) -> None: + """Reset message processing statistics""" + self.message_count = 0 + self.error_count = 0 + self.last_message_time = None + self.logger.info("MQTT message handler statistics reset") + + +class MachineStateProcessor: + """Processes machine state changes and determines actions""" + + def __init__(self, state_manager: StateManager, event_system: EventSystem): + self.state_manager = state_manager + self.event_system = event_system + self.logger = logging.getLogger(__name__) + + def process_state_change(self, machine_name: str, old_state: str, new_state: str) -> None: + """Process a machine state change and determine what actions to take""" + self.logger.info(f"Processing state change for {machine_name}: {old_state} -> {new_state}") + + # Handle state transitions + if old_state != 'on' and new_state == 'on': + self._handle_machine_turned_on(machine_name) + elif old_state == 'on' and new_state != 'on': + self._handle_machine_turned_off(machine_name) + elif new_state == 'error': + self._handle_machine_error(machine_name) + + def _handle_machine_turned_on(self, machine_name: str) -> None: + """Handle machine turning on - should start recording""" + self.logger.info(f"Machine {machine_name} turned ON - should start recording") + + # The actual recording start will be handled by the camera manager + # which listens to the MACHINE_STATE_CHANGED event + + # We could add additional logic here, such as: + # - Checking if camera is available + # - Pre-warming camera settings + # - Sending notifications + + def _handle_machine_turned_off(self, machine_name: str) -> None: + """Handle machine turning off - should stop recording""" + self.logger.info(f"Machine {machine_name} turned OFF - should stop recording") + + # The actual recording stop will be handled by the camera manager + # which listens to the MACHINE_STATE_CHANGED event + + def _handle_machine_error(self, machine_name: str) -> None: + """Handle machine error state""" + self.logger.warning(f"Machine {machine_name} in ERROR state") + + # Could implement error handling logic here: + # - Stop recording if active + # - Send alerts + # - Log error details diff --git a/usda_vision_system/storage/__init__.py b/usda_vision_system/storage/__init__.py new file mode 100644 index 0000000..ecb45fd --- /dev/null +++ b/usda_vision_system/storage/__init__.py @@ -0,0 +1,9 @@ +""" +Storage module for the USDA Vision Camera System. + +This module handles file organization, management, and cleanup for recorded videos. +""" + +from .manager import StorageManager + +__all__ = ["StorageManager"] diff --git a/usda_vision_system/storage/__pycache__/__init__.cpython-311.pyc b/usda_vision_system/storage/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da433afcf2cb72865d9d117e1328d0bb80f1276d GIT binary patch literal 430 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 literal 0 HcmV?d00001 diff --git a/usda_vision_system/storage/manager.py b/usda_vision_system/storage/manager.py new file mode 100644 index 0000000..33ecb26 --- /dev/null +++ b/usda_vision_system/storage/manager.py @@ -0,0 +1,349 @@ +""" +Storage Manager for the USDA Vision Camera System. + +This module handles file organization, cleanup, and management for recorded videos. +""" + +import os +import logging +import shutil +from typing import Dict, List, Optional, Any, Tuple +from datetime import datetime, timedelta +from pathlib import Path +import json + +from ..core.config import Config, StorageConfig +from ..core.state_manager import StateManager + + +class StorageManager: + """Manages storage and file organization for recorded videos""" + + def __init__(self, config: Config, state_manager: StateManager): + self.config = config + self.storage_config = config.storage + self.state_manager = state_manager + self.logger = logging.getLogger(__name__) + + # Ensure base storage directory exists + self._ensure_storage_structure() + + # File tracking + self.file_index_path = os.path.join(self.storage_config.base_path, "file_index.json") + self.file_index = self._load_file_index() + + def _ensure_storage_structure(self) -> None: + """Ensure storage directory structure exists""" + try: + # Create base storage directory + Path(self.storage_config.base_path).mkdir(parents=True, exist_ok=True) + + # Create camera-specific directories + for camera_config in self.config.cameras: + Path(camera_config.storage_path).mkdir(parents=True, exist_ok=True) + self.logger.debug(f"Ensured storage directory: {camera_config.storage_path}") + + self.logger.info("Storage directory structure verified") + + except Exception as e: + self.logger.error(f"Error creating storage structure: {e}") + raise + + def _load_file_index(self) -> Dict[str, Any]: + """Load file index from disk""" + try: + if os.path.exists(self.file_index_path): + with open(self.file_index_path, 'r') as f: + return json.load(f) + else: + return {"files": {}, "last_updated": None} + except Exception as e: + self.logger.error(f"Error loading file index: {e}") + return {"files": {}, "last_updated": None} + + def _save_file_index(self) -> None: + """Save file index to disk""" + try: + self.file_index["last_updated"] = datetime.now().isoformat() + with open(self.file_index_path, 'w') as f: + json.dump(self.file_index, f, indent=2) + except Exception as e: + self.logger.error(f"Error saving file index: {e}") + + def register_recording_file(self, camera_name: str, filename: str, start_time: datetime, + machine_trigger: Optional[str] = None) -> str: + """Register a new recording file""" + try: + file_id = os.path.basename(filename) + + file_info = { + "camera_name": camera_name, + "filename": filename, + "file_id": file_id, + "start_time": start_time.isoformat(), + "end_time": None, + "file_size_bytes": None, + "duration_seconds": None, + "machine_trigger": machine_trigger, + "status": "recording", + "created_at": datetime.now().isoformat() + } + + self.file_index["files"][file_id] = file_info + self._save_file_index() + + self.logger.info(f"Registered recording file: {file_id}") + return file_id + + except Exception as e: + self.logger.error(f"Error registering recording file: {e}") + return "" + + def finalize_recording_file(self, file_id: str, end_time: datetime, + duration_seconds: float, frame_count: Optional[int] = None) -> bool: + """Finalize a recording file after recording stops""" + try: + if file_id not in self.file_index["files"]: + self.logger.warning(f"File ID not found in index: {file_id}") + return False + + file_info = self.file_index["files"][file_id] + filename = file_info["filename"] + + # Update file information + file_info["end_time"] = end_time.isoformat() + file_info["duration_seconds"] = duration_seconds + file_info["status"] = "completed" + + # Get file size if file exists + if os.path.exists(filename): + file_info["file_size_bytes"] = os.path.getsize(filename) + + if frame_count is not None: + file_info["frame_count"] = frame_count + + self._save_file_index() + + self.logger.info(f"Finalized recording file: {file_id} (duration: {duration_seconds:.1f}s)") + return True + + except Exception as e: + self.logger.error(f"Error finalizing recording file: {e}") + return False + + def get_recording_files(self, camera_name: Optional[str] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + limit: Optional[int] = None) -> List[Dict[str, Any]]: + """Get list of recording files with optional filters""" + try: + files = [] + + for file_id, file_info in self.file_index["files"].items(): + # Filter by camera name + if camera_name and file_info["camera_name"] != camera_name: + continue + + # Filter by date range + if start_date or end_date: + file_start = datetime.fromisoformat(file_info["start_time"]) + if start_date and file_start < start_date: + continue + if end_date and file_start > end_date: + continue + + files.append(file_info.copy()) + + # Sort by start time (newest first) + files.sort(key=lambda x: x["start_time"], reverse=True) + + # Apply limit + if limit: + files = files[:limit] + + return files + + except Exception as e: + self.logger.error(f"Error getting recording files: {e}") + return [] + + def get_storage_statistics(self) -> Dict[str, Any]: + """Get storage usage statistics""" + try: + stats = { + "base_path": self.storage_config.base_path, + "total_files": 0, + "total_size_bytes": 0, + "cameras": {}, + "disk_usage": {} + } + + # Get disk usage for base path + if os.path.exists(self.storage_config.base_path): + disk_usage = shutil.disk_usage(self.storage_config.base_path) + stats["disk_usage"] = { + "total_bytes": disk_usage.total, + "used_bytes": disk_usage.used, + "free_bytes": disk_usage.free, + "used_percent": (disk_usage.used / disk_usage.total) * 100 + } + + # Analyze files by camera + for file_info in self.file_index["files"].values(): + camera_name = file_info["camera_name"] + + if camera_name not in stats["cameras"]: + stats["cameras"][camera_name] = { + "file_count": 0, + "total_size_bytes": 0, + "total_duration_seconds": 0 + } + + stats["total_files"] += 1 + stats["cameras"][camera_name]["file_count"] += 1 + + if file_info.get("file_size_bytes"): + size = file_info["file_size_bytes"] + stats["total_size_bytes"] += size + stats["cameras"][camera_name]["total_size_bytes"] += size + + if file_info.get("duration_seconds"): + duration = file_info["duration_seconds"] + stats["cameras"][camera_name]["total_duration_seconds"] += duration + + return stats + + except Exception as e: + self.logger.error(f"Error getting storage statistics: {e}") + return {} + + def cleanup_old_files(self, max_age_days: Optional[int] = None) -> Dict[str, Any]: + """Clean up old recording files""" + if max_age_days is None: + max_age_days = self.storage_config.cleanup_older_than_days + + cutoff_date = datetime.now() - timedelta(days=max_age_days) + + cleanup_stats = { + "files_removed": 0, + "bytes_freed": 0, + "errors": [] + } + + try: + files_to_remove = [] + + # Find files older than cutoff date + for file_id, file_info in self.file_index["files"].items(): + try: + file_start = datetime.fromisoformat(file_info["start_time"]) + if file_start < cutoff_date and file_info["status"] == "completed": + files_to_remove.append((file_id, file_info)) + except Exception as e: + cleanup_stats["errors"].append(f"Error parsing date for {file_id}: {e}") + + # Remove old files + for file_id, file_info in files_to_remove: + try: + filename = file_info["filename"] + + # Remove physical file + if os.path.exists(filename): + file_size = os.path.getsize(filename) + os.remove(filename) + cleanup_stats["bytes_freed"] += file_size + self.logger.info(f"Removed old file: {filename}") + + # Remove from index + del self.file_index["files"][file_id] + cleanup_stats["files_removed"] += 1 + + except Exception as e: + error_msg = f"Error removing file {file_id}: {e}" + cleanup_stats["errors"].append(error_msg) + self.logger.error(error_msg) + + # Save updated index + if cleanup_stats["files_removed"] > 0: + self._save_file_index() + + self.logger.info(f"Cleanup completed: {cleanup_stats['files_removed']} files removed, " + f"{cleanup_stats['bytes_freed']} bytes freed") + + return cleanup_stats + + except Exception as e: + self.logger.error(f"Error during cleanup: {e}") + cleanup_stats["errors"].append(str(e)) + return cleanup_stats + + def get_file_info(self, file_id: str) -> Optional[Dict[str, Any]]: + """Get information about a specific file""" + return self.file_index["files"].get(file_id) + + def delete_file(self, file_id: str) -> bool: + """Delete a specific recording file""" + try: + if file_id not in self.file_index["files"]: + self.logger.warning(f"File ID not found: {file_id}") + return False + + file_info = self.file_index["files"][file_id] + filename = file_info["filename"] + + # Remove physical file + if os.path.exists(filename): + os.remove(filename) + self.logger.info(f"Deleted file: {filename}") + + # Remove from index + del self.file_index["files"][file_id] + self._save_file_index() + + return True + + except Exception as e: + self.logger.error(f"Error deleting file {file_id}: {e}") + return False + + def verify_storage_integrity(self) -> Dict[str, Any]: + """Verify storage integrity and fix issues""" + integrity_report = { + "total_files_in_index": len(self.file_index["files"]), + "missing_files": [], + "orphaned_files": [], + "corrupted_entries": [], + "fixed_issues": 0 + } + + try: + # Check for missing files (in index but not on disk) + for file_id, file_info in list(self.file_index["files"].items()): + filename = file_info.get("filename") + if filename and not os.path.exists(filename): + integrity_report["missing_files"].append(file_id) + # Remove from index + del self.file_index["files"][file_id] + integrity_report["fixed_issues"] += 1 + + # Check for orphaned files (on disk but not in index) + for camera_config in self.config.cameras: + storage_path = Path(camera_config.storage_path) + if storage_path.exists(): + for video_file in storage_path.glob("*.avi"): + file_id = video_file.name + if file_id not in self.file_index["files"]: + integrity_report["orphaned_files"].append(str(video_file)) + + # Save updated index if fixes were made + if integrity_report["fixed_issues"] > 0: + self._save_file_index() + + self.logger.info(f"Storage integrity check completed: {integrity_report['fixed_issues']} issues fixed") + + return integrity_report + + except Exception as e: + self.logger.error(f"Error during integrity check: {e}") + integrity_report["error"] = str(e) + return integrity_report diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..363539a --- /dev/null +++ b/uv.lock @@ -0,0 +1,850 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +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 = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { 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/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 = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +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 = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, +] + +[[package]] +name = "fonttools" +version = "4.59.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" }, + { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704, upload-time = "2025-07-16T12:04:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764, upload-time = "2025-07-16T12:04:23.985Z" }, + { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699, upload-time = "2025-07-16T12:04:25.664Z" }, + { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934, upload-time = "2025-07-16T12:04:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319, upload-time = "2025-07-16T12:04:30.074Z" }, + { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753, upload-time = "2025-07-16T12:04:32.292Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688, upload-time = "2025-07-16T12:04:34.444Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560, upload-time = "2025-07-16T12:04:36.034Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/47/57e897fb7094afb2d26e8b2e4af9a45c7cf1a405acdeeca001fdf2c98501/imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996", size = 389963, upload-time = "2025-01-20T02:42:37.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796, upload-time = "2025-01-20T02:42:34.931Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" }, +] + +[[package]] +name = "numpy" +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/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494, upload-time = "2025-07-24T20:27:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889, upload-time = "2025-07-24T20:27:29.558Z" }, + { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560, upload-time = "2025-07-24T20:27:46.803Z" }, + { 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/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { 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/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { 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/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { 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/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { 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/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paho-mqtt" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[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 = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +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 = "starlette" +version = "0.47.2" +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/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +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]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +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 = "usda-vision-cameras" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "imageio" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "paho-mqtt" }, + { name = "pillow" }, + { name = "pytz" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "uvicorn" }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.104.0" }, + { name = "imageio", specifier = ">=2.37.0" }, + { name = "matplotlib", specifier = ">=3.10.3" }, + { name = "numpy", specifier = ">=2.3.2" }, + { name = "opencv-python", specifier = ">=4.11.0.86" }, + { name = "paho-mqtt", specifier = ">=2.1.0" }, + { name = "pillow", specifier = ">=11.3.0" }, + { name = "pytz", specifier = ">=2023.3" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "tqdm", specifier = ">=4.67.1" }, + { name = "uvicorn", specifier = ">=0.24.0" }, + { name = "websockets", specifier = ">=12.0" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +]