Massive update - API and other modules added
This commit is contained in:
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.11
|
||||
BIN
Camera/Data/054012620023.mvdat
Normal file
BIN
Camera/Data/054012620023.mvdat
Normal file
Binary file not shown.
BIN
Camera/Data/054052320151.mvdat
Normal file
BIN
Camera/Data/054052320151.mvdat
Normal file
Binary file not shown.
221
README.md
221
README.md
@@ -1 +1,220 @@
|
||||
# USDA-Vision-Cameras
|
||||
# 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
|
||||
```
|
||||
|
||||
58
check_time.py
Executable file
58
check_time.py
Executable file
@@ -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()
|
||||
47
config.json
Normal file
47
config.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
18
main.py
Normal file
18
main.py
Normal file
@@ -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()
|
||||
184
old tests/IMPLEMENTATION_SUMMARY.md
Normal file
184
old tests/IMPLEMENTATION_SUMMARY.md
Normal file
@@ -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**
|
||||
1
old tests/README.md
Normal file
1
old tests/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# USDA-Vision-Cameras
|
||||
249
old tests/README_SYSTEM.md
Normal file
249
old tests/README_SYSTEM.md
Normal file
@@ -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`
|
||||
190
old tests/TIMEZONE_SETUP_SUMMARY.md
Normal file
190
old tests/TIMEZONE_SETUP_SUMMARY.md
Normal file
@@ -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**
|
||||
@@ -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,
|
||||
349
old tests/camera_test_setup.ipynb
Normal file
349
old tests/camera_test_setup.ipynb
Normal file
@@ -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
|
||||
}
|
||||
385
old tests/gige_camera_advanced.ipynb
Normal file
385
old tests/gige_camera_advanced.ipynb
Normal file
@@ -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
|
||||
}
|
||||
6
old tests/main.py
Normal file
6
old tests/main.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from usda-vision-cameras!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
146
old tests/mqtt test.ipynb
Normal file
146
old tests/mqtt test.ipynb
Normal file
@@ -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
|
||||
}
|
||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
@@ -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",
|
||||
]
|
||||
BIN
python demo/__pycache__/mvsdk.cpython-311.pyc
Normal file
BIN
python demo/__pycache__/mvsdk.cpython-311.pyc
Normal file
Binary file not shown.
289
setup_timezone.sh
Executable file
289
setup_timezone.sh
Executable file
@@ -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)"
|
||||
59
start_system.sh
Executable file
59
start_system.sh
Executable file
@@ -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."
|
||||
55
start_system.sh.backup
Executable file
55
start_system.sh.backup
Executable file
@@ -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."
|
||||
223
test_system.py
Normal file
223
test_system.py
Normal file
@@ -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())
|
||||
56
test_timezone.py
Normal file
56
test_timezone.py
Normal file
@@ -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()
|
||||
13
usda_vision_system/__init__.py
Normal file
13
usda_vision_system/__init__.py
Normal file
@@ -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"]
|
||||
BIN
usda_vision_system/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
usda_vision_system/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/__pycache__/main.cpython-311.pyc
Normal file
BIN
usda_vision_system/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
10
usda_vision_system/api/__init__.py
Normal file
10
usda_vision_system/api/__init__.py
Normal file
@@ -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"]
|
||||
BIN
usda_vision_system/api/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
usda_vision_system/api/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/api/__pycache__/models.cpython-311.pyc
Normal file
BIN
usda_vision_system/api/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/api/__pycache__/server.cpython-311.pyc
Normal file
BIN
usda_vision_system/api/__pycache__/server.cpython-311.pyc
Normal file
Binary file not shown.
145
usda_vision_system/api/models.py
Normal file
145
usda_vision_system/api/models.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Data models for the USDA Vision Camera System API.
|
||||
|
||||
This module defines Pydantic models for API requests and responses.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SystemStatusResponse(BaseModel):
|
||||
"""System status response model"""
|
||||
system_started: bool
|
||||
mqtt_connected: bool
|
||||
last_mqtt_message: Optional[str] = None
|
||||
machines: Dict[str, Dict[str, Any]]
|
||||
cameras: Dict[str, Dict[str, Any]]
|
||||
active_recordings: int
|
||||
total_recordings: int
|
||||
uptime_seconds: Optional[float] = None
|
||||
|
||||
|
||||
class MachineStatusResponse(BaseModel):
|
||||
"""Machine status response model"""
|
||||
name: str
|
||||
state: str
|
||||
last_updated: str
|
||||
last_message: Optional[str] = None
|
||||
mqtt_topic: Optional[str] = None
|
||||
|
||||
|
||||
class CameraStatusResponse(BaseModel):
|
||||
"""Camera status response model"""
|
||||
name: str
|
||||
status: str
|
||||
is_recording: bool
|
||||
last_checked: str
|
||||
last_error: Optional[str] = None
|
||||
device_info: Optional[Dict[str, Any]] = None
|
||||
current_recording_file: Optional[str] = None
|
||||
recording_start_time: Optional[str] = None
|
||||
|
||||
|
||||
class RecordingInfoResponse(BaseModel):
|
||||
"""Recording information response model"""
|
||||
camera_name: str
|
||||
filename: str
|
||||
start_time: str
|
||||
state: str
|
||||
end_time: Optional[str] = None
|
||||
file_size_bytes: Optional[int] = None
|
||||
frame_count: Optional[int] = None
|
||||
duration_seconds: Optional[float] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class StartRecordingRequest(BaseModel):
|
||||
"""Start recording request model"""
|
||||
camera_name: str
|
||||
filename: Optional[str] = None
|
||||
|
||||
|
||||
class StartRecordingResponse(BaseModel):
|
||||
"""Start recording response model"""
|
||||
success: bool
|
||||
message: str
|
||||
filename: Optional[str] = None
|
||||
|
||||
|
||||
class StopRecordingRequest(BaseModel):
|
||||
"""Stop recording request model"""
|
||||
camera_name: str
|
||||
|
||||
|
||||
class StopRecordingResponse(BaseModel):
|
||||
"""Stop recording response model"""
|
||||
success: bool
|
||||
message: str
|
||||
duration_seconds: Optional[float] = None
|
||||
|
||||
|
||||
class StorageStatsResponse(BaseModel):
|
||||
"""Storage statistics response model"""
|
||||
base_path: str
|
||||
total_files: int
|
||||
total_size_bytes: int
|
||||
cameras: Dict[str, Dict[str, Any]]
|
||||
disk_usage: Dict[str, Any]
|
||||
|
||||
|
||||
class FileListRequest(BaseModel):
|
||||
"""File list request model"""
|
||||
camera_name: Optional[str] = None
|
||||
start_date: Optional[str] = None
|
||||
end_date: Optional[str] = None
|
||||
limit: Optional[int] = Field(default=100, le=1000)
|
||||
|
||||
|
||||
class FileListResponse(BaseModel):
|
||||
"""File list response model"""
|
||||
files: List[Dict[str, Any]]
|
||||
total_count: int
|
||||
|
||||
|
||||
class CleanupRequest(BaseModel):
|
||||
"""Cleanup request model"""
|
||||
max_age_days: Optional[int] = None
|
||||
|
||||
|
||||
class CleanupResponse(BaseModel):
|
||||
"""Cleanup response model"""
|
||||
files_removed: int
|
||||
bytes_freed: int
|
||||
errors: List[str]
|
||||
|
||||
|
||||
class EventResponse(BaseModel):
|
||||
"""Event response model"""
|
||||
event_type: str
|
||||
source: str
|
||||
data: Dict[str, Any]
|
||||
timestamp: str
|
||||
|
||||
|
||||
class WebSocketMessage(BaseModel):
|
||||
"""WebSocket message model"""
|
||||
type: str
|
||||
data: Dict[str, Any]
|
||||
timestamp: Optional[str] = None
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Error response model"""
|
||||
error: str
|
||||
details: Optional[str] = None
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
class SuccessResponse(BaseModel):
|
||||
"""Success response model"""
|
||||
success: bool = True
|
||||
message: str
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
426
usda_vision_system/api/server.py
Normal file
426
usda_vision_system/api/server.py
Normal file
@@ -0,0 +1,426 @@
|
||||
"""
|
||||
FastAPI Server for the USDA Vision Camera System.
|
||||
|
||||
This module provides REST API endpoints and WebSocket support for dashboard integration.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timedelta
|
||||
import threading
|
||||
|
||||
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Depends, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
|
||||
from ..core.config import Config
|
||||
from ..core.state_manager import StateManager
|
||||
from ..core.events import EventSystem, EventType, Event
|
||||
from ..storage.manager import StorageManager
|
||||
from .models import *
|
||||
|
||||
|
||||
class WebSocketManager:
|
||||
"""Manages WebSocket connections for real-time updates"""
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
self.logger = logging.getLogger(f"{__name__}.WebSocketManager")
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
self.logger.info(f"WebSocket connected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
if websocket in self.active_connections:
|
||||
self.active_connections.remove(websocket)
|
||||
self.logger.info(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
async def send_personal_message(self, message: dict, websocket: WebSocket):
|
||||
try:
|
||||
await websocket.send_text(json.dumps(message))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error sending personal message: {e}")
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
if not self.active_connections:
|
||||
return
|
||||
|
||||
disconnected = []
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_text(json.dumps(message))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error broadcasting to connection: {e}")
|
||||
disconnected.append(connection)
|
||||
|
||||
# Remove disconnected connections
|
||||
for connection in disconnected:
|
||||
self.disconnect(connection)
|
||||
|
||||
|
||||
class APIServer:
|
||||
"""FastAPI server for the USDA Vision Camera System"""
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem,
|
||||
camera_manager, mqtt_client, storage_manager: StorageManager):
|
||||
self.config = config
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.camera_manager = camera_manager
|
||||
self.mqtt_client = mqtt_client
|
||||
self.storage_manager = storage_manager
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# FastAPI app
|
||||
self.app = FastAPI(
|
||||
title="USDA Vision Camera System API",
|
||||
description="API for monitoring and controlling the USDA vision camera system",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# WebSocket manager
|
||||
self.websocket_manager = WebSocketManager()
|
||||
|
||||
# Server state
|
||||
self.server_start_time = datetime.now()
|
||||
self.running = False
|
||||
self._server_thread: Optional[threading.Thread] = None
|
||||
|
||||
# Setup CORS
|
||||
self.app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Configure appropriately for production
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Setup routes
|
||||
self._setup_routes()
|
||||
|
||||
# Subscribe to events for WebSocket broadcasting
|
||||
self._setup_event_subscriptions()
|
||||
|
||||
def _setup_routes(self):
|
||||
"""Setup API routes"""
|
||||
|
||||
@self.app.get("/", response_model=SuccessResponse)
|
||||
async def root():
|
||||
return SuccessResponse(message="USDA Vision Camera System API")
|
||||
|
||||
@self.app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
@self.app.get("/system/status", response_model=SystemStatusResponse)
|
||||
async def get_system_status():
|
||||
"""Get overall system status"""
|
||||
try:
|
||||
summary = self.state_manager.get_system_summary()
|
||||
uptime = (datetime.now() - self.server_start_time).total_seconds()
|
||||
|
||||
return SystemStatusResponse(
|
||||
system_started=summary["system_started"],
|
||||
mqtt_connected=summary["mqtt_connected"],
|
||||
last_mqtt_message=summary["last_mqtt_message"],
|
||||
machines=summary["machines"],
|
||||
cameras=summary["cameras"],
|
||||
active_recordings=summary["active_recordings"],
|
||||
total_recordings=summary["total_recordings"],
|
||||
uptime_seconds=uptime
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting system status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/machines", response_model=Dict[str, MachineStatusResponse])
|
||||
async def get_machines():
|
||||
"""Get all machine statuses"""
|
||||
try:
|
||||
machines = self.state_manager.get_all_machines()
|
||||
return {
|
||||
name: MachineStatusResponse(
|
||||
name=machine.name,
|
||||
state=machine.state.value,
|
||||
last_updated=machine.last_updated.isoformat(),
|
||||
last_message=machine.last_message,
|
||||
mqtt_topic=machine.mqtt_topic
|
||||
)
|
||||
for name, machine in machines.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting machines: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras", response_model=Dict[str, CameraStatusResponse])
|
||||
async def get_cameras():
|
||||
"""Get all camera statuses"""
|
||||
try:
|
||||
cameras = self.state_manager.get_all_cameras()
|
||||
return {
|
||||
name: CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
for name, camera in cameras.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting cameras: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras/{camera_name}/status", response_model=CameraStatusResponse)
|
||||
async def get_camera_status(camera_name: str):
|
||||
"""Get specific camera status"""
|
||||
try:
|
||||
camera = self.state_manager.get_camera_status(camera_name)
|
||||
if not camera:
|
||||
raise HTTPException(status_code=404, detail=f"Camera not found: {camera_name}")
|
||||
|
||||
return CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting camera status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/start-recording", response_model=StartRecordingResponse)
|
||||
async def start_recording(camera_name: str, request: StartRecordingRequest):
|
||||
"""Manually start recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.manual_start_recording(camera_name, request.filename)
|
||||
|
||||
if success:
|
||||
return StartRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording started for {camera_name}",
|
||||
filename=request.filename
|
||||
)
|
||||
else:
|
||||
return StartRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to start recording for {camera_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/stop-recording", response_model=StopRecordingResponse)
|
||||
async def stop_recording(camera_name: str):
|
||||
"""Manually stop recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.manual_stop_recording(camera_name)
|
||||
|
||||
if success:
|
||||
return StopRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording stopped for {camera_name}"
|
||||
)
|
||||
else:
|
||||
return StopRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to stop recording for {camera_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/recordings", response_model=Dict[str, RecordingInfoResponse])
|
||||
async def get_recordings():
|
||||
"""Get all recording sessions"""
|
||||
try:
|
||||
recordings = self.state_manager.get_all_recordings()
|
||||
return {
|
||||
rid: RecordingInfoResponse(
|
||||
camera_name=recording.camera_name,
|
||||
filename=recording.filename,
|
||||
start_time=recording.start_time.isoformat(),
|
||||
state=recording.state.value,
|
||||
end_time=recording.end_time.isoformat() if recording.end_time else None,
|
||||
file_size_bytes=recording.file_size_bytes,
|
||||
frame_count=recording.frame_count,
|
||||
duration_seconds=(recording.end_time - recording.start_time).total_seconds() if recording.end_time else None,
|
||||
error_message=recording.error_message
|
||||
)
|
||||
for rid, recording in recordings.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting recordings: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/storage/stats", response_model=StorageStatsResponse)
|
||||
async def get_storage_stats():
|
||||
"""Get storage statistics"""
|
||||
try:
|
||||
stats = self.storage_manager.get_storage_statistics()
|
||||
return StorageStatsResponse(**stats)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting storage stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/storage/files", response_model=FileListResponse)
|
||||
async def get_files(request: FileListRequest):
|
||||
"""Get list of recording files"""
|
||||
try:
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
if request.start_date:
|
||||
start_date = datetime.fromisoformat(request.start_date)
|
||||
if request.end_date:
|
||||
end_date = datetime.fromisoformat(request.end_date)
|
||||
|
||||
files = self.storage_manager.get_recording_files(
|
||||
camera_name=request.camera_name,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=request.limit
|
||||
)
|
||||
|
||||
return FileListResponse(
|
||||
files=files,
|
||||
total_count=len(files)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting files: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/storage/cleanup", response_model=CleanupResponse)
|
||||
async def cleanup_storage(request: CleanupRequest):
|
||||
"""Clean up old storage files"""
|
||||
try:
|
||||
result = self.storage_manager.cleanup_old_files(request.max_age_days)
|
||||
return CleanupResponse(**result)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during cleanup: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for real-time updates"""
|
||||
await self.websocket_manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
# Keep connection alive and handle incoming messages
|
||||
data = await websocket.receive_text()
|
||||
# Echo back for now - could implement commands later
|
||||
await self.websocket_manager.send_personal_message(
|
||||
{"type": "echo", "data": data}, websocket
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
self.websocket_manager.disconnect(websocket)
|
||||
|
||||
def _setup_event_subscriptions(self):
|
||||
"""Setup event subscriptions for WebSocket broadcasting"""
|
||||
|
||||
def broadcast_event(event: Event):
|
||||
"""Broadcast event to all WebSocket connections"""
|
||||
try:
|
||||
message = {
|
||||
"type": "event",
|
||||
"event_type": event.event_type.value,
|
||||
"source": event.source,
|
||||
"data": event.data,
|
||||
"timestamp": event.timestamp.isoformat()
|
||||
}
|
||||
|
||||
# Use asyncio to broadcast (need to handle thread safety)
|
||||
asyncio.create_task(self.websocket_manager.broadcast(message))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error broadcasting event: {e}")
|
||||
|
||||
# Subscribe to all event types for broadcasting
|
||||
for event_type in EventType:
|
||||
self.event_system.subscribe(event_type, broadcast_event)
|
||||
|
||||
def start(self) -> 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)
|
||||
}
|
||||
12
usda_vision_system/camera/__init__.py
Normal file
12
usda_vision_system/camera/__init__.py
Normal file
@@ -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"]
|
||||
BIN
usda_vision_system/camera/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
usda_vision_system/camera/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/camera/__pycache__/manager.cpython-311.pyc
Normal file
BIN
usda_vision_system/camera/__pycache__/manager.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/camera/__pycache__/monitor.cpython-311.pyc
Normal file
BIN
usda_vision_system/camera/__pycache__/monitor.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc
Normal file
BIN
usda_vision_system/camera/__pycache__/recorder.cpython-311.pyc
Normal file
Binary file not shown.
320
usda_vision_system/camera/manager.py
Normal file
320
usda_vision_system/camera/manager.py
Normal file
@@ -0,0 +1,320 @@
|
||||
"""
|
||||
Camera Manager for the USDA Vision Camera System.
|
||||
|
||||
This module manages GigE camera discovery, initialization, and coordination
|
||||
with the recording system based on machine state changes.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from datetime import datetime
|
||||
|
||||
# Add python demo to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo'))
|
||||
import mvsdk
|
||||
|
||||
from ..core.config import Config, CameraConfig
|
||||
from ..core.state_manager import StateManager, CameraStatus
|
||||
from ..core.events import EventSystem, EventType, Event, publish_camera_status_changed
|
||||
from ..core.timezone_utils import format_filename_timestamp
|
||||
from .recorder import CameraRecorder
|
||||
from .monitor import CameraMonitor
|
||||
|
||||
|
||||
class CameraManager:
|
||||
"""Manages all cameras in the system"""
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem):
|
||||
self.config = config
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Camera management
|
||||
self.available_cameras: List[Any] = [] # mvsdk camera device info
|
||||
self.camera_recorders: Dict[str, CameraRecorder] = {} # camera_name -> 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
|
||||
267
usda_vision_system/camera/monitor.py
Normal file
267
usda_vision_system/camera/monitor.py
Normal file
@@ -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
|
||||
372
usda_vision_system/camera/recorder.py
Normal file
372
usda_vision_system/camera/recorder.py
Normal file
@@ -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
|
||||
}
|
||||
15
usda_vision_system/core/__init__.py
Normal file
15
usda_vision_system/core/__init__.py
Normal file
@@ -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"]
|
||||
BIN
usda_vision_system/core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
usda_vision_system/core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/core/__pycache__/config.cpython-311.pyc
Normal file
BIN
usda_vision_system/core/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/core/__pycache__/events.cpython-311.pyc
Normal file
BIN
usda_vision_system/core/__pycache__/events.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
207
usda_vision_system/core/config.py
Normal file
207
usda_vision_system/core/config.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Configuration management for the USDA Vision Camera System.
|
||||
|
||||
This module handles all configuration settings including MQTT broker settings,
|
||||
camera configurations, storage paths, and system parameters.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class MQTTConfig:
|
||||
"""MQTT broker configuration"""
|
||||
broker_host: str = "192.168.1.110"
|
||||
broker_port: int = 1883
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
topics: Dict[str, str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.topics is None:
|
||||
self.topics = {
|
||||
"vibratory_conveyor": "vision/vibratory_conveyor/state",
|
||||
"blower_separator": "vision/blower_separator/state"
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class CameraConfig:
|
||||
"""Individual camera configuration"""
|
||||
name: str
|
||||
machine_topic: str # Which MQTT topic triggers this camera
|
||||
storage_path: str
|
||||
exposure_ms: float = 1.0
|
||||
gain: float = 3.5
|
||||
target_fps: float = 3.0
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class StorageConfig:
|
||||
"""Storage configuration"""
|
||||
base_path: str = "/storage"
|
||||
max_file_size_mb: int = 1000 # Max size per video file
|
||||
max_recording_duration_minutes: int = 60 # Max recording duration
|
||||
cleanup_older_than_days: int = 30 # Auto cleanup old files
|
||||
|
||||
|
||||
@dataclass
|
||||
class SystemConfig:
|
||||
"""System-wide configuration"""
|
||||
camera_check_interval_seconds: int = 2
|
||||
log_level: str = "INFO"
|
||||
log_file: str = "usda_vision_system.log"
|
||||
api_host: str = "0.0.0.0"
|
||||
api_port: int = 8000
|
||||
enable_api: bool = True
|
||||
timezone: str = "America/New_York" # Atlanta, Georgia timezone
|
||||
|
||||
|
||||
class Config:
|
||||
"""Main configuration manager"""
|
||||
|
||||
def __init__(self, config_file: Optional[str] = None):
|
||||
self.config_file = config_file or "config.json"
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Default configurations
|
||||
self.mqtt = MQTTConfig()
|
||||
self.storage = StorageConfig()
|
||||
self.system = SystemConfig()
|
||||
|
||||
# Camera configurations - will be populated from config file or defaults
|
||||
self.cameras: List[CameraConfig] = []
|
||||
|
||||
# Load configuration
|
||||
self.load_config()
|
||||
|
||||
# Ensure storage directories exist
|
||||
self._ensure_storage_directories()
|
||||
|
||||
def load_config(self) -> 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]
|
||||
}
|
||||
195
usda_vision_system/core/events.py
Normal file
195
usda_vision_system/core/events.py
Normal file
@@ -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
|
||||
}
|
||||
)
|
||||
260
usda_vision_system/core/logging_config.py
Normal file
260
usda_vision_system/core/logging_config.py
Normal file
@@ -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)
|
||||
328
usda_vision_system/core/state_manager.py
Normal file
328
usda_vision_system/core/state_manager.py
Normal file
@@ -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
|
||||
225
usda_vision_system/core/timezone_utils.py
Normal file
225
usda_vision_system/core/timezone_utils.py
Normal file
@@ -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("=====================================")
|
||||
288
usda_vision_system/main.py
Normal file
288
usda_vision_system/main.py
Normal file
@@ -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()
|
||||
11
usda_vision_system/mqtt/__init__.py
Normal file
11
usda_vision_system/mqtt/__init__.py
Normal file
@@ -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"]
|
||||
BIN
usda_vision_system/mqtt/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
usda_vision_system/mqtt/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/mqtt/__pycache__/client.cpython-311.pyc
Normal file
BIN
usda_vision_system/mqtt/__pycache__/client.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/mqtt/__pycache__/handlers.cpython-311.pyc
Normal file
BIN
usda_vision_system/mqtt/__pycache__/handlers.cpython-311.pyc
Normal file
Binary file not shown.
251
usda_vision_system/mqtt/client.py
Normal file
251
usda_vision_system/mqtt/client.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
MQTT Client for the USDA Vision Camera System.
|
||||
|
||||
This module provides MQTT connectivity and message handling for machine state updates.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, Optional, Callable, List
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from ..core.config import Config, MQTTConfig
|
||||
from ..core.state_manager import StateManager
|
||||
from ..core.events import EventSystem, EventType, publish_machine_state_changed
|
||||
from .handlers import MQTTMessageHandler
|
||||
|
||||
|
||||
class MQTTClient:
|
||||
"""MQTT client for receiving machine state updates"""
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem):
|
||||
self.config = config
|
||||
self.mqtt_config = config.mqtt
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# MQTT client
|
||||
self.client: Optional[mqtt.Client] = None
|
||||
self.connected = False
|
||||
self.running = False
|
||||
|
||||
# Threading
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
# Message handler
|
||||
self.message_handler = MQTTMessageHandler(state_manager, event_system)
|
||||
|
||||
# Connection retry settings
|
||||
self.reconnect_delay = 5 # seconds
|
||||
self.max_reconnect_attempts = 10
|
||||
|
||||
# Topic mapping (topic -> 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
|
||||
153
usda_vision_system/mqtt/handlers.py
Normal file
153
usda_vision_system/mqtt/handlers.py
Normal file
@@ -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
|
||||
9
usda_vision_system/storage/__init__.py
Normal file
9
usda_vision_system/storage/__init__.py
Normal file
@@ -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"]
|
||||
BIN
usda_vision_system/storage/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
usda_vision_system/storage/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
usda_vision_system/storage/__pycache__/manager.cpython-311.pyc
Normal file
BIN
usda_vision_system/storage/__pycache__/manager.cpython-311.pyc
Normal file
Binary file not shown.
349
usda_vision_system/storage/manager.py
Normal file
349
usda_vision_system/storage/manager.py
Normal file
@@ -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
|
||||
850
uv.lock
generated
Normal file
850
uv.lock
generated
Normal file
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user