for assorted things

oof

-10
sandbox/flight-notifier/.dockerignore
··· 1 - *.db 2 - .env 3 - __pycache__/ 4 - *.pyc 5 - .venv/ 6 - .git/ 7 - .gitignore 8 - justfile 9 - DESIGN.md 10 - .DS_Store
-13
sandbox/flight-notifier/.env
··· 1 - HUE_BRIDGE_IP=192.168.0.165 2 - HUE_BRIDGE_USERNAME=5KfCdRdTuTR0F1FgHTNL4E9rmHToRMNUSlfz1IaF 3 - 4 - # BSKY_HANDLE=alternatebuild.dev 5 - # BSKY_PASSWORD=MUSEUM3solarium6bower4sappy 6 - 7 - GITHUB_TOKEN=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 8 - 9 - FLIGHTRADAR_API_TOKEN=019872de-d599-7368-a704-d03532e9ad5f|WvTG09fMDNkfU7lRtXkh4hffoYU6dlRfY0QTdTX1dc12b55a 10 - 11 - # Bluesky credentials 12 - BSKY_HANDLE=phi.alternatebuild.dev 13 - BSKY_PASSWORD=cxha-k3ss-jhde-maj4
-253
sandbox/flight-notifier/DESIGN.md
··· 1 - # Flight Notifier Web App Design 2 - 3 - ## Overview 4 - 5 - A browser-based progressive web app that monitors flights overhead using device location and sends notifications via BlueSky DMs. Quick MVP for ngrok deployment with eventual path to production. 6 - 7 - ## Architecture 8 - 9 - ### MVP (ngrok deployment) 10 - ``` 11 - Browser (Geolocation) → FastAPI → FlightRadar24 API 12 - 13 - BlueSky DMs 14 - ``` 15 - 16 - ### Core Components 17 - 18 - 1. **Browser Frontend** 19 - - Single HTML page with vanilla JS (keep it simple) 20 - - Geolocation API for real-time position 21 - - WebSocket or polling for updates 22 - - Service Worker for background checks (future) 23 - - Local notifications + BlueSky DM option 24 - 25 - 2. **FastAPI Backend** 26 - - `/api/check-flights` - POST with lat/lon, returns flights 27 - - `/api/subscribe` - WebSocket endpoint for live updates 28 - - `/api/notify` - Send BlueSky DM for specific flight 29 - - Reuses existing flight monitoring script logic 30 - 31 - 3. **Deployment Strategy** 32 - - Phase 1: ngrok + local FastAPI (immediate) 33 - - Phase 2: Fly.io with proper auth (1-2 weeks) 34 - - Phase 3: PWA with service workers (1 month) 35 - 36 - ## User Experience 37 - 38 - ### MVP Flow 39 - 1. User visits ngrok URL 40 - 2. Browser asks for location permission 41 - 3. Big "Check Flights" button + auto-check toggle 42 - 4. Shows nearby flights with "Send DM" buttons 43 - 5. Enter BlueSky handle once, saved in localStorage 44 - 45 - ### Future Enhancements 46 - - Background notifications (requires HTTPS + service worker) 47 - - Flight filtering UI 48 - - Custom notification templates 49 - - Flight history/tracking 50 - - Multiple notification channels 51 - 52 - ## Technical Decisions 53 - 54 - ### Why Browser First? 55 - - Instant deployment, no app store 56 - - Geolocation API is mature 57 - - Works on all devices 58 - - Progressive enhancement path 59 - - Can add native later 60 - 61 - ### Why ngrok for MVP? 62 - - Zero deployment complexity 63 - - HTTPS for geolocation 64 - - Share with testers immediately 65 - - Iterate quickly 66 - 67 - ### Data Flow 68 - ```javascript 69 - // Browser 70 - navigator.geolocation.watchPosition(async (pos) => { 71 - const flights = await fetch('/api/check-flights', { 72 - method: 'POST', 73 - body: JSON.stringify({ 74 - latitude: pos.coords.latitude, 75 - longitude: pos.coords.longitude, 76 - radius_miles: 5 77 - }) 78 - }); 79 - // Update UI 80 - }); 81 - ``` 82 - 83 - ## Implementation Plan 84 - 85 - ### Today (MVP) 86 - 1. ✅ Create sandbox structure 87 - 2. Build minimal FastAPI server 88 - 3. HTML page with geolocation 89 - 4. Deploy with ngrok 90 - 5. Test with friends 91 - 92 - ### This Week 93 - 1. Add WebSocket for live updates 94 - 2. Better flight filtering UI 95 - 3. Notification preferences 96 - 4. Deploy to Fly.io 97 - 98 - ### Next Month 99 - 1. Service worker for background 100 - 2. Push notifications 101 - 3. Flight tracking/history 102 - 4. iOS/Android PWA polish 103 - 104 - ## Security Considerations (Post-MVP) 105 - 106 - ### Rate Limiting 107 - ```python 108 - # Per-user limits 109 - user_limits = { 110 - "checks_per_minute": 10, 111 - "dms_per_hour": 20, 112 - "websocket_connections": 1 113 - } 114 - ``` 115 - 116 - ### Authentication 117 - - BlueSky OAuth for production 118 - - Validate handle ownership 119 - - API keys for power users 120 - 121 - ### Privacy 122 - - Don't store location history 123 - - Clear position data on disconnect 124 - - GDPR-compliant data handling 125 - 126 - ## API Design 127 - 128 - ### Check Flights 129 - ``` 130 - POST /api/check-flights 131 - { 132 - "latitude": 41.8781, 133 - "longitude": -87.6298, 134 - "radius_miles": 5, 135 - "filters": { 136 - "aircraft_type": ["B737"] 137 - } 138 - } 139 - 140 - Response: 141 - { 142 - "flights": [ 143 - { 144 - "id": "abc123", 145 - "callsign": "UAL123", 146 - "aircraft_type": "B737", 147 - "distance_miles": 2.5, 148 - "altitude": 15000, 149 - "heading": 270 150 - } 151 - ] 152 - } 153 - ``` 154 - 155 - ### Send Notification 156 - ``` 157 - POST /api/notify 158 - { 159 - "flight_id": "abc123", 160 - "bsky_handle": "user.bsky.social", 161 - "template": "custom" 162 - } 163 - ``` 164 - 165 - ### WebSocket Subscribe 166 - ``` 167 - WS /api/subscribe 168 - → {"latitude": 41.8781, "longitude": -87.6298} 169 - ← {"type": "flight", "data": {...}} 170 - ← {"type": "flight_exit", "id": "abc123"} 171 - ``` 172 - 173 - ## Scaling Considerations 174 - 175 - ### Caching Strategy 176 - - Cache FlightRadar responses (15-30s) 177 - - Group nearby users for batch queries 178 - - Redis for shared state 179 - 180 - ### Database Schema (Future) 181 - ```sql 182 - CREATE TABLE users ( 183 - id UUID PRIMARY KEY, 184 - bsky_handle TEXT UNIQUE, 185 - created_at TIMESTAMP 186 - ); 187 - 188 - CREATE TABLE subscriptions ( 189 - user_id UUID REFERENCES users, 190 - latitude FLOAT, 191 - longitude FLOAT, 192 - radius_miles FLOAT, 193 - filters JSONB, 194 - active BOOLEAN 195 - ); 196 - 197 - CREATE TABLE notifications ( 198 - id UUID PRIMARY KEY, 199 - user_id UUID REFERENCES users, 200 - flight_id TEXT, 201 - sent_at TIMESTAMP 202 - ); 203 - ``` 204 - 205 - ## Development Notes 206 - 207 - ### Local Setup 208 - ```bash 209 - cd sandbox/flight-notifier 210 - uvicorn app:app --reload 211 - ngrok http 8000 212 - ``` 213 - 214 - ### Environment Variables 215 - ``` 216 - BSKY_HANDLE=bot.handle 217 - BSKY_PASSWORD=xxx 218 - FLIGHTRADAR_API_TOKEN=xxx 219 - ``` 220 - 221 - ### Testing 222 - - Use browser dev tools for geo spoofing 223 - - Test with multiple simultaneous users 224 - - Verify rate limits work 225 - - Check mobile experience 226 - 227 - ## Future Ideas 228 - 229 - ### iOS Shortcuts Integration 230 - ```javascript 231 - // Expose webhook for Shortcuts 232 - POST /api/shortcuts/check 233 - { 234 - "latitude": 41.8781, 235 - "longitude": -87.6298, 236 - "shortcut_callback": "shortcuts://callback" 237 - } 238 - ``` 239 - 240 - ### Flight Prediction 241 - - Learn user patterns 242 - - Notify before overhead 243 - - "Your usual 5pm flight approaching" 244 - 245 - ### Social Features 246 - - Share interesting flights 247 - - Local plane spotter groups 248 - - Flight photo integration 249 - 250 - ### Gamification 251 - - Spot rare aircraft 252 - - Track unique registrations 253 - - Monthly leaderboards
-32
sandbox/flight-notifier/Dockerfile
··· 1 - # Use Python 3.12 slim image 2 - FROM python:3.12-slim 3 - 4 - # Install system dependencies 5 - RUN apt-get update && apt-get install -y \ 6 - curl \ 7 - && rm -rf /var/lib/apt/lists/* 8 - 9 - # Install UV 10 - RUN curl -LsSf https://astral.sh/uv/install.sh | sh 11 - ENV PATH="/root/.cargo/bin:$PATH" 12 - 13 - # Set working directory 14 - WORKDIR /app 15 - 16 - # Copy project files 17 - COPY pyproject.toml uv.lock ./ 18 - COPY src/ ./src/ 19 - COPY static/ ./static/ 20 - 21 - # Install dependencies with UV 22 - RUN uv sync --frozen --no-dev 23 - 24 - # Create non-root user 25 - RUN useradd -m -u 1001 appuser && chown -R appuser:appuser /app 26 - USER appuser 27 - 28 - # Expose port 29 - EXPOSE 8000 30 - 31 - # Run the application 32 - CMD ["uv", "run", "uvicorn", "flight_notifier.main:app", "--host", "0.0.0.0", "--port", "8000"]
-69
sandbox/flight-notifier/README.md
··· 1 - # Flight Notifier Web App 2 - 3 - Browser-based flight monitoring with BlueSky DM notifications. 4 - 5 - ## Quick Start 6 - 7 - ```bash 8 - # Install dependencies 9 - just install 10 - 11 - # Run the app 12 - just dev 13 - 14 - # In another terminal, expose via ngrok 15 - just ngrok 16 - 17 - # Share the ngrok URL with testers! 18 - ``` 19 - 20 - ## How it Works 21 - 22 - 1. Browser asks for location permission 23 - 2. Click "Check Flights Now" or enable auto-check 24 - 3. Shows nearby flights with details 25 - 4. Click "Send BlueSky DM" to get notified 26 - 27 - ## Features 28 - 29 - - 🗺️ Real-time geolocation 30 - - ✈️ Live flight data from FlightRadar24 31 - - 📱 Mobile-friendly interface 32 - - 🔔 Browser notifications for new flights 33 - - 💬 BlueSky DM integration 34 - - 🔄 Auto-refresh every 30 seconds 35 - 36 - ## Development 37 - 38 - ```bash 39 - # Format code 40 - just fmt 41 - 42 - # Run linter 43 - just lint 44 - ``` 45 - 46 - ## Environment Variables 47 - 48 - Create a `.env` file: 49 - 50 - ``` 51 - BSKY_HANDLE=your-bot.bsky.social 52 - BSKY_PASSWORD=your-app-password 53 - FLIGHTRADAR_API_TOKEN=your-token 54 - ``` 55 - 56 - ## Architecture 57 - 58 - - FastAPI backend (`src/flight_notifier/main.py`) 59 - - Vanilla JS frontend (`static/index.html`) 60 - - Reuses flight monitoring logic from parent script 61 - - Ready for deployment to Fly.io or similar 62 - 63 - ## Future Enhancements 64 - 65 - - Service Workers for background monitoring 66 - - Push notifications 67 - - Flight filtering UI 68 - - User accounts & preferences 69 - - WebSocket for real-time updates
sandbox/flight-notifier/flight_notifier.db

This is a binary file and will not be displayed.

-29
sandbox/flight-notifier/fly.toml
··· 1 - # fly.toml app configuration file 2 - # 3 - # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 - # 5 - 6 - app = "flight-notifier" 7 - primary_region = "ord" 8 - 9 - [build] 10 - 11 - [http_service] 12 - internal_port = 8000 13 - force_https = true 14 - auto_stop_machines = "stop" 15 - auto_start_machines = true 16 - min_machines_running = 0 17 - processes = ["app"] 18 - 19 - [[vm]] 20 - size = "shared-cpu-1x" 21 - memory = "512mb" 22 - 23 - [env] 24 - PORT = "8000" 25 - 26 - [[mounts]] 27 - source = "flight_notifier_data" 28 - destination = "/app/data" 29 - processes = ["app"]
-17
sandbox/flight-notifier/justfile
··· 1 - # Core development commands 2 - dev: 3 - uv run uvicorn src.flight_notifier.main:app --reload 4 - 5 - ngrok: 6 - ngrok http 8000 7 - 8 - install: 9 - uv sync 10 - 11 - fmt: 12 - uv run ruff format src/ 13 - 14 - lint: 15 - uv run ruff check src/ 16 - 17 - check: lint
-31
sandbox/flight-notifier/pyproject.toml
··· 1 - [project] 2 - name = "flight-notifier" 3 - version = "0.1.0" 4 - description = "Browser-based flight monitoring with BlueSky notifications" 5 - readme = "README.md" 6 - authors = [{ name = "alternatebuild.dev" }] 7 - requires-python = ">=3.12" 8 - dependencies = [ 9 - "atproto", 10 - "fastapi", 11 - "geopy", 12 - "httpx", 13 - "jinja2", 14 - "pydantic-settings", 15 - "uvicorn", 16 - "sqlalchemy", 17 - "alembic", 18 - "asyncpg", 19 - "python-jose[cryptography]", 20 - "python-multipart", 21 - "apscheduler", 22 - ] 23 - 24 - [tool.uv] 25 - dev-dependencies = [ 26 - "ruff", 27 - ] 28 - 29 - [build-system] 30 - requires = ["hatchling"] 31 - build-backend = "hatchling.build"
-3
sandbox/flight-notifier/server.log
··· 1 - warning: `VIRTUAL_ENV=/Users/nate/tangled.sh/@alternatebuild.dev/scripts/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead 2 - INFO: Will watch for changes in these directories: ['/Users/nate/tangled.sh/@alternatebuild.dev/scripts/sandbox/flight-notifier'] 3 - ERROR: [Errno 48] Address already in use
-3
sandbox/flight-notifier/src/flight_notifier/__init__.py
··· 1 - """Flight notifier web application.""" 2 - 3 - __version__ = "0.1.0"
sandbox/flight-notifier/src/flight_notifier/__pycache__/__init__.cpython-312.pyc

This is a binary file and will not be displayed.

sandbox/flight-notifier/src/flight_notifier/__pycache__/flight_monitor.cpython-312.pyc

This is a binary file and will not be displayed.

sandbox/flight-notifier/src/flight_notifier/__pycache__/main.cpython-312.pyc

This is a binary file and will not be displayed.

-94
sandbox/flight-notifier/src/flight_notifier/auth.py
··· 1 - """BlueSky OAuth and authentication.""" 2 - 3 - from datetime import datetime, timedelta, timezone 4 - from typing import Optional 5 - 6 - from fastapi import Depends, HTTPException, status 7 - from fastapi.security import OAuth2PasswordBearer 8 - from jose import JWTError, jwt 9 - from pydantic import BaseModel 10 - from sqlalchemy.orm import Session 11 - 12 - from flight_notifier.database import User, get_db 13 - 14 - import secrets 15 - 16 - # TODO: Move to settings 17 - SECRET_KEY = secrets.token_urlsafe(32) 18 - ALGORITHM = "HS256" 19 - ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 1 week 20 - 21 - oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 22 - 23 - 24 - class Token(BaseModel): 25 - access_token: str 26 - token_type: str 27 - 28 - 29 - class TokenData(BaseModel): 30 - bsky_handle: str | None = None 31 - 32 - 33 - class UserCreate(BaseModel): 34 - bsky_handle: str 35 - bsky_did: str 36 - 37 - 38 - def create_access_token(data: dict, expires_delta: timedelta | None = None): 39 - """Create JWT access token.""" 40 - to_encode = data.copy() 41 - if expires_delta: 42 - expire = datetime.now(timezone.utc) + expires_delta 43 - else: 44 - expire = datetime.now(timezone.utc) + timedelta(minutes=15) 45 - to_encode.update({"exp": expire}) 46 - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 47 - return encoded_jwt 48 - 49 - 50 - async def get_current_user( 51 - token: str = Depends(oauth2_scheme), 52 - db: Session = Depends(get_db) 53 - ) -> User: 54 - """Get current user from JWT token.""" 55 - credentials_exception = HTTPException( 56 - status_code=status.HTTP_401_UNAUTHORIZED, 57 - detail="Could not validate credentials", 58 - headers={"WWW-Authenticate": "Bearer"}, 59 - ) 60 - 61 - try: 62 - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 63 - bsky_handle: str = payload.get("sub") 64 - if bsky_handle is None: 65 - raise credentials_exception 66 - token_data = TokenData(bsky_handle=bsky_handle) 67 - except JWTError: 68 - raise credentials_exception 69 - 70 - user = db.query(User).filter(User.bsky_handle == token_data.bsky_handle).first() 71 - if user is None: 72 - raise credentials_exception 73 - 74 - # Update last seen 75 - user.last_seen = datetime.now(timezone.utc) 76 - db.commit() 77 - 78 - return user 79 - 80 - 81 - def get_or_create_user(db: Session, user_create: UserCreate) -> User: 82 - """Get existing user or create new one.""" 83 - user = db.query(User).filter(User.bsky_handle == user_create.bsky_handle).first() 84 - 85 - if not user: 86 - user = User( 87 - bsky_handle=user_create.bsky_handle, 88 - bsky_did=user_create.bsky_did 89 - ) 90 - db.add(user) 91 - db.commit() 92 - db.refresh(user) 93 - 94 - return user
-78
sandbox/flight-notifier/src/flight_notifier/database.py
··· 1 - """Database models and session management.""" 2 - 3 - from datetime import datetime, timezone 4 - from typing import Any 5 - 6 - from sqlalchemy import JSON, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine 7 - from sqlalchemy.ext.declarative import declarative_base 8 - from sqlalchemy.orm import relationship, sessionmaker 9 - 10 - Base = declarative_base() 11 - 12 - 13 - class User(Base): 14 - __tablename__ = "users" 15 - 16 - id = Column(Integer, primary_key=True) 17 - bsky_handle = Column(String, unique=True, nullable=False, index=True) 18 - bsky_did = Column(String, unique=True, nullable=False) 19 - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) 20 - last_seen = Column(DateTime, default=lambda: datetime.now(timezone.utc)) 21 - 22 - subscriptions = relationship("Subscription", back_populates="user", cascade="all, delete-orphan") 23 - 24 - 25 - class Subscription(Base): 26 - __tablename__ = "subscriptions" 27 - 28 - id = Column(Integer, primary_key=True) 29 - user_id = Column(Integer, ForeignKey("users.id"), nullable=False) 30 - latitude = Column(Float, nullable=False) 31 - longitude = Column(Float, nullable=False) 32 - radius_miles = Column(Float, default=5.0) 33 - filters = Column(JSON, default=dict) 34 - message_template = Column(String, nullable=True) 35 - active = Column(Boolean, default=True) 36 - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) 37 - updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) 38 - 39 - user = relationship("User", back_populates="subscriptions") 40 - notifications = relationship("Notification", back_populates="subscription", cascade="all, delete-orphan") 41 - 42 - 43 - class Notification(Base): 44 - __tablename__ = "notifications" 45 - 46 - id = Column(Integer, primary_key=True) 47 - subscription_id = Column(Integer, ForeignKey("subscriptions.id"), nullable=False) 48 - flight_id = Column(String, nullable=False) 49 - sent_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) 50 - flight_data = Column(JSON) 51 - 52 - subscription = relationship("Subscription", back_populates="notifications") 53 - 54 - # Index to prevent duplicate notifications 55 - __table_args__ = ( 56 - {"sqlite_autoincrement": True}, 57 - ) 58 - 59 - 60 - # Database setup 61 - DATABASE_URL = "sqlite:///./flight_notifier.db" # Use PostgreSQL in production 62 - 63 - engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) 64 - SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 65 - 66 - 67 - def get_db(): 68 - """Dependency to get database session.""" 69 - db = SessionLocal() 70 - try: 71 - yield db 72 - finally: 73 - db.close() 74 - 75 - 76 - def init_db(): 77 - """Initialize database tables.""" 78 - Base.metadata.create_all(bind=engine)
-473
sandbox/flight-notifier/src/flight_notifier/flight_monitor.py
··· 1 - """Flight monitoring utilities for BlueSky notifications.""" 2 - 3 - import argparse 4 - import time 5 - import math 6 - import json 7 - import sys 8 - from datetime import datetime 9 - from concurrent.futures import ThreadPoolExecutor, as_completed 10 - 11 - import httpx 12 - from atproto import Client 13 - from geopy import distance 14 - from jinja2 import Template 15 - from pydantic import BaseModel, Field 16 - from pydantic_settings import BaseSettings, SettingsConfigDict 17 - 18 - 19 - 20 - class Settings(BaseSettings): 21 - """App settings loaded from environment variables""" 22 - 23 - model_config = SettingsConfigDict(env_file=".env", extra="ignore") 24 - 25 - bsky_handle: str = Field(...) 26 - bsky_password: str = Field(...) 27 - flightradar_api_token: str = Field(...) 28 - 29 - 30 - class Subscriber(BaseModel): 31 - """Subscriber with location and notification preferences""" 32 - 33 - handle: str 34 - latitude: float 35 - longitude: float 36 - radius_miles: float = 5.0 37 - filters: dict[str, list[str]] = Field(default_factory=dict) 38 - message_template: str | None = None 39 - 40 - 41 - class Flight(BaseModel): 42 - """Flight data model""" 43 - 44 - hex: str 45 - latitude: float 46 - longitude: float 47 - altitude: float | None = None 48 - ground_speed: float | None = None 49 - heading: float | None = None 50 - aircraft_type: str | None = None 51 - registration: str | None = None 52 - origin: str | None = None 53 - destination: str | None = None 54 - callsign: str | None = None 55 - distance_miles: float 56 - 57 - 58 - def get_flights_in_area( 59 - settings: Settings, latitude: float, longitude: float, radius_miles: float 60 - ) -> list[Flight]: 61 - """Get flights within the specified radius using FlightRadar24 API.""" 62 - lat_offset = radius_miles / 69 # 1 degree latitude ≈ 69 miles 63 - lon_offset = radius_miles / (69 * abs(math.cos(math.radians(latitude)))) 64 - 65 - bounds = { 66 - "north": latitude + lat_offset, 67 - "south": latitude - lat_offset, 68 - "west": longitude - lon_offset, 69 - "east": longitude + lon_offset, 70 - } 71 - 72 - headers = { 73 - "Authorization": f"Bearer {settings.flightradar_api_token}", 74 - "Accept": "application/json", 75 - "Accept-Version": "v1", 76 - } 77 - 78 - url = "https://fr24api.flightradar24.com/api/live/flight-positions/full" 79 - params = { 80 - "bounds": f"{bounds['north']},{bounds['south']},{bounds['west']},{bounds['east']}" 81 - } 82 - 83 - try: 84 - with httpx.Client() as client: 85 - response = client.get(url, headers=headers, params=params, timeout=10) 86 - response.raise_for_status() 87 - data = response.json() 88 - 89 - flights_in_radius = [] 90 - center = (latitude, longitude) 91 - 92 - if isinstance(data, dict) and "data" in data: 93 - for flight_data in data["data"]: 94 - lat = flight_data.get("lat") 95 - lon = flight_data.get("lon") 96 - 97 - if lat and lon: 98 - flight_pos = (lat, lon) 99 - dist = distance.distance(center, flight_pos).miles 100 - if dist <= radius_miles: 101 - flight = Flight( 102 - hex=flight_data.get("fr24_id", ""), 103 - latitude=lat, 104 - longitude=lon, 105 - altitude=flight_data.get("alt"), 106 - ground_speed=flight_data.get("gspeed"), 107 - heading=flight_data.get("track"), 108 - aircraft_type=flight_data.get("type"), 109 - registration=flight_data.get("reg"), 110 - origin=flight_data.get("orig_iata"), 111 - destination=flight_data.get("dest_iata"), 112 - callsign=flight_data.get("flight"), 113 - distance_miles=round(dist, 2), 114 - ) 115 - flights_in_radius.append(flight) 116 - 117 - return flights_in_radius 118 - except httpx.HTTPStatusError as e: 119 - print(f"HTTP error fetching flights: {e}") 120 - print(f"Response status: {e.response.status_code}") 121 - print(f"Response content: {e.response.text[:500]}") 122 - return [] 123 - except Exception as e: 124 - print(f"Error fetching flights: {e}") 125 - return [] 126 - 127 - 128 - DEFAULT_MESSAGE_TEMPLATE = """✈️ Flight passing overhead! 129 - 130 - Flight: {{ flight.callsign or 'Unknown' }} 131 - Distance: {{ flight.distance_miles }} miles 132 - {%- if flight.altitude %} 133 - Altitude: {{ "{:,.0f}".format(flight.altitude) }} ft 134 - {%- endif %} 135 - {%- if flight.ground_speed %} 136 - Speed: {{ "{:.0f}".format(flight.ground_speed) }} kts 137 - {%- endif %} 138 - {%- if flight.heading %} 139 - Heading: {{ "{:.0f}".format(flight.heading) }}° 140 - {%- endif %} 141 - {%- if flight.aircraft_type %} 142 - Aircraft: {{ flight.aircraft_type }} 143 - {%- endif %} 144 - {%- if flight.origin or flight.destination %} 145 - Route: {{ flight.origin or '???' }} → {{ flight.destination or '???' }} 146 - {%- endif %} 147 - 148 - Time: {{ timestamp }}""" 149 - 150 - 151 - def format_flight_info(flight: Flight, template_str: str | None = None) -> str: 152 - """Format flight information for a DM using Jinja2 template.""" 153 - template_str = template_str or DEFAULT_MESSAGE_TEMPLATE 154 - template = Template(template_str) 155 - 156 - return template.render( 157 - flight=flight, 158 - timestamp=datetime.now().strftime('%H:%M:%S') 159 - ) 160 - 161 - 162 - def send_dm(client: Client, message: str, target_handle: str) -> bool: 163 - """Send a direct message to the specified handle on BlueSky.""" 164 - try: 165 - resolved = client.com.atproto.identity.resolve_handle( 166 - params={"handle": target_handle} 167 - ) 168 - target_did = resolved.did 169 - 170 - chat_client = client.with_bsky_chat_proxy() 171 - 172 - convo_response = chat_client.chat.bsky.convo.get_convo_for_members( 173 - {"members": [target_did]} 174 - ) 175 - 176 - if not convo_response or not convo_response.convo: 177 - print(f"Could not create/get conversation with {target_handle}") 178 - return False 179 - 180 - recipient = None 181 - for member in convo_response.convo.members: 182 - if member.did != client.me.did: 183 - recipient = member 184 - break 185 - 186 - if not recipient or recipient.handle != target_handle: 187 - print( 188 - f"ERROR: About to message wrong person! Expected {target_handle}, but found {recipient.handle if recipient else 'no recipient'}" 189 - ) 190 - return False 191 - 192 - chat_client.chat.bsky.convo.send_message( 193 - data={ 194 - "convoId": convo_response.convo.id, 195 - "message": {"text": message, "facets": []}, 196 - } 197 - ) 198 - 199 - print(f"DM sent to {target_handle}") 200 - return True 201 - 202 - except Exception as e: 203 - print(f"Error sending DM to {target_handle}: {e}") 204 - return False 205 - 206 - 207 - def flight_matches_filters(flight: Flight, filters: dict[str, list[str]]) -> bool: 208 - """Check if a flight matches the subscriber's filters.""" 209 - if not filters: 210 - return True 211 - 212 - for field, allowed_values in filters.items(): 213 - if not allowed_values: 214 - continue 215 - 216 - flight_value = getattr(flight, field, None) 217 - if flight_value is None: 218 - return False 219 - 220 - if field == "aircraft_type": 221 - # Case-insensitive partial matching for aircraft types 222 - flight_value_lower = str(flight_value).lower() 223 - if not any(allowed.lower() in flight_value_lower for allowed in allowed_values): 224 - return False 225 - else: 226 - # Exact matching for other fields 227 - if str(flight_value) not in [str(v) for v in allowed_values]: 228 - return False 229 - 230 - return True 231 - 232 - 233 - def process_subscriber( 234 - client: Client, 235 - settings: Settings, 236 - subscriber: Subscriber, 237 - notified_flights: dict[str, set[str]], 238 - ) -> None: 239 - """Process flights for a single subscriber.""" 240 - try: 241 - flights = get_flights_in_area( 242 - settings, subscriber.latitude, subscriber.longitude, subscriber.radius_miles 243 - ) 244 - 245 - if subscriber.handle not in notified_flights: 246 - notified_flights[subscriber.handle] = set() 247 - 248 - subscriber_notified = notified_flights[subscriber.handle] 249 - filtered_count = 0 250 - 251 - for flight in flights: 252 - flight_id = flight.hex 253 - 254 - if not flight_matches_filters(flight, subscriber.filters): 255 - filtered_count += 1 256 - continue 257 - 258 - if flight_id not in subscriber_notified: 259 - message = format_flight_info(flight, subscriber.message_template) 260 - print(f"\n[{subscriber.handle}] {message}\n") 261 - 262 - if send_dm(client, message, subscriber.handle): 263 - print(f"DM sent to {subscriber.handle} for flight {flight_id}") 264 - subscriber_notified.add(flight_id) 265 - else: 266 - print( 267 - f"Failed to send DM to {subscriber.handle} for flight {flight_id}" 268 - ) 269 - 270 - current_flight_ids = {f.hex for f in flights} 271 - notified_flights[subscriber.handle] &= current_flight_ids 272 - 273 - if not flights: 274 - print( 275 - f"[{subscriber.handle}] No flights in range at {datetime.now().strftime('%H:%M:%S')}" 276 - ) 277 - elif filtered_count > 0 and filtered_count == len(flights): 278 - print( 279 - f"[{subscriber.handle}] {filtered_count} flights filtered out at {datetime.now().strftime('%H:%M:%S')}" 280 - ) 281 - 282 - except Exception as e: 283 - print(f"Error processing subscriber {subscriber.handle}: {e}") 284 - 285 - 286 - def load_subscribers(subscribers_input: str | None) -> list[Subscriber]: 287 - """Load subscribers from JSON file or stdin.""" 288 - if subscribers_input: 289 - with open(subscribers_input, "r") as f: 290 - data = json.load(f) 291 - else: 292 - print("Reading subscriber data from stdin (provide JSON array)...") 293 - data = json.load(sys.stdin) 294 - 295 - return [Subscriber(**item) for item in data] 296 - 297 - 298 - def main(): 299 - """Main monitoring loop.""" 300 - parser = argparse.ArgumentParser( 301 - description="Monitor flights overhead and send BlueSky DMs" 302 - ) 303 - 304 - parser.add_argument( 305 - "--subscribers", 306 - type=str, 307 - help="JSON file with subscriber list, or '-' for stdin", 308 - ) 309 - parser.add_argument( 310 - "--latitude", type=float, default=41.8781, help="Latitude (default: Chicago)" 311 - ) 312 - parser.add_argument( 313 - "--longitude", type=float, default=-87.6298, help="Longitude (default: Chicago)" 314 - ) 315 - parser.add_argument( 316 - "--radius", type=float, default=5.0, help="Radius in miles (default: 5)" 317 - ) 318 - parser.add_argument( 319 - "--handle", 320 - type=str, 321 - default="alternatebuild.dev", 322 - help="BlueSky handle to DM (default: alternatebuild.dev)", 323 - ) 324 - parser.add_argument( 325 - "--filter-aircraft-type", 326 - type=str, 327 - nargs="+", 328 - help="Filter by aircraft types (e.g., B737 A320 C172)", 329 - ) 330 - parser.add_argument( 331 - "--filter-callsign", 332 - type=str, 333 - nargs="+", 334 - help="Filter by callsigns (e.g., UAL DL AAL)", 335 - ) 336 - parser.add_argument( 337 - "--filter-origin", 338 - type=str, 339 - nargs="+", 340 - help="Filter by origin airports (e.g., ORD LAX JFK)", 341 - ) 342 - parser.add_argument( 343 - "--filter-destination", 344 - type=str, 345 - nargs="+", 346 - help="Filter by destination airports (e.g., ORD LAX JFK)", 347 - ) 348 - parser.add_argument( 349 - "--message-template", 350 - type=str, 351 - help="Custom Jinja2 template for messages", 352 - ) 353 - parser.add_argument( 354 - "--message-template-file", 355 - type=str, 356 - help="Path to file containing custom Jinja2 template", 357 - ) 358 - parser.add_argument( 359 - "--interval", 360 - type=int, 361 - default=60, 362 - help="Check interval in seconds (default: 60)", 363 - ) 364 - parser.add_argument( 365 - "--once", action="store_true", help="Run once and exit (for testing)" 366 - ) 367 - parser.add_argument( 368 - "--max-workers", 369 - type=int, 370 - default=5, 371 - help="Max concurrent workers for processing subscribers (default: 5)", 372 - ) 373 - args = parser.parse_args() 374 - 375 - try: 376 - settings = Settings() 377 - except Exception as e: 378 - print(f"Error loading settings: {e}") 379 - print( 380 - "Ensure .env file exists with BSKY_HANDLE, BSKY_PASSWORD, and FLIGHTRADAR_API_TOKEN" 381 - ) 382 - return 383 - 384 - client = Client() 385 - try: 386 - client.login(settings.bsky_handle, settings.bsky_password) 387 - print(f"Logged in to BlueSky as {settings.bsky_handle}") 388 - except Exception as e: 389 - print(f"Error logging into BlueSky: {e}") 390 - return 391 - 392 - if args.subscribers: 393 - if args.subscribers == "-": 394 - subscribers_input = None 395 - else: 396 - subscribers_input = args.subscribers 397 - 398 - try: 399 - subscribers = load_subscribers(subscribers_input) 400 - print(f"Loaded {len(subscribers)} subscriber(s)") 401 - except Exception as e: 402 - print(f"Error loading subscribers: {e}") 403 - return 404 - else: 405 - # Build filters from CLI args 406 - filters = {} 407 - if args.filter_aircraft_type: 408 - filters["aircraft_type"] = args.filter_aircraft_type 409 - if args.filter_callsign: 410 - filters["callsign"] = args.filter_callsign 411 - if args.filter_origin: 412 - filters["origin"] = args.filter_origin 413 - if args.filter_destination: 414 - filters["destination"] = args.filter_destination 415 - 416 - # Load custom template if provided 417 - message_template = None 418 - if args.message_template_file: 419 - with open(args.message_template_file, "r") as f: 420 - message_template = f.read() 421 - elif args.message_template: 422 - message_template = args.message_template 423 - 424 - subscribers = [ 425 - Subscriber( 426 - handle=args.handle, 427 - latitude=args.latitude, 428 - longitude=args.longitude, 429 - radius_miles=args.radius, 430 - filters=filters, 431 - message_template=message_template, 432 - ) 433 - ] 434 - print( 435 - f"Monitoring flights within {args.radius} miles of ({args.latitude}, {args.longitude}) for {args.handle}" 436 - ) 437 - if filters: 438 - print(f"Active filters: {filters}") 439 - 440 - print(f"Checking every {args.interval} seconds...") 441 - 442 - notified_flights: dict[str, set[str]] = {} 443 - 444 - while True: 445 - try: 446 - with ThreadPoolExecutor(max_workers=args.max_workers) as executor: 447 - futures = [] 448 - for subscriber in subscribers: 449 - future = executor.submit( 450 - process_subscriber, 451 - client, 452 - settings, 453 - subscriber, 454 - notified_flights, 455 - ) 456 - futures.append(future) 457 - 458 - for future in as_completed(futures): 459 - future.result() 460 - 461 - if args.once: 462 - break 463 - 464 - time.sleep(args.interval) 465 - 466 - except KeyboardInterrupt: 467 - print("\nStopping flight monitor...") 468 - break 469 - except Exception as e: 470 - print(f"Error in monitoring loop: {e}") 471 - time.sleep(args.interval) 472 - 473 -
-342
sandbox/flight-notifier/src/flight_notifier/main.py
··· 1 - """FastAPI backend for flight notifier web app.""" 2 - 3 - import logging 4 - from contextlib import asynccontextmanager 5 - from datetime import datetime, timedelta, timezone 6 - 7 - logging.basicConfig(level=logging.INFO) 8 - 9 - from atproto import Client 10 - from fastapi import Depends, FastAPI, HTTPException, status 11 - from fastapi.middleware.cors import CORSMiddleware 12 - from fastapi.responses import HTMLResponse 13 - from fastapi.security import OAuth2PasswordRequestForm 14 - from pydantic import BaseModel, Field 15 - from sqlalchemy.orm import Session 16 - 17 - from flight_notifier.auth import ( 18 - ACCESS_TOKEN_EXPIRE_MINUTES, 19 - Token, 20 - UserCreate, 21 - create_access_token, 22 - get_current_user, 23 - get_or_create_user, 24 - ) 25 - from flight_notifier.database import Subscription, User, get_db, init_db 26 - from flight_notifier.flight_monitor import ( 27 - Flight, 28 - Settings, 29 - flight_matches_filters, 30 - format_flight_info, 31 - get_flights_in_area, 32 - send_dm, 33 - ) 34 - from flight_notifier.worker import SubscriptionProcessor 35 - 36 - 37 - @asynccontextmanager 38 - async def lifespan(app: FastAPI): 39 - """Application lifespan manager.""" 40 - # Initialize database 41 - init_db() 42 - 43 - # Start background worker 44 - app.state.processor = SubscriptionProcessor(settings, bsky_client) 45 - app.state.processor.start() 46 - 47 - yield 48 - 49 - # Stop background worker 50 - app.state.processor.stop() 51 - 52 - 53 - app = FastAPI(title="Flight Notifier", lifespan=lifespan) 54 - 55 - # Enable CORS for development 56 - app.add_middleware( 57 - CORSMiddleware, 58 - allow_origins=["*"], 59 - allow_credentials=True, 60 - allow_methods=["*"], 61 - allow_headers=["*"], 62 - ) 63 - 64 - # Global state (TODO: use Redis) 65 - settings = Settings() 66 - bsky_client = Client() 67 - try: 68 - bsky_client.login(settings.bsky_handle, settings.bsky_password) 69 - print(f"Logged in to BlueSky as {settings.bsky_handle}") 70 - except Exception as e: 71 - print(f"Warning: Could not login to BlueSky: {e}") 72 - bsky_client = None 73 - 74 - 75 - class CheckFlightsRequest(BaseModel): 76 - latitude: float 77 - longitude: float 78 - radius_miles: float = 5.0 79 - filters: dict[str, list[str]] = Field(default_factory=dict) 80 - 81 - 82 - class NotifyRequest(BaseModel): 83 - flight: dict 84 - bsky_handle: str 85 - template: str | None = None 86 - 87 - 88 - class FlightResponse(BaseModel): 89 - flights: list[Flight] 90 - timestamp: str 91 - 92 - 93 - class SubscriptionCreate(BaseModel): 94 - latitude: float 95 - longitude: float 96 - radius_miles: float = 5.0 97 - filters: dict[str, list[str]] = Field(default_factory=dict) 98 - message_template: str | None = None 99 - 100 - 101 - class SubscriptionResponse(BaseModel): 102 - id: int 103 - latitude: float 104 - longitude: float 105 - radius_miles: float 106 - filters: dict[str, list[str]] 107 - message_template: str | None 108 - active: bool 109 - created_at: datetime 110 - updated_at: datetime 111 - 112 - class Config: 113 - from_attributes = True 114 - 115 - 116 - @app.get("/") 117 - async def root() -> HTMLResponse: 118 - """Serve the main web interface.""" 119 - with open("static/index.html", "r") as f: 120 - return HTMLResponse(content=f.read()) 121 - 122 - 123 - @app.get("/app.html") 124 - async def app_page() -> HTMLResponse: 125 - """Serve the subscription management page.""" 126 - with open("static/app.html", "r") as f: 127 - return HTMLResponse(content=f.read()) 128 - 129 - 130 - @app.get("/api/auth/me") 131 - async def get_me(current_user: User = Depends(get_current_user)): 132 - """Get current user info.""" 133 - return { 134 - "bsky_handle": current_user.bsky_handle, 135 - "created_at": current_user.created_at, 136 - } 137 - 138 - 139 - @app.post("/api/auth/login", response_model=Token) 140 - async def login( 141 - form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) 142 - ): 143 - """Login with BlueSky credentials.""" 144 - try: 145 - # Verify BlueSky credentials 146 - temp_client = Client() 147 - temp_client.login(form_data.username, form_data.password) 148 - 149 - # Get user info 150 - profile = temp_client.com.atproto.identity.resolve_handle( 151 - params={"handle": form_data.username} 152 - ) 153 - 154 - # Create or get user 155 - user_create = UserCreate(bsky_handle=form_data.username, bsky_did=profile.did) 156 - user = get_or_create_user(db, user_create) 157 - 158 - # Create access token 159 - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 160 - access_token = create_access_token( 161 - data={"sub": user.bsky_handle}, expires_delta=access_token_expires 162 - ) 163 - 164 - return {"access_token": access_token, "token_type": "bearer"} 165 - 166 - except Exception as e: 167 - import traceback 168 - 169 - logging.error(f"Login error: {str(e)}") 170 - logging.error(traceback.format_exc()) 171 - raise HTTPException( 172 - status_code=status.HTTP_401_UNAUTHORIZED, 173 - detail=f"Invalid BlueSky credentials: {str(e)}", 174 - headers={"WWW-Authenticate": "Bearer"}, 175 - ) 176 - 177 - 178 - @app.get("/api/subscriptions", response_model=list[SubscriptionResponse]) 179 - async def get_subscriptions( 180 - current_user: User = Depends(get_current_user), db: Session = Depends(get_db) 181 - ): 182 - """Get user's subscriptions.""" 183 - subscriptions = ( 184 - db.query(Subscription).filter(Subscription.user_id == current_user.id).all() 185 - ) 186 - return subscriptions 187 - 188 - 189 - @app.post("/api/subscriptions", response_model=SubscriptionResponse) 190 - async def create_subscription( 191 - subscription: SubscriptionCreate, 192 - current_user: User = Depends(get_current_user), 193 - db: Session = Depends(get_db), 194 - ): 195 - """Create a new subscription.""" 196 - db_subscription = Subscription(user_id=current_user.id, **subscription.model_dump()) 197 - db.add(db_subscription) 198 - db.commit() 199 - db.refresh(db_subscription) 200 - return db_subscription 201 - 202 - 203 - @app.put("/api/subscriptions/{subscription_id}", response_model=SubscriptionResponse) 204 - async def update_subscription( 205 - subscription_id: int, 206 - subscription: SubscriptionCreate, 207 - current_user: User = Depends(get_current_user), 208 - db: Session = Depends(get_db), 209 - ): 210 - """Update a subscription.""" 211 - db_subscription = ( 212 - db.query(Subscription) 213 - .filter( 214 - Subscription.id == subscription_id, Subscription.user_id == current_user.id 215 - ) 216 - .first() 217 - ) 218 - 219 - if not db_subscription: 220 - raise HTTPException(status_code=404, detail="Subscription not found") 221 - 222 - for key, value in subscription.model_dump().items(): 223 - setattr(db_subscription, key, value) 224 - 225 - db_subscription.updated_at = datetime.now(timezone.utc) 226 - db.commit() 227 - db.refresh(db_subscription) 228 - return db_subscription 229 - 230 - 231 - @app.delete("/api/subscriptions/{subscription_id}") 232 - async def delete_subscription( 233 - subscription_id: int, 234 - current_user: User = Depends(get_current_user), 235 - db: Session = Depends(get_db), 236 - ): 237 - """Delete a subscription.""" 238 - db_subscription = ( 239 - db.query(Subscription) 240 - .filter( 241 - Subscription.id == subscription_id, Subscription.user_id == current_user.id 242 - ) 243 - .first() 244 - ) 245 - 246 - if not db_subscription: 247 - raise HTTPException(status_code=404, detail="Subscription not found") 248 - 249 - db.delete(db_subscription) 250 - db.commit() 251 - return {"status": "deleted"} 252 - 253 - 254 - @app.post("/api/subscriptions/{subscription_id}/check") 255 - async def check_subscription_now( 256 - subscription_id: int, 257 - current_user: User = Depends(get_current_user), 258 - db: Session = Depends(get_db), 259 - ): 260 - """Manually check a subscription for flights right now.""" 261 - # Get the subscription with user relationship 262 - subscription = ( 263 - db.query(Subscription) 264 - .filter( 265 - Subscription.id == subscription_id, Subscription.user_id == current_user.id 266 - ) 267 - .first() 268 - ) 269 - 270 - if not subscription: 271 - raise HTTPException(status_code=404, detail="Subscription not found") 272 - 273 - # Use the worker's process method to check this subscription 274 - if hasattr(app.state, "processor"): 275 - try: 276 - # Get flights and process them through the worker logic 277 - result = await app.state.processor._process_subscription_manual( 278 - db, subscription 279 - ) 280 - return result 281 - except Exception as e: 282 - raise HTTPException(status_code=500, detail=str(e)) 283 - else: 284 - raise HTTPException(status_code=503, detail="Flight processor not available") 285 - 286 - 287 - @app.post("/api/check-flights") 288 - async def check_flights( 289 - request: CheckFlightsRequest, current_user: User = Depends(get_current_user) 290 - ) -> FlightResponse: 291 - """Check for flights in the specified area.""" 292 - try: 293 - flights = get_flights_in_area( 294 - settings, request.latitude, request.longitude, request.radius_miles 295 - ) 296 - 297 - # Apply filters if provided 298 - if request.filters: 299 - flights = [f for f in flights if flight_matches_filters(f, request.filters)] 300 - 301 - return FlightResponse(flights=flights, timestamp=datetime.now().isoformat()) 302 - except Exception as e: 303 - raise HTTPException(status_code=500, detail=str(e)) 304 - 305 - 306 - @app.post("/api/notify") 307 - async def notify( 308 - request: NotifyRequest, current_user: User = Depends(get_current_user) 309 - ) -> dict[str, str]: 310 - """Send a BlueSky DM notification for a flight.""" 311 - if not bsky_client: 312 - raise HTTPException(status_code=503, detail="BlueSky client not available") 313 - 314 - try: 315 - # Only allow users to send DMs to themselves 316 - if request.bsky_handle != current_user.bsky_handle: 317 - raise HTTPException( 318 - status_code=403, 319 - detail="You can only send notifications to your own handle", 320 - ) 321 - 322 - # Reconstruct Flight object 323 - flight = Flight(**request.flight) 324 - message = format_flight_info(flight, request.template) 325 - 326 - success = send_dm(bsky_client, message, request.bsky_handle) 327 - if not success: 328 - raise HTTPException(status_code=500, detail="Failed to send DM") 329 - 330 - return {"status": "sent", "message": message} 331 - except Exception as e: 332 - raise HTTPException(status_code=500, detail=str(e)) 333 - 334 - 335 - @app.get("/api/health") 336 - async def health() -> dict[str, str | bool]: 337 - """Health check endpoint.""" 338 - return { 339 - "status": "ok", 340 - "bsky_connected": bsky_client is not None, 341 - "timestamp": datetime.now().isoformat(), 342 - }
-233
sandbox/flight-notifier/src/flight_notifier/worker.py
··· 1 - """Background worker for processing subscriptions.""" 2 - 3 - import asyncio 4 - import logging 5 - from collections import defaultdict 6 - from datetime import datetime, timedelta 7 - from typing import Any 8 - 9 - from apscheduler.schedulers.asyncio import AsyncIOScheduler 10 - from sqlalchemy import and_ 11 - from sqlalchemy.orm import Session 12 - 13 - from flight_notifier.database import Notification, SessionLocal, Subscription 14 - from flight_notifier.flight_monitor import ( 15 - Settings, 16 - flight_matches_filters, 17 - format_flight_info, 18 - get_flights_in_area, 19 - send_dm, 20 - ) 21 - 22 - logger = logging.getLogger(__name__) 23 - 24 - # Global state for tracking sent notifications 25 - sent_notifications: dict[int, set[str]] = defaultdict(set) 26 - 27 - 28 - class SubscriptionProcessor: 29 - """Process flight subscriptions efficiently.""" 30 - 31 - def __init__(self, settings: Settings, bsky_client): 32 - self.settings = settings 33 - self.bsky_client = bsky_client 34 - self.scheduler = AsyncIOScheduler() 35 - 36 - async def process_all_subscriptions(self): 37 - """Process all active subscriptions.""" 38 - db = SessionLocal() 39 - try: 40 - # Get all active subscriptions 41 - subscriptions = db.query(Subscription).filter( 42 - Subscription.active == True 43 - ).all() 44 - 45 - if not subscriptions: 46 - logger.info("No active subscriptions to process") 47 - return 48 - 49 - # Group subscriptions by location for efficiency 50 - location_groups = self._group_by_location(subscriptions) 51 - 52 - # Process each location group 53 - for location_key, subs in location_groups.items(): 54 - await self._process_location_group(db, location_key, subs) 55 - 56 - except Exception as e: 57 - logger.error(f"Error processing subscriptions: {e}") 58 - finally: 59 - db.close() 60 - 61 - def _group_by_location(self, subscriptions: list[Subscription]) -> dict: 62 - """Group subscriptions by nearby locations to minimize API calls.""" 63 - groups = defaultdict(list) 64 - 65 - for sub in subscriptions: 66 - # Round to 2 decimal places (about 1km precision) 67 - lat_key = round(sub.latitude, 2) 68 - lon_key = round(sub.longitude, 2) 69 - location_key = (lat_key, lon_key) 70 - groups[location_key].append(sub) 71 - 72 - return groups 73 - 74 - async def _process_location_group( 75 - self, 76 - db: Session, 77 - location_key: tuple[float, float], 78 - subscriptions: list[Subscription] 79 - ): 80 - """Process a group of subscriptions at similar locations.""" 81 - lat, lon = location_key 82 - 83 - # Use the largest radius from the group 84 - max_radius = max(sub.radius_miles for sub in subscriptions) 85 - 86 - try: 87 - # Fetch flights for this location 88 - flights = get_flights_in_area(self.settings, lat, lon, max_radius) 89 - 90 - # Process each subscription 91 - for sub in subscriptions: 92 - await self._process_subscription_flights(db, sub, flights) 93 - 94 - except Exception as e: 95 - logger.error(f"Error processing location {location_key}: {e}") 96 - 97 - async def _process_subscription_flights( 98 - self, 99 - db: Session, 100 - subscription: Subscription, 101 - all_flights: list 102 - ): 103 - """Process flights for a single subscription.""" 104 - # Filter flights within this subscription's radius 105 - center = (subscription.latitude, subscription.longitude) 106 - flights_in_range = [ 107 - f for f in all_flights 108 - if f.distance_miles <= subscription.radius_miles 109 - ] 110 - 111 - # Apply subscription filters 112 - if subscription.filters: 113 - flights_in_range = [ 114 - f for f in flights_in_range 115 - if flight_matches_filters(f, subscription.filters) 116 - ] 117 - 118 - # Check for new flights 119 - subscription_sent = sent_notifications[subscription.id] 120 - 121 - for flight in flights_in_range: 122 - if flight.hex not in subscription_sent: 123 - # Send notification 124 - success = await self._send_notification( 125 - db, subscription, flight 126 - ) 127 - if success: 128 - subscription_sent.add(flight.hex) 129 - 130 - # Clean up old flight IDs 131 - current_flight_ids = {f.hex for f in flights_in_range} 132 - sent_notifications[subscription.id] &= current_flight_ids 133 - 134 - async def _send_notification( 135 - self, 136 - db: Session, 137 - subscription: Subscription, 138 - flight 139 - ) -> bool: 140 - """Send notification for a flight.""" 141 - try: 142 - # Format message 143 - message = format_flight_info(flight, subscription.message_template) 144 - 145 - # Send DM 146 - success = send_dm( 147 - self.bsky_client, 148 - message, 149 - subscription.user.bsky_handle 150 - ) 151 - 152 - if success: 153 - # Record notification 154 - notification = Notification( 155 - subscription_id=subscription.id, 156 - flight_id=flight.hex, 157 - flight_data=flight.model_dump() 158 - ) 159 - db.add(notification) 160 - db.commit() 161 - 162 - logger.info( 163 - f"Sent notification to {subscription.user.bsky_handle} " 164 - f"for flight {flight.hex}" 165 - ) 166 - 167 - return success 168 - 169 - except Exception as e: 170 - logger.error(f"Error sending notification: {e}") 171 - return False 172 - 173 - def start(self): 174 - """Start the background scheduler.""" 175 - # Process subscriptions every 30 seconds 176 - self.scheduler.add_job( 177 - self.process_all_subscriptions, 178 - 'interval', 179 - seconds=30, 180 - id='process_subscriptions', 181 - replace_existing=True 182 - ) 183 - self.scheduler.start() 184 - logger.info("Subscription processor started") 185 - 186 - def stop(self): 187 - """Stop the background scheduler.""" 188 - self.scheduler.shutdown() 189 - logger.info("Subscription processor stopped") 190 - 191 - async def _process_subscription_manual(self, db: Session, subscription: Subscription) -> dict: 192 - """Manually process a single subscription and return results.""" 193 - try: 194 - # Get flights for this subscription 195 - flights = get_flights_in_area( 196 - self.settings, 197 - subscription.latitude, 198 - subscription.longitude, 199 - subscription.radius_miles 200 - ) 201 - 202 - # Apply filters 203 - if subscription.filters: 204 - flights = [ 205 - f for f in flights 206 - if flight_matches_filters(f, subscription.filters) 207 - ] 208 - 209 - if flights: 210 - # Send notifications for new flights 211 - sent_count = 0 212 - for flight in flights[:3]: # Limit to 3 flights for manual check 213 - success = await self._send_notification(db, subscription, flight) 214 - if success: 215 - sent_count += 1 216 - 217 - return { 218 - "status": "found", 219 - "flights_found": len(flights), 220 - "notifications_sent": sent_count, 221 - "message": f"Found {len(flights)} flights, sent {sent_count} notifications" 222 - } 223 - else: 224 - return { 225 - "status": "no_flights", 226 - "flights_found": 0, 227 - "notifications_sent": 0, 228 - "message": "No flights found in your area with current filters" 229 - } 230 - 231 - except Exception as e: 232 - logger.error(f"Error in manual check: {e}") 233 - raise
-777
sandbox/flight-notifier/static/app.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - 4 - <head> 5 - <meta charset="UTF-8"> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 - <title>Flight Notifier - Manage Subscriptions</title> 8 - <style> 9 - * { 10 - margin: 0; 11 - padding: 0; 12 - box-sizing: border-box; 13 - } 14 - 15 - body { 16 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 17 - background: #0a0e1a; 18 - color: #e4e6eb; 19 - min-height: 100vh; 20 - padding: 20px; 21 - line-height: 1.6; 22 - } 23 - 24 - .container { 25 - max-width: 800px; 26 - margin: 0 auto; 27 - } 28 - 29 - h1 { 30 - text-align: center; 31 - margin-bottom: 40px; 32 - font-size: 1.8rem; 33 - font-weight: 600; 34 - } 35 - 36 - .user-info { 37 - background: rgba(255, 255, 255, 0.03); 38 - padding: 15px 20px; 39 - border-radius: 8px; 40 - margin-bottom: 30px; 41 - display: flex; 42 - justify-content: space-between; 43 - align-items: center; 44 - border: 1px solid rgba(255, 255, 255, 0.08); 45 - } 46 - 47 - .subscription-form { 48 - background: rgba(255, 255, 255, 0.03); 49 - padding: 30px; 50 - border-radius: 12px; 51 - margin-bottom: 40px; 52 - border: 1px solid rgba(255, 255, 255, 0.08); 53 - } 54 - 55 - .form-row { 56 - display: grid; 57 - grid-template-columns: 1fr 1fr; 58 - gap: 15px; 59 - margin-bottom: 15px; 60 - } 61 - 62 - .input-group { 63 - margin-bottom: 15px; 64 - } 65 - 66 - label { 67 - display: block; 68 - margin-bottom: 5px; 69 - font-size: 0.9rem; 70 - color: rgba(255, 255, 255, 0.6); 71 - } 72 - 73 - input, 74 - select { 75 - width: 100%; 76 - padding: 12px; 77 - background: rgba(255, 255, 255, 0.03); 78 - border: 1px solid rgba(255, 255, 255, 0.08); 79 - border-radius: 6px; 80 - color: #e4e6eb; 81 - font-size: 1rem; 82 - transition: all 0.2s; 83 - } 84 - 85 - input:focus, 86 - select:focus { 87 - outline: none; 88 - border-color: #00a8cc; 89 - background: rgba(255, 255, 255, 0.08); 90 - } 91 - 92 - button { 93 - padding: 12px 20px; 94 - border: none; 95 - border-radius: 8px; 96 - font-size: 1rem; 97 - cursor: pointer; 98 - transition: all 0.2s; 99 - } 100 - 101 - .btn-primary { 102 - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 103 - color: white; 104 - font-weight: 500; 105 - } 106 - 107 - .btn-primary:hover { 108 - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); 109 - transform: translateY(-1px); 110 - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 111 - } 112 - 113 - .btn-secondary { 114 - background: transparent; 115 - color: #a8b2d1; 116 - border: 1px solid rgba(168, 178, 209, 0.3); 117 - } 118 - 119 - .btn-secondary:hover { 120 - background: rgba(168, 178, 209, 0.1); 121 - border-color: rgba(168, 178, 209, 0.5); 122 - color: #e4e6eb; 123 - } 124 - 125 - .btn-danger { 126 - background: #e74c3c; 127 - color: white; 128 - } 129 - 130 - .btn-danger:hover { 131 - background: #c0392b; 132 - box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3); 133 - } 134 - 135 - .subscriptions-list { 136 - display: grid; 137 - gap: 20px; 138 - } 139 - 140 - .subscription-card { 141 - background: rgba(255, 255, 255, 0.03); 142 - padding: 24px; 143 - border-radius: 12px; 144 - position: relative; 145 - border: 1px solid rgba(255, 255, 255, 0.08); 146 - transition: all 0.2s; 147 - } 148 - 149 - .subscription-card:hover { 150 - background: rgba(255, 255, 255, 0.05); 151 - border-color: rgba(102, 126, 234, 0.3); 152 - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); 153 - } 154 - 155 - .subscription-header { 156 - display: flex; 157 - justify-content: space-between; 158 - align-items: start; 159 - margin-bottom: 15px; 160 - } 161 - 162 - .subscription-location { 163 - font-size: 1.05rem; 164 - font-weight: 500; 165 - color: rgba(255, 255, 255, 0.9); 166 - } 167 - 168 - .subscription-status { 169 - padding: 4px 12px; 170 - border-radius: 20px; 171 - font-size: 0.85rem; 172 - background: rgba(52, 211, 153, 0.15); 173 - color: #34d399; 174 - border: 1px solid rgba(52, 211, 153, 0.3); 175 - } 176 - 177 - .subscription-status.inactive { 178 - background: rgba(239, 68, 68, 0.15); 179 - color: #ef4444; 180 - border: 1px solid rgba(239, 68, 68, 0.3); 181 - } 182 - 183 - .subscription-details { 184 - display: grid; 185 - grid-template-columns: repeat(2, 1fr); 186 - gap: 10px; 187 - margin-bottom: 15px; 188 - font-size: 0.9rem; 189 - color: rgba(255, 255, 255, 0.6); 190 - } 191 - 192 - .subscription-actions { 193 - display: flex; 194 - gap: 10px; 195 - } 196 - 197 - .edit-mode { 198 - background: rgba(102, 126, 234, 0.05); 199 - border-color: rgba(102, 126, 234, 0.3); 200 - } 201 - 202 - .edit-form { 203 - margin-top: 20px; 204 - padding-top: 20px; 205 - border-top: 1px solid rgba(255, 255, 255, 0.1); 206 - } 207 - 208 - .edit-form .form-row { 209 - margin-bottom: 15px; 210 - } 211 - 212 - .edit-actions { 213 - display: flex; 214 - gap: 10px; 215 - margin-top: 20px; 216 - } 217 - 218 - .btn-save { 219 - background: linear-gradient(135deg, #34d399 0%, #10b981 100%); 220 - color: white; 221 - } 222 - 223 - .btn-save:hover { 224 - background: linear-gradient(135deg, #10b981 0%, #34d399 100%); 225 - box-shadow: 0 4px 12px rgba(52, 211, 153, 0.3); 226 - } 227 - 228 - .btn-cancel { 229 - background: rgba(255, 255, 255, 0.05); 230 - color: #e4e6eb; 231 - border: 1px solid rgba(255, 255, 255, 0.1); 232 - } 233 - 234 - .btn-cancel:hover { 235 - background: rgba(255, 255, 255, 0.08); 236 - border-color: rgba(255, 255, 255, 0.2); 237 - } 238 - 239 - .filter-tags { 240 - display: flex; 241 - flex-wrap: wrap; 242 - margin-top: 12px; 243 - margin-bottom: 8px; 244 - } 245 - 246 - .filter-tag { 247 - background: rgba(255, 255, 255, 0.08); 248 - padding: 5px 12px; 249 - border-radius: 16px; 250 - font-size: 0.85rem; 251 - display: inline-block; 252 - margin-right: 6px; 253 - margin-bottom: 6px; 254 - border: 1px solid rgba(255, 255, 255, 0.05); 255 - } 256 - 257 - .no-subscriptions { 258 - text-align: center; 259 - padding: 60px 20px; 260 - color: rgba(255, 255, 255, 0.4); 261 - } 262 - 263 - .loading { 264 - text-align: center; 265 - padding: 40px; 266 - } 267 - 268 - .error { 269 - background: #3a2328; 270 - color: #f8a5a5; 271 - padding: 15px; 272 - border-radius: 8px; 273 - margin-bottom: 15px; 274 - } 275 - 276 - .map-picker { 277 - height: 60px; 278 - background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); 279 - border: 1px solid rgba(102, 126, 234, 0.2); 280 - border-radius: 8px; 281 - margin-bottom: 20px; 282 - display: flex; 283 - align-items: center; 284 - justify-content: center; 285 - color: #a8b2d1; 286 - cursor: pointer; 287 - transition: all 0.2s; 288 - } 289 - 290 - .map-picker:hover { 291 - background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%); 292 - border-color: rgba(102, 126, 234, 0.4); 293 - color: #e4e6eb; 294 - transform: translateY(-1px); 295 - } 296 - </style> 297 - </head> 298 - 299 - <body> 300 - <div class="container"> 301 - <h1>✈️ Flight Notifier</h1> 302 - 303 - <div class="user-info"> 304 - <div> 305 - <strong>Logged in as:</strong> <span id="userHandle">Loading...</span> 306 - </div> 307 - <button class="btn-secondary" onclick="logout()">Logout</button> 308 - </div> 309 - 310 - <div class="subscription-form"> 311 - <h2 style="font-size: 1.3rem; margin-bottom: 25px; font-weight: 500;">Add New Subscription</h2> 312 - 313 - <div class="map-picker" onclick="useCurrentLocation()"> 314 - 📍 Use current location 315 - </div> 316 - 317 - <div class="form-row"> 318 - <div class="input-group"> 319 - <label for="latitude">Latitude</label> 320 - <input type="number" id="latitude" step="0.0001" required> 321 - </div> 322 - <div class="input-group"> 323 - <label for="longitude">Longitude</label> 324 - <input type="number" id="longitude" step="0.0001" required> 325 - </div> 326 - </div> 327 - 328 - <div class="form-row"> 329 - <div class="input-group"> 330 - <label for="radius">Radius (miles)</label> 331 - <input type="number" id="radius" value="5" min="1" max="50"> 332 - </div> 333 - <div class="input-group"> 334 - <label for="filterPreset">Quick Filters</label> 335 - <select id="filterPreset" onchange="applyPreset()"> 336 - <option value="">No filter</option> 337 - <option value="military">🚁 Military Aircraft</option> 338 - <option value="commercial">🛫 Commercial Flights</option> 339 - <option value="ga">🛩️ General Aviation</option> 340 - </select> 341 - </div> 342 - </div> 343 - 344 - <div style="margin-top: 20px;"> 345 - <h3 style="font-size: 1rem; margin-bottom: 15px; color: rgba(255, 255, 255, 0.6); font-weight: 400;">Filters (Optional)</h3> 346 - <div class="input-group"> 347 - <label for="aircraftTypes">Aircraft Types</label> 348 - <input type="text" id="aircraftTypes" placeholder="B737, A320, C172 (comma separated)"> 349 - <small style="color: rgba(255, 255, 255, 0.4); font-size: 0.85rem;">Filter by aircraft model (partial match)</small> 350 - </div> 351 - <div class="input-group"> 352 - <label for="airlines">Airlines</label> 353 - <input type="text" id="airlines" placeholder="UAL, AAL, DAL (comma separated)"> 354 - <small style="color: rgba(255, 255, 255, 0.4); font-size: 0.85rem;">Filter by airline callsign prefix</small> 355 - </div> 356 - </div> 357 - 358 - <button class="btn-primary" onclick="createSubscription()" style="margin-top: 20px; width: 100%;"> 359 - Create Subscription 360 - </button> 361 - </div> 362 - 363 - <h2 style="font-size: 1.3rem; margin-bottom: 20px; font-weight: 500;">Your Subscriptions</h2> 364 - <div class="subscriptions-list" id="subscriptionsList"> 365 - <div class="loading">Loading subscriptions...</div> 366 - </div> 367 - </div> 368 - 369 - <script> 370 - let authToken = localStorage.getItem('authToken'); 371 - let currentUser = null; 372 - 373 - // Check auth on load 374 - if (!authToken) { 375 - window.location.href = '/'; 376 - } 377 - 378 - // Load user info and subscriptions 379 - window.addEventListener('load', () => { 380 - loadUserInfo(); 381 - loadSubscriptions(); 382 - // Auto-request location on load 383 - requestLocationSilently(); 384 - }); 385 - 386 - async function loadUserInfo() { 387 - try { 388 - const response = await fetch('/api/auth/me', { 389 - headers: { 390 - 'Authorization': `Bearer ${authToken}` 391 - } 392 - }); 393 - 394 - if (response.ok) { 395 - currentUser = await response.json(); 396 - document.getElementById('userHandle').textContent = currentUser.bsky_handle; 397 - } else { 398 - window.location.href = '/'; 399 - } 400 - } catch (error) { 401 - console.error('Error loading user info:', error); 402 - } 403 - } 404 - 405 - async function loadSubscriptions() { 406 - const container = document.getElementById('subscriptionsList'); 407 - 408 - try { 409 - const response = await fetch('/api/subscriptions', { 410 - headers: { 411 - 'Authorization': `Bearer ${authToken}` 412 - } 413 - }); 414 - 415 - if (!response.ok) { 416 - throw new Error('Failed to load subscriptions'); 417 - } 418 - 419 - const subscriptions = await response.json(); 420 - 421 - if (subscriptions.length === 0) { 422 - container.innerHTML = ` 423 - <div class="no-subscriptions"> 424 - <p>You don't have any subscriptions yet.</p> 425 - <p>Create one above to start receiving flight notifications!</p> 426 - </div> 427 - `; 428 - return; 429 - } 430 - 431 - container.innerHTML = subscriptions.map(sub => ` 432 - <div class="subscription-card" id="sub-${sub.id}"> 433 - <div class="view-mode" id="view-${sub.id}"> 434 - <div class="subscription-header"> 435 - <div> 436 - <div class="subscription-location"> 437 - 📍 ${sub.latitude.toFixed(4)}, ${sub.longitude.toFixed(4)} 438 - </div> 439 - ${sub.active ? '' : '<div class="subscription-status inactive">Inactive</div>'} 440 - </div> 441 - </div> 442 - 443 - <div class="subscription-details"> 444 - <div><span style="color: rgba(255, 255, 255, 0.5);">Radius:</span> ${sub.radius_miles} ${sub.radius_miles === 1 ? 'mile' : 'miles'}</div> 445 - <div><span style="color: rgba(255, 255, 255, 0.5);">Created:</span> ${new Date(sub.created_at).toLocaleDateString()}</div> 446 - </div> 447 - 448 - ${sub.filters && Object.keys(sub.filters).length > 0 ? ` 449 - <div class="filter-tags"> 450 - ${Object.entries(sub.filters).map(([key, values]) => { 451 - const displayKey = key === 'aircraft_type' ? 'Aircraft' : key === 'callsign' ? 'Airlines' : key; 452 - return values.map(v => `<span class="filter-tag">${displayKey}: ${v}</span>`).join(' '); 453 - }).join(' ')} 454 - </div> 455 - ` : ''} 456 - 457 - <div class="subscription-actions"> 458 - <button class="btn-secondary" onclick="editSubscription(${sub.id})"> 459 - Edit 460 - </button> 461 - <button class="btn-secondary" onclick="checkSubscription(${sub.id})"> 462 - Check Now 463 - </button> 464 - <button class="btn-danger" onclick="deleteSubscription(${sub.id})"> 465 - Delete 466 - </button> 467 - </div> 468 - </div> 469 - 470 - <div class="edit-mode" id="edit-${sub.id}" style="display: none;"> 471 - <div class="edit-form"> 472 - <div class="map-picker" onclick="useCurrentLocationForEdit(${sub.id})" style="height: 40px; margin-bottom: 15px;"> 473 - 📍 Update to current location 474 - </div> 475 - 476 - <div class="form-row"> 477 - <div class="input-group"> 478 - <label>Latitude</label> 479 - <input type="number" id="edit-lat-${sub.id}" value="${sub.latitude}" step="0.0001"> 480 - </div> 481 - <div class="input-group"> 482 - <label>Longitude</label> 483 - <input type="number" id="edit-lon-${sub.id}" value="${sub.longitude}" step="0.0001"> 484 - </div> 485 - </div> 486 - 487 - <div class="input-group"> 488 - <label>Radius (miles)</label> 489 - <input type="number" id="edit-radius-${sub.id}" value="${sub.radius_miles}" min="1" max="50"> 490 - </div> 491 - 492 - <div class="input-group"> 493 - <label>Aircraft Types</label> 494 - <input type="text" id="edit-aircraft-${sub.id}" 495 - value="${sub.filters?.aircraft_type?.join(', ') || ''}" 496 - placeholder="B737, A320, C172 (comma separated)"> 497 - </div> 498 - 499 - <div class="input-group"> 500 - <label>Airlines</label> 501 - <input type="text" id="edit-airlines-${sub.id}" 502 - value="${sub.filters?.callsign?.join(', ') || ''}" 503 - placeholder="UAL, AAL, DAL (comma separated)"> 504 - </div> 505 - 506 - <div class="edit-actions"> 507 - <button class="btn-save" onclick="saveSubscription(${sub.id})"> 508 - Save Changes 509 - </button> 510 - <button class="btn-cancel" onclick="cancelEdit(${sub.id})"> 511 - Cancel 512 - </button> 513 - </div> 514 - </div> 515 - </div> 516 - </div> 517 - `).join(''); 518 - 519 - } catch (error) { 520 - container.innerHTML = `<div class="error">Error loading subscriptions: ${error.message}</div>`; 521 - } 522 - } 523 - 524 - function useCurrentLocation() { 525 - if (!navigator.geolocation) { 526 - alert('Geolocation is not supported by your browser'); 527 - return; 528 - } 529 - 530 - navigator.geolocation.getCurrentPosition( 531 - (position) => { 532 - document.getElementById('latitude').value = position.coords.latitude.toFixed(4); 533 - document.getElementById('longitude').value = position.coords.longitude.toFixed(4); 534 - }, 535 - (error) => { 536 - alert('Unable to get location: ' + error.message); 537 - } 538 - ); 539 - } 540 - 541 - function requestLocationSilently() { 542 - if (!navigator.geolocation) { 543 - return; 544 - } 545 - 546 - // Try to get location without prompting (will only work if already granted) 547 - navigator.geolocation.getCurrentPosition( 548 - (position) => { 549 - // Auto-populate if fields are empty 550 - const latInput = document.getElementById('latitude'); 551 - const lonInput = document.getElementById('longitude'); 552 - 553 - if (!latInput.value && !lonInput.value) { 554 - latInput.value = position.coords.latitude.toFixed(4); 555 - lonInput.value = position.coords.longitude.toFixed(4); 556 - } 557 - }, 558 - () => { 559 - // Silently fail - user hasn't granted permission yet 560 - } 561 - ); 562 - } 563 - 564 - function applyPreset() { 565 - const preset = document.getElementById('filterPreset').value; 566 - const aircraftInput = document.getElementById('aircraftTypes'); 567 - const airlinesInput = document.getElementById('airlines'); 568 - 569 - switch (preset) { 570 - case 'military': 571 - aircraftInput.value = 'UH, AH, CH, F16, F18'; 572 - airlinesInput.value = ''; 573 - break; 574 - case 'commercial': 575 - aircraftInput.value = 'B737, B747, A320, A330'; 576 - airlinesInput.value = 'UAL, AAL, DAL, SWA'; 577 - break; 578 - case 'ga': 579 - aircraftInput.value = 'C172, C182, PA28'; 580 - airlinesInput.value = ''; 581 - break; 582 - default: 583 - aircraftInput.value = ''; 584 - airlinesInput.value = ''; 585 - } 586 - } 587 - 588 - async function createSubscription() { 589 - const latitude = parseFloat(document.getElementById('latitude').value); 590 - const longitude = parseFloat(document.getElementById('longitude').value); 591 - const radius = parseFloat(document.getElementById('radius').value); 592 - 593 - if (!latitude || !longitude) { 594 - alert('Please enter valid coordinates'); 595 - return; 596 - } 597 - 598 - const filters = {}; 599 - const aircraftTypes = document.getElementById('aircraftTypes').value; 600 - if (aircraftTypes) { 601 - filters.aircraft_type = aircraftTypes.split(',').map(t => t.trim()).filter(t => t); 602 - } 603 - 604 - const airlines = document.getElementById('airlines').value; 605 - if (airlines) { 606 - filters.callsign = airlines.split(',').map(a => a.trim()).filter(a => a); 607 - } 608 - 609 - try { 610 - const response = await fetch('/api/subscriptions', { 611 - method: 'POST', 612 - headers: { 613 - 'Content-Type': 'application/json', 614 - 'Authorization': `Bearer ${authToken}` 615 - }, 616 - body: JSON.stringify({ 617 - latitude, 618 - longitude, 619 - radius_miles: radius, 620 - filters 621 - }) 622 - }); 623 - 624 - if (!response.ok) { 625 - throw new Error('Failed to create subscription'); 626 - } 627 - 628 - // Clear form 629 - document.getElementById('latitude').value = ''; 630 - document.getElementById('longitude').value = ''; 631 - document.getElementById('aircraftTypes').value = ''; 632 - document.getElementById('airlines').value = ''; 633 - document.getElementById('filterPreset').value = ''; 634 - 635 - // Reload subscriptions 636 - loadSubscriptions(); 637 - 638 - } catch (error) { 639 - alert('Error creating subscription: ' + error.message); 640 - } 641 - } 642 - 643 - async function deleteSubscription(id) { 644 - if (!confirm('Are you sure you want to delete this subscription?')) { 645 - return; 646 - } 647 - 648 - try { 649 - const response = await fetch(`/api/subscriptions/${id}`, { 650 - method: 'DELETE', 651 - headers: { 652 - 'Authorization': `Bearer ${authToken}` 653 - } 654 - }); 655 - 656 - if (!response.ok) { 657 - throw new Error('Failed to delete subscription'); 658 - } 659 - 660 - loadSubscriptions(); 661 - 662 - } catch (error) { 663 - alert('Error deleting subscription: ' + error.message); 664 - } 665 - } 666 - 667 - async function checkSubscription(id) { 668 - try { 669 - const response = await fetch(`/api/subscriptions/${id}/check`, { 670 - method: 'POST', 671 - headers: { 672 - 'Authorization': `Bearer ${authToken}` 673 - } 674 - }); 675 - 676 - if (!response.ok) { 677 - throw new Error('Failed to check subscription'); 678 - } 679 - 680 - const result = await response.json(); 681 - 682 - if (result.status === 'found') { 683 - alert(`✈️ ${result.message}`); 684 - } else { 685 - alert(`No flights found in your area right now.`); 686 - } 687 - 688 - } catch (error) { 689 - alert('Error checking subscription: ' + error.message); 690 - } 691 - } 692 - 693 - function editSubscription(id) { 694 - // Hide view mode, show edit mode 695 - document.getElementById(`view-${id}`).style.display = 'none'; 696 - document.getElementById(`edit-${id}`).style.display = 'block'; 697 - document.getElementById(`sub-${id}`).classList.add('edit-mode'); 698 - } 699 - 700 - function useCurrentLocationForEdit(id) { 701 - if (!navigator.geolocation) { 702 - alert('Geolocation is not supported by your browser'); 703 - return; 704 - } 705 - 706 - navigator.geolocation.getCurrentPosition( 707 - (position) => { 708 - document.getElementById(`edit-lat-${id}`).value = position.coords.latitude.toFixed(4); 709 - document.getElementById(`edit-lon-${id}`).value = position.coords.longitude.toFixed(4); 710 - }, 711 - (error) => { 712 - alert('Unable to get location: ' + error.message); 713 - } 714 - ); 715 - } 716 - 717 - function cancelEdit(id) { 718 - // Hide edit mode, show view mode 719 - document.getElementById(`edit-${id}`).style.display = 'none'; 720 - document.getElementById(`view-${id}`).style.display = 'block'; 721 - document.getElementById(`sub-${id}`).classList.remove('edit-mode'); 722 - } 723 - 724 - async function saveSubscription(id) { 725 - const latitude = parseFloat(document.getElementById(`edit-lat-${id}`).value); 726 - const longitude = parseFloat(document.getElementById(`edit-lon-${id}`).value); 727 - const radius = parseFloat(document.getElementById(`edit-radius-${id}`).value); 728 - 729 - const filters = {}; 730 - const aircraftTypes = document.getElementById(`edit-aircraft-${id}`).value; 731 - if (aircraftTypes) { 732 - filters.aircraft_type = aircraftTypes.split(',').map(t => t.trim()).filter(t => t); 733 - } 734 - 735 - const airlines = document.getElementById(`edit-airlines-${id}`).value; 736 - if (airlines) { 737 - filters.callsign = airlines.split(',').map(a => a.trim()).filter(a => a); 738 - } 739 - 740 - try { 741 - const response = await fetch(`/api/subscriptions/${id}`, { 742 - method: 'PUT', 743 - headers: { 744 - 'Content-Type': 'application/json', 745 - 'Authorization': `Bearer ${authToken}` 746 - }, 747 - body: JSON.stringify({ 748 - latitude, 749 - longitude, 750 - radius_miles: radius, 751 - filters 752 - }) 753 - }); 754 - 755 - if (!response.ok) { 756 - throw new Error('Failed to update subscription'); 757 - } 758 - 759 - // Reload subscriptions to show updated data 760 - loadSubscriptions(); 761 - 762 - } catch (error) { 763 - alert('Error updating subscription: ' + error.message); 764 - } 765 - } 766 - 767 - function logout() { 768 - localStorage.removeItem('authToken'); 769 - // Also clear saved credentials for security 770 - localStorage.removeItem('bskyHandle'); 771 - localStorage.removeItem('bskyAppPassword'); 772 - window.location.href = '/'; 773 - } 774 - </script> 775 - </body> 776 - 777 - </html>
-792
sandbox/flight-notifier/static/index.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - 4 - <head> 5 - <meta charset="UTF-8"> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 - <title>Flight Notifier</title> 8 - <style> 9 - * { 10 - margin: 0; 11 - padding: 0; 12 - box-sizing: border-box; 13 - } 14 - 15 - body { 16 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 17 - background: #0a0e1a; 18 - color: #e4e6eb; 19 - min-height: 100vh; 20 - padding: 20px; 21 - line-height: 1.6; 22 - } 23 - 24 - .container { 25 - max-width: 600px; 26 - margin: 0 auto; 27 - } 28 - 29 - h1 { 30 - text-align: center; 31 - margin-bottom: 30px; 32 - font-size: 2rem; 33 - } 34 - 35 - .controls { 36 - background: rgba(255, 255, 255, 0.03); 37 - padding: 20px; 38 - border-radius: 12px; 39 - margin-bottom: 20px; 40 - border: 1px solid rgba(255, 255, 255, 0.08); 41 - } 42 - 43 - .location-status { 44 - padding: 10px; 45 - background: #2d2f33; 46 - border-radius: 8px; 47 - margin-bottom: 15px; 48 - font-size: 0.9rem; 49 - } 50 - 51 - .location-status.active { 52 - background: #0f4c5c; 53 - } 54 - 55 - .button-group { 56 - display: flex; 57 - gap: 10px; 58 - margin-bottom: 15px; 59 - } 60 - 61 - button { 62 - flex: 1; 63 - padding: 12px 20px; 64 - border: none; 65 - border-radius: 8px; 66 - font-size: 1rem; 67 - cursor: pointer; 68 - transition: all 0.2s; 69 - } 70 - 71 - .btn-primary { 72 - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 73 - color: white; 74 - } 75 - 76 - .btn-primary:hover { 77 - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); 78 - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 79 - } 80 - 81 - .btn-secondary { 82 - background: #3a3b3c; 83 - color: #e4e6eb; 84 - } 85 - 86 - .btn-secondary:hover { 87 - background: #4a4b4c; 88 - } 89 - 90 - .input-group { 91 - margin-bottom: 15px; 92 - } 93 - 94 - label { 95 - display: block; 96 - margin-bottom: 5px; 97 - font-size: 0.9rem; 98 - color: #b0b3b8; 99 - } 100 - 101 - input { 102 - width: 100%; 103 - padding: 10px; 104 - background: #2d2f33; 105 - border: 1px solid #3a3b3c; 106 - border-radius: 8px; 107 - color: #e4e6eb; 108 - font-size: 1rem; 109 - } 110 - 111 - .toggle { 112 - display: flex; 113 - align-items: center; 114 - gap: 10px; 115 - margin-bottom: 15px; 116 - } 117 - 118 - .toggle input { 119 - width: auto; 120 - } 121 - 122 - .flights { 123 - background: #1c1e21; 124 - padding: 20px; 125 - border-radius: 12px; 126 - min-height: 200px; 127 - } 128 - 129 - .flight-card { 130 - background: #2d2f33; 131 - padding: 15px; 132 - border-radius: 8px; 133 - margin-bottom: 15px; 134 - position: relative; 135 - } 136 - 137 - .flight-header { 138 - display: flex; 139 - justify-content: space-between; 140 - align-items: center; 141 - margin-bottom: 10px; 142 - } 143 - 144 - .flight-callsign { 145 - font-size: 1.2rem; 146 - font-weight: bold; 147 - } 148 - 149 - .flight-distance { 150 - background: #00a8cc; 151 - padding: 4px 12px; 152 - border-radius: 20px; 153 - font-size: 0.85rem; 154 - } 155 - 156 - .flight-details { 157 - display: grid; 158 - grid-template-columns: repeat(2, 1fr); 159 - gap: 8px; 160 - margin-bottom: 10px; 161 - font-size: 0.9rem; 162 - color: #b0b3b8; 163 - } 164 - 165 - .dm-button { 166 - width: 100%; 167 - padding: 10px; 168 - background: #5865f2; 169 - color: white; 170 - border: none; 171 - border-radius: 6px; 172 - cursor: pointer; 173 - font-size: 0.9rem; 174 - } 175 - 176 - .dm-button:hover { 177 - background: #4752c4; 178 - } 179 - 180 - .no-flights { 181 - text-align: center; 182 - color: #b0b3b8; 183 - padding: 40px; 184 - } 185 - 186 - .loading { 187 - text-align: center; 188 - padding: 40px; 189 - } 190 - 191 - .error { 192 - background: #3a2328; 193 - color: #f8a5a5; 194 - padding: 15px; 195 - border-radius: 8px; 196 - margin-bottom: 15px; 197 - } 198 - 199 - .filters-section { 200 - margin-top: 20px; 201 - background: #2d2f33; 202 - padding: 15px; 203 - border-radius: 8px; 204 - cursor: pointer; 205 - } 206 - 207 - .filters-section summary { 208 - font-weight: 600; 209 - padding: 5px; 210 - user-select: none; 211 - } 212 - 213 - .filters-section summary:hover { 214 - color: #00a8cc; 215 - } 216 - 217 - .filters-content { 218 - margin-top: 15px; 219 - padding-top: 15px; 220 - border-top: 1px solid #3a3b3c; 221 - } 222 - 223 - .filters-content small { 224 - color: #8b8d91; 225 - font-size: 0.85rem; 226 - display: block; 227 - margin-top: 2px; 228 - } 229 - 230 - .filter-presets { 231 - display: flex; 232 - gap: 8px; 233 - margin-top: 15px; 234 - flex-wrap: wrap; 235 - } 236 - 237 - .preset-btn { 238 - padding: 8px 16px; 239 - font-size: 0.9rem; 240 - background: #3a3b3c; 241 - border: 1px solid #4a4b4c; 242 - flex: 1; 243 - min-width: 120px; 244 - } 245 - 246 - .preset-btn:hover { 247 - background: #4a4b4c; 248 - border-color: #00a8cc; 249 - } 250 - 251 - .flight-type { 252 - display: inline-block; 253 - padding: 2px 8px; 254 - border-radius: 4px; 255 - font-size: 0.8rem; 256 - margin-left: 8px; 257 - background: #4a4b4c; 258 - } 259 - 260 - .flight-type.military { 261 - background: #5c4c8c; 262 - } 263 - 264 - .flight-type.commercial { 265 - background: #2c5c8c; 266 - } 267 - 268 - .flight-type.ga { 269 - background: #5c8c4c; 270 - } 271 - 272 - @keyframes pulse { 273 - 0% { 274 - opacity: 0.6; 275 - } 276 - 277 - 50% { 278 - opacity: 1; 279 - } 280 - 281 - 100% { 282 - opacity: 0.6; 283 - } 284 - } 285 - 286 - .flight-card.new { 287 - animation: pulse 2s ease-in-out; 288 - border: 1px solid #00a8cc; 289 - } 290 - </style> 291 - </head> 292 - 293 - <body> 294 - <div class="container"> 295 - <h1>✈️ Flight Notifier</h1> 296 - 297 - <div class="login-form" id="loginForm"> 298 - <h2>Login with BlueSky</h2> 299 - <p style="margin-bottom: 20px; color: #b0b3b8;"> 300 - Sign in with your BlueSky account to manage flight subscriptions 301 - </p> 302 - 303 - <div class="input-group"> 304 - <label for="bskyHandle">BlueSky Handle</label> 305 - <input type="text" id="bskyHandle" placeholder="yourhandle.bsky.social" required> 306 - </div> 307 - 308 - <div class="input-group"> 309 - <label for="bskyPassword">App Password</label> 310 - <input type="password" id="bskyPassword" placeholder="xxxx-xxxx-xxxx-xxxx" required> 311 - <small style="color: #8b8d91; display: block; margin-top: 5px;"> 312 - Need an app password? 313 - <a href="https://bsky.app/settings/app-passwords" target="_blank" style="color: #00a8cc;"> 314 - Click here to create one. 315 - </a> <b>Select "Allows access to direct messages"</b> 316 - </small> 317 - </div> 318 - 319 - <div class="toggle" style="margin-top: 15px;"> 320 - <input type="checkbox" id="rememberMe" checked> 321 - <label for="rememberMe" style="color: #b0b3b8; font-size: 0.95rem; cursor: pointer;">Remember me</label> 322 - </div> 323 - 324 - <details style="margin-top: 15px; background: #2d2f33; padding: 15px; border-radius: 8px;"> 325 - <summary style="cursor: pointer; font-weight: 600;">📱 How to get an app password</summary> 326 - <ol style="margin-top: 10px; margin-left: 20px; color: #b0b3b8; line-height: 1.8;"> 327 - <li>Make sure you <a href="https://bsky.app/settings/account" target="_blank">have a BlueSky 328 - account</a></li> 329 - <li>Click <a href="https://bsky.app/settings/app-passwords" target="_blank">this link</a> or go to 330 - BlueSky Settings</li> 331 - <li>Click "Add App Password"</li> 332 - <li>Name it something like "Flight Notifier"</li> 333 - <li>Select "Allows access to direct messages"</li> 334 - <li>Copy the password (looks like: abcd-efgh-ijkl-mnop) and paste it here</li> 335 - <li>Paste it here and login!</li> 336 - </ol> 337 - <p style="margin-top: 10px; color: #8b8d91; font-size: 0.9rem;"> 338 - 💡 App passwords keep your main password safe. You can revoke them anytime. 339 - </p> 340 - </details> 341 - 342 - <button class="btn-primary" onclick="login()" style="width: 100%; margin-top: 20px;"> 343 - Login 344 - </button> 345 - 346 - <div style="margin-top: 30px; padding: 20px; background: #2d2f33; border-radius: 8px;"> 347 - <h3 style="margin-bottom: 10px;">✈️ What is Flight Notifier?</h3> 348 - <p style="color: #b0b3b8; margin-bottom: 10px;"> 349 - Get BlueSky DMs when aircraft fly overhead! Create location-based subscriptions 350 - and filter by aircraft type, airline, or route. 351 - </p> 352 - <ul style="color: #b0b3b8; margin-left: 20px;"> 353 - <li>Real-time flight tracking</li> 354 - <li>Customizable notifications</li> 355 - <li>Multiple location subscriptions</li> 356 - <li>Filter by aircraft type or airline</li> 357 - </ul> 358 - </div> 359 - </div> 360 - 361 - <div class="controls" id="demoMode" style="display: none;"> 362 - <div class="location-status" id="locationStatus"> 363 - 📍 Location: Not available 364 - </div> 365 - 366 - <div class="button-group"> 367 - <button class="btn-primary" onclick="checkFlights()"> 368 - Check Flights Now 369 - </button> 370 - <button class="btn-secondary" onclick="requestLocation()"> 371 - Update Location 372 - </button> 373 - </div> 374 - 375 - <div class="toggle"> 376 - <input type="checkbox" id="autoCheck" onchange="toggleAutoCheck()"> 377 - <label for="autoCheck">Auto-check every 30 seconds</label> 378 - </div> 379 - 380 - <div class="input-group"> 381 - <label for="demoBskyHandle">BlueSky Handle (for DMs)</label> 382 - <input type="text" id="demoBskyHandle" placeholder="yourhandle.bsky.social" value="alternatebuild.dev"> 383 - </div> 384 - 385 - <div class="input-group"> 386 - <label for="radiusMiles">Search Radius (miles)</label> 387 - <input type="number" id="radiusMiles" value="5" min="1" max="50"> 388 - </div> 389 - 390 - <details class="filters-section"> 391 - <summary>✈️ Advanced Filters</summary> 392 - <div class="filters-content"> 393 - <div class="input-group"> 394 - <label for="aircraftTypes">Aircraft Types</label> 395 - <input type="text" id="aircraftTypes" placeholder="e.g., B737, A320, C172 (comma separated)"> 396 - <small>Filter by aircraft model (partial match)</small> 397 - </div> 398 - 399 - <div class="input-group"> 400 - <label for="airlines">Airlines</label> 401 - <input type="text" id="airlines" placeholder="e.g., UAL, AAL, DAL (comma separated)"> 402 - <small>Filter by airline callsign prefix</small> 403 - </div> 404 - 405 - <div class="input-group"> 406 - <label for="origins">Origin Airports</label> 407 - <input type="text" id="origins" placeholder="e.g., ORD, LAX, JFK (comma separated)"> 408 - <small>Filter by departure airport</small> 409 - </div> 410 - 411 - <div class="input-group"> 412 - <label for="destinations">Destination Airports</label> 413 - <input type="text" id="destinations" placeholder="e.g., ORD, LAX, JFK (comma separated)"> 414 - <small>Filter by arrival airport</small> 415 - </div> 416 - 417 - <div class="filter-presets"> 418 - <button class="preset-btn" onclick="setPreset('military')">🚁 Military</button> 419 - <button class="preset-btn" onclick="setPreset('commercial')">🛫 Commercial</button> 420 - <button class="preset-btn" onclick="setPreset('ga')">🛩️ General Aviation</button> 421 - <button class="preset-btn" onclick="clearFilters()">❌ Clear</button> 422 - </div> 423 - </div> 424 - </details> 425 - </div> 426 - 427 - <div class="flights" id="flightsContainer"> 428 - <div class="no-flights"> 429 - Press "Check Flights Now" to search for aircraft overhead 430 - </div> 431 - </div> 432 - </div> 433 - 434 - <script> 435 - let currentPosition = null; 436 - let autoCheckInterval = null; 437 - let lastFlightIds = new Set(); 438 - 439 - // Check if already logged in 440 - const authToken = localStorage.getItem('authToken'); 441 - if (authToken) { 442 - // Verify token is still valid before redirecting 443 - fetch('/api/auth/me', { 444 - headers: { 445 - 'Authorization': `Bearer ${authToken}` 446 - } 447 - }).then(response => { 448 - if (response.ok) { 449 - window.location.href = '/app.html'; 450 - } else { 451 - // Token is invalid, remove it 452 - localStorage.removeItem('authToken'); 453 - } 454 - }).catch(() => { 455 - // Error checking auth, remove token 456 - localStorage.removeItem('authToken'); 457 - }); 458 - } 459 - 460 - // Auto-fill saved credentials 461 - const savedHandle = localStorage.getItem('bskyHandle'); 462 - const savedPassword = localStorage.getItem('bskyAppPassword'); 463 - if (savedHandle) { 464 - document.getElementById('bskyHandle').value = savedHandle; 465 - } 466 - if (savedPassword) { 467 - document.getElementById('bskyPassword').value = savedPassword; 468 - } 469 - 470 - // Handle enter key in password field 471 - document.getElementById('bskyPassword').addEventListener('keypress', (e) => { 472 - if (e.key === 'Enter') { 473 - login(); 474 - } 475 - }); 476 - 477 - async function login() { 478 - const handle = document.getElementById('bskyHandle').value; 479 - const password = document.getElementById('bskyPassword').value; 480 - const rememberMe = document.getElementById('rememberMe')?.checked; 481 - 482 - if (!handle || !password) { 483 - alert('Please enter both handle and password'); 484 - return; 485 - } 486 - 487 - try { 488 - const response = await fetch('/api/auth/login', { 489 - method: 'POST', 490 - headers: { 491 - 'Content-Type': 'application/x-www-form-urlencoded', 492 - }, 493 - body: new URLSearchParams({ 494 - username: handle, 495 - password: password 496 - }) 497 - }); 498 - 499 - if (!response.ok) { 500 - throw new Error('Invalid credentials'); 501 - } 502 - 503 - const data = await response.json(); 504 - localStorage.setItem('authToken', data.access_token); 505 - 506 - // Store credentials if remember me is checked 507 - if (rememberMe) { 508 - localStorage.setItem('bskyHandle', handle); 509 - localStorage.setItem('bskyAppPassword', password); 510 - } else { 511 - // Clear stored credentials 512 - localStorage.removeItem('bskyHandle'); 513 - localStorage.removeItem('bskyAppPassword'); 514 - } 515 - 516 - window.location.href = '/app.html'; 517 - 518 - } catch (error) { 519 - alert('Login failed: ' + error.message); 520 - } 521 - } 522 - 523 - function requestLocation() { 524 - if (!navigator.geolocation) { 525 - showError('Geolocation is not supported by your browser'); 526 - return; 527 - } 528 - 529 - const status = document.getElementById('locationStatus'); 530 - status.textContent = '📍 Getting location...'; 531 - 532 - navigator.geolocation.getCurrentPosition( 533 - (position) => { 534 - currentPosition = position.coords; 535 - status.textContent = `📍 Location: ${position.coords.latitude.toFixed(4)}, ${position.coords.longitude.toFixed(4)}`; 536 - status.classList.add('active'); 537 - }, 538 - (error) => { 539 - status.textContent = '📍 Location access denied'; 540 - status.classList.remove('active'); 541 - showError('Unable to get location: ' + error.message); 542 - } 543 - ); 544 - } 545 - 546 - function getActiveFilters() { 547 - const filters = {}; 548 - 549 - const aircraftTypes = document.getElementById('aircraftTypes').value; 550 - if (aircraftTypes) { 551 - filters.aircraft_type = aircraftTypes.split(',').map(t => t.trim()).filter(t => t); 552 - } 553 - 554 - const airlines = document.getElementById('airlines').value; 555 - if (airlines) { 556 - filters.callsign = airlines.split(',').map(a => a.trim()).filter(a => a); 557 - } 558 - 559 - const origins = document.getElementById('origins').value; 560 - if (origins) { 561 - filters.origin = origins.split(',').map(o => o.trim()).filter(o => o); 562 - } 563 - 564 - const destinations = document.getElementById('destinations').value; 565 - if (destinations) { 566 - filters.destination = destinations.split(',').map(d => d.trim()).filter(d => d); 567 - } 568 - 569 - return filters; 570 - } 571 - 572 - async function checkFlights() { 573 - if (!currentPosition) { 574 - showError('Please allow location access first'); 575 - requestLocation(); 576 - return; 577 - } 578 - 579 - const container = document.getElementById('flightsContainer'); 580 - container.innerHTML = '<div class="loading">🔍 Searching for flights...</div>'; 581 - 582 - try { 583 - const radius = document.getElementById('radiusMiles').value; 584 - const filters = getActiveFilters(); 585 - 586 - const response = await fetch('/api/check-flights', { 587 - method: 'POST', 588 - headers: { 589 - 'Content-Type': 'application/json', 590 - }, 591 - body: JSON.stringify({ 592 - latitude: currentPosition.latitude, 593 - longitude: currentPosition.longitude, 594 - radius_miles: parseFloat(radius), 595 - filters: filters 596 - }) 597 - }); 598 - 599 - if (!response.ok) { 600 - throw new Error('Failed to fetch flights'); 601 - } 602 - 603 - const data = await response.json(); 604 - displayFlights(data.flights); 605 - 606 - // Check for new flights 607 - const currentFlightIds = new Set(data.flights.map(f => f.hex)); 608 - for (const flight of data.flights) { 609 - if (!lastFlightIds.has(flight.hex)) { 610 - // New flight detected! 611 - if (lastFlightIds.size > 0) { // Don't notify on first check 612 - notifyNewFlight(flight); 613 - } 614 - } 615 - } 616 - lastFlightIds = currentFlightIds; 617 - 618 - } catch (error) { 619 - showError('Error checking flights: ' + error.message); 620 - } 621 - } 622 - 623 - function getFlightType(flight) { 624 - const type = flight.aircraft_type?.toUpperCase() || ''; 625 - const callsign = flight.callsign?.toUpperCase() || ''; 626 - 627 - // Military aircraft 628 - if (type.match(/UH|AH|CH|MH|HH|F\d|B52|C130|C17|KC|E\d|P\d|T38/) || 629 - callsign.match(/^(RCH|REACH|VIPER|EAGLE|HAWK)/)) { 630 - return 'military'; 631 - } 632 - 633 - // General aviation 634 - if (type.match(/C172|C182|PA|SR2|DA4|PC12|TBM|M20/) || 635 - flight.altitude && flight.altitude < 10000) { 636 - return 'ga'; 637 - } 638 - 639 - // Commercial 640 - if (type.match(/B7|A3|A2|CRJ|E\d{3}|MD/) || 641 - callsign.match(/^(UAL|AAL|DAL|SWA|JBU|NKS|FFT)/)) { 642 - return 'commercial'; 643 - } 644 - 645 - return ''; 646 - } 647 - 648 - function displayFlights(flights) { 649 - const container = document.getElementById('flightsContainer'); 650 - 651 - if (flights.length === 0) { 652 - container.innerHTML = '<div class="no-flights">No flights in range</div>'; 653 - return; 654 - } 655 - 656 - container.innerHTML = flights.map(flight => { 657 - const flightType = getFlightType(flight); 658 - const isNew = !lastFlightIds.has(flight.hex); 659 - const typeLabel = flightType ? `<span class="flight-type ${flightType}">${flightType}</span>` : ''; 660 - 661 - return ` 662 - <div class="flight-card ${isNew ? 'new' : ''}"> 663 - <div class="flight-header"> 664 - <div class="flight-callsign"> 665 - ${flight.callsign || 'Unknown'} 666 - ${typeLabel} 667 - </div> 668 - <div class="flight-distance">${flight.distance_miles} mi</div> 669 - </div> 670 - <div class="flight-details"> 671 - ${flight.altitude ? `<div>Altitude: ${flight.altitude.toLocaleString()} ft</div>` : ''} 672 - ${flight.ground_speed ? `<div>Speed: ${Math.round(flight.ground_speed)} kts</div>` : ''} 673 - ${flight.heading ? `<div>Heading: ${Math.round(flight.heading)}°</div>` : ''} 674 - ${flight.aircraft_type ? `<div>Aircraft: ${flight.aircraft_type}</div>` : ''} 675 - ${flight.registration ? `<div>Registration: ${flight.registration}</div>` : ''} 676 - ${flight.origin || flight.destination ? 677 - `<div>Route: ${flight.origin || '???'} → ${flight.destination || '???'}</div>` : ''} 678 - </div> 679 - <button class="dm-button" onclick='sendDM(${JSON.stringify(flight)})'> 680 - Send BlueSky DM 681 - </button> 682 - </div> 683 - `; 684 - }).join(''); 685 - } 686 - 687 - async function sendDM(flight) { 688 - const handle = document.getElementById('bskyHandle').value; 689 - if (!handle) { 690 - showError('Please enter your BlueSky handle'); 691 - return; 692 - } 693 - 694 - // Save handle for next time 695 - localStorage.setItem('bskyHandle', handle); 696 - 697 - try { 698 - const response = await fetch('/api/notify', { 699 - method: 'POST', 700 - headers: { 701 - 'Content-Type': 'application/json', 702 - }, 703 - body: JSON.stringify({ 704 - flight: flight, 705 - bsky_handle: handle 706 - }) 707 - }); 708 - 709 - if (!response.ok) { 710 - throw new Error('Failed to send DM'); 711 - } 712 - 713 - // Visual feedback 714 - alert('DM sent successfully!'); 715 - } catch (error) { 716 - showError('Error sending DM: ' + error.message); 717 - } 718 - } 719 - 720 - function toggleAutoCheck() { 721 - const checkbox = document.getElementById('autoCheck'); 722 - 723 - if (checkbox.checked) { 724 - // Start auto-checking 725 - checkFlights(); // Check immediately 726 - autoCheckInterval = setInterval(checkFlights, 30000); // Then every 30s 727 - } else { 728 - // Stop auto-checking 729 - if (autoCheckInterval) { 730 - clearInterval(autoCheckInterval); 731 - autoCheckInterval = null; 732 - } 733 - } 734 - } 735 - 736 - function showError(message) { 737 - const container = document.getElementById('flightsContainer'); 738 - container.innerHTML = `<div class="error">❌ ${message}</div>` + container.innerHTML; 739 - } 740 - 741 - function notifyNewFlight(flight) { 742 - // Browser notification if permitted 743 - if ("Notification" in window && Notification.permission === "granted") { 744 - new Notification(`✈️ New flight overhead!`, { 745 - body: `${flight.callsign || 'Unknown'} - ${flight.distance_miles} miles away`, 746 - icon: '/favicon.ico' 747 - }); 748 - } 749 - } 750 - 751 - // Request notification permission 752 - if ("Notification" in window && Notification.permission === "default") { 753 - Notification.requestPermission(); 754 - } 755 - 756 - function setPreset(preset) { 757 - const aircraftInput = document.getElementById('aircraftTypes'); 758 - const airlinesInput = document.getElementById('airlines'); 759 - 760 - switch (preset) { 761 - case 'military': 762 - aircraftInput.value = 'UH, AH, CH, F16, F18, B52, C130, C17, KC135'; 763 - airlinesInput.value = 'RCH, REACH'; 764 - break; 765 - case 'commercial': 766 - aircraftInput.value = 'B737, B747, B777, A320, A330, A350, CRJ, E175'; 767 - airlinesInput.value = 'UAL, AAL, DAL, SWA, JBU'; 768 - break; 769 - case 'ga': 770 - aircraftInput.value = 'C172, C182, PA28, SR22, DA40, PC12'; 771 - airlinesInput.value = ''; 772 - break; 773 - } 774 - 775 - document.getElementById('origins').value = ''; 776 - document.getElementById('destinations').value = ''; 777 - 778 - // Trigger a new search 779 - checkFlights(); 780 - } 781 - 782 - function clearFilters() { 783 - document.getElementById('aircraftTypes').value = ''; 784 - document.getElementById('airlines').value = ''; 785 - document.getElementById('origins').value = ''; 786 - document.getElementById('destinations').value = ''; 787 - checkFlights(); 788 - } 789 - </script> 790 - </body> 791 - 792 - </html>
-778
sandbox/flight-notifier/uv.lock
··· 1 - version = 1 2 - revision = 2 3 - requires-python = ">=3.12" 4 - 5 - [[package]] 6 - name = "alembic" 7 - version = "1.16.4" 8 - source = { registry = "https://pypi.org/simple" } 9 - dependencies = [ 10 - { name = "mako" }, 11 - { name = "sqlalchemy" }, 12 - { name = "typing-extensions" }, 13 - ] 14 - sdist = { url = "https://files.pythonhosted.org/packages/83/52/72e791b75c6b1efa803e491f7cbab78e963695e76d4ada05385252927e76/alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2", size = 1968161, upload-time = "2025-07-10T16:17:20.192Z" } 15 - wheels = [ 16 - { url = "https://files.pythonhosted.org/packages/c2/62/96b5217b742805236614f05904541000f55422a6060a90d7fd4ce26c172d/alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", size = 247026, upload-time = "2025-07-10T16:17:21.845Z" }, 17 - ] 18 - 19 - [[package]] 20 - name = "annotated-types" 21 - version = "0.7.0" 22 - source = { registry = "https://pypi.org/simple" } 23 - 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" } 24 - wheels = [ 25 - { 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" }, 26 - ] 27 - 28 - [[package]] 29 - name = "anyio" 30 - version = "4.9.0" 31 - source = { registry = "https://pypi.org/simple" } 32 - dependencies = [ 33 - { name = "idna" }, 34 - { name = "sniffio" }, 35 - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 36 - ] 37 - 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" } 38 - wheels = [ 39 - { 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" }, 40 - ] 41 - 42 - [[package]] 43 - name = "apscheduler" 44 - version = "3.11.0" 45 - source = { registry = "https://pypi.org/simple" } 46 - dependencies = [ 47 - { name = "tzlocal" }, 48 - ] 49 - sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347, upload-time = "2024-11-24T19:39:26.463Z" } 50 - wheels = [ 51 - { url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" }, 52 - ] 53 - 54 - [[package]] 55 - name = "asyncpg" 56 - version = "0.30.0" 57 - source = { registry = "https://pypi.org/simple" } 58 - sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" } 59 - wheels = [ 60 - { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162, upload-time = "2024-10-20T00:29:41.88Z" }, 61 - { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025, upload-time = "2024-10-20T00:29:43.352Z" }, 62 - { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243, upload-time = "2024-10-20T00:29:44.922Z" }, 63 - { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059, upload-time = "2024-10-20T00:29:46.891Z" }, 64 - { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596, upload-time = "2024-10-20T00:29:49.201Z" }, 65 - { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632, upload-time = "2024-10-20T00:29:50.768Z" }, 66 - { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186, upload-time = "2024-10-20T00:29:52.394Z" }, 67 - { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064, upload-time = "2024-10-20T00:29:53.757Z" }, 68 - { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload-time = "2024-10-20T00:29:55.165Z" }, 69 - { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload-time = "2024-10-20T00:29:57.14Z" }, 70 - { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload-time = "2024-10-20T00:29:58.499Z" }, 71 - { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471, upload-time = "2024-10-20T00:30:00.354Z" }, 72 - { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253, upload-time = "2024-10-20T00:30:02.794Z" }, 73 - { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720, upload-time = "2024-10-20T00:30:04.501Z" }, 74 - { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404, upload-time = "2024-10-20T00:30:06.537Z" }, 75 - { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" }, 76 - ] 77 - 78 - [[package]] 79 - name = "atproto" 80 - version = "0.0.61" 81 - source = { registry = "https://pypi.org/simple" } 82 - dependencies = [ 83 - { name = "click" }, 84 - { name = "cryptography" }, 85 - { name = "dnspython" }, 86 - { name = "httpx" }, 87 - { name = "libipld" }, 88 - { name = "pydantic" }, 89 - { name = "typing-extensions" }, 90 - { name = "websockets" }, 91 - ] 92 - sdist = { url = "https://files.pythonhosted.org/packages/b1/59/6f5074b3a45e0e3c1853544240e9039e86219feb30ff1bb5e8582c791547/atproto-0.0.61.tar.gz", hash = "sha256:98e022daf538d14f134ce7c91d42c4c973f3493ac56e43a84daa4c881f102beb", size = 189208, upload-time = "2025-04-19T00:20:11.918Z" } 93 - wheels = [ 94 - { url = "https://files.pythonhosted.org/packages/bd/b6/da9963bf54d4c0a8a590b6297d8858c395243dbb04cb581fdadb5fe7eac7/atproto-0.0.61-py3-none-any.whl", hash = "sha256:658da5832aaeea4a12a9a74235f9c90c11453e77d596fdccb1f8b39d56245b88", size = 380426, upload-time = "2025-04-19T00:20:10.026Z" }, 95 - ] 96 - 97 - [[package]] 98 - name = "certifi" 99 - version = "2025.8.3" 100 - source = { registry = "https://pypi.org/simple" } 101 - sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } 102 - wheels = [ 103 - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, 104 - ] 105 - 106 - [[package]] 107 - name = "cffi" 108 - version = "1.17.1" 109 - source = { registry = "https://pypi.org/simple" } 110 - dependencies = [ 111 - { name = "pycparser" }, 112 - ] 113 - sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } 114 - wheels = [ 115 - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, 116 - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, 117 - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, 118 - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, 119 - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, 120 - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, 121 - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, 122 - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, 123 - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, 124 - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, 125 - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, 126 - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, 127 - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, 128 - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, 129 - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, 130 - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, 131 - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, 132 - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, 133 - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, 134 - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, 135 - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, 136 - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, 137 - ] 138 - 139 - [[package]] 140 - name = "click" 141 - version = "8.2.1" 142 - source = { registry = "https://pypi.org/simple" } 143 - dependencies = [ 144 - { name = "colorama", marker = "sys_platform == 'win32'" }, 145 - ] 146 - 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" } 147 - wheels = [ 148 - { 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" }, 149 - ] 150 - 151 - [[package]] 152 - name = "colorama" 153 - version = "0.4.6" 154 - source = { registry = "https://pypi.org/simple" } 155 - 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" } 156 - wheels = [ 157 - { 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" }, 158 - ] 159 - 160 - [[package]] 161 - name = "cryptography" 162 - version = "45.0.5" 163 - source = { registry = "https://pypi.org/simple" } 164 - dependencies = [ 165 - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, 166 - ] 167 - sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } 168 - wheels = [ 169 - { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, 170 - { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, 171 - { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, 172 - { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, 173 - { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, 174 - { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, 175 - { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, 176 - { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, 177 - { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, 178 - { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, 179 - { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, 180 - { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, 181 - { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, 182 - { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, 183 - { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, 184 - { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, 185 - { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, 186 - { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, 187 - { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, 188 - { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, 189 - { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, 190 - { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, 191 - { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, 192 - { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, 193 - ] 194 - 195 - [[package]] 196 - name = "dnspython" 197 - version = "2.7.0" 198 - source = { registry = "https://pypi.org/simple" } 199 - sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } 200 - wheels = [ 201 - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, 202 - ] 203 - 204 - [[package]] 205 - name = "ecdsa" 206 - version = "0.19.1" 207 - source = { registry = "https://pypi.org/simple" } 208 - dependencies = [ 209 - { name = "six" }, 210 - ] 211 - sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" } 212 - wheels = [ 213 - { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, 214 - ] 215 - 216 - [[package]] 217 - name = "fastapi" 218 - version = "0.116.1" 219 - source = { registry = "https://pypi.org/simple" } 220 - dependencies = [ 221 - { name = "pydantic" }, 222 - { name = "starlette" }, 223 - { name = "typing-extensions" }, 224 - ] 225 - 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" } 226 - wheels = [ 227 - { 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" }, 228 - ] 229 - 230 - [[package]] 231 - name = "flight-notifier" 232 - version = "0.1.0" 233 - source = { editable = "." } 234 - dependencies = [ 235 - { name = "alembic" }, 236 - { name = "apscheduler" }, 237 - { name = "asyncpg" }, 238 - { name = "atproto" }, 239 - { name = "fastapi" }, 240 - { name = "geopy" }, 241 - { name = "httpx" }, 242 - { name = "jinja2" }, 243 - { name = "pydantic-settings" }, 244 - { name = "python-jose", extra = ["cryptography"] }, 245 - { name = "python-multipart" }, 246 - { name = "sqlalchemy" }, 247 - { name = "uvicorn" }, 248 - ] 249 - 250 - [package.dev-dependencies] 251 - dev = [ 252 - { name = "ruff" }, 253 - ] 254 - 255 - [package.metadata] 256 - requires-dist = [ 257 - { name = "alembic" }, 258 - { name = "apscheduler" }, 259 - { name = "asyncpg" }, 260 - { name = "atproto" }, 261 - { name = "fastapi" }, 262 - { name = "geopy" }, 263 - { name = "httpx" }, 264 - { name = "jinja2" }, 265 - { name = "pydantic-settings" }, 266 - { name = "python-jose", extras = ["cryptography"] }, 267 - { name = "python-multipart" }, 268 - { name = "sqlalchemy" }, 269 - { name = "uvicorn" }, 270 - ] 271 - 272 - [package.metadata.requires-dev] 273 - dev = [{ name = "ruff" }] 274 - 275 - [[package]] 276 - name = "geographiclib" 277 - version = "2.0" 278 - source = { registry = "https://pypi.org/simple" } 279 - sdist = { url = "https://files.pythonhosted.org/packages/96/cd/90271fd195d79a9c2af0ca21632b297a6cc3e852e0413a2e4519e67be213/geographiclib-2.0.tar.gz", hash = "sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859", size = 36720, upload-time = "2022-04-23T13:01:11.495Z" } 280 - wheels = [ 281 - { url = "https://files.pythonhosted.org/packages/9f/5a/a26132406f1f40cf51ea349a5f11b0a46cec02a2031ff82e391c2537247a/geographiclib-2.0-py3-none-any.whl", hash = "sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734", size = 40324, upload-time = "2022-04-23T13:01:09.958Z" }, 282 - ] 283 - 284 - [[package]] 285 - name = "geopy" 286 - version = "2.4.1" 287 - source = { registry = "https://pypi.org/simple" } 288 - dependencies = [ 289 - { name = "geographiclib" }, 290 - ] 291 - sdist = { url = "https://files.pythonhosted.org/packages/0e/fd/ef6d53875ceab72c1fad22dbed5ec1ad04eb378c2251a6a8024bad890c3b/geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1", size = 117625, upload-time = "2023-11-23T21:49:32.734Z" } 292 - wheels = [ 293 - { url = "https://files.pythonhosted.org/packages/e5/15/cf2a69ade4b194aa524ac75112d5caac37414b20a3a03e6865dfe0bd1539/geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7", size = 125437, upload-time = "2023-11-23T21:49:30.421Z" }, 294 - ] 295 - 296 - [[package]] 297 - name = "greenlet" 298 - version = "3.2.3" 299 - source = { registry = "https://pypi.org/simple" } 300 - sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } 301 - wheels = [ 302 - { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, 303 - { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, 304 - { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, 305 - { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, 306 - { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, 307 - { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, 308 - { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, 309 - { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, 310 - { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, 311 - { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, 312 - { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, 313 - { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, 314 - { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, 315 - { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, 316 - { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, 317 - { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, 318 - { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, 319 - { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, 320 - { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, 321 - { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, 322 - { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, 323 - { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, 324 - { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, 325 - { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, 326 - { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, 327 - ] 328 - 329 - [[package]] 330 - name = "h11" 331 - version = "0.16.0" 332 - source = { registry = "https://pypi.org/simple" } 333 - 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" } 334 - wheels = [ 335 - { 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" }, 336 - ] 337 - 338 - [[package]] 339 - name = "httpcore" 340 - version = "1.0.9" 341 - source = { registry = "https://pypi.org/simple" } 342 - dependencies = [ 343 - { name = "certifi" }, 344 - { name = "h11" }, 345 - ] 346 - sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 347 - wheels = [ 348 - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 349 - ] 350 - 351 - [[package]] 352 - name = "httpx" 353 - version = "0.28.1" 354 - source = { registry = "https://pypi.org/simple" } 355 - dependencies = [ 356 - { name = "anyio" }, 357 - { name = "certifi" }, 358 - { name = "httpcore" }, 359 - { name = "idna" }, 360 - ] 361 - sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 362 - wheels = [ 363 - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 364 - ] 365 - 366 - [[package]] 367 - name = "idna" 368 - version = "3.10" 369 - source = { registry = "https://pypi.org/simple" } 370 - 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" } 371 - wheels = [ 372 - { 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" }, 373 - ] 374 - 375 - [[package]] 376 - name = "jinja2" 377 - version = "3.1.6" 378 - source = { registry = "https://pypi.org/simple" } 379 - dependencies = [ 380 - { name = "markupsafe" }, 381 - ] 382 - sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } 383 - wheels = [ 384 - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, 385 - ] 386 - 387 - [[package]] 388 - name = "libipld" 389 - version = "3.1.1" 390 - source = { registry = "https://pypi.org/simple" } 391 - sdist = { url = "https://files.pythonhosted.org/packages/84/ac/21f2b0f9848c9d99a87e3cc626e7af0fc24883911ec5d7578686cc2a09d1/libipld-3.1.1.tar.gz", hash = "sha256:4b9a9da0ea5d848e9fa12c700027619a1e37ecc1da39dbd1424c0e9062f29e44", size = 4380425, upload-time = "2025-06-24T23:12:51.395Z" } 392 - wheels = [ 393 - { url = "https://files.pythonhosted.org/packages/fe/07/975b9dde7e27489218c21db4357bd852cd71c388c06abedcff2b86a500ab/libipld-3.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27d2fb2b19a9784a932a41fd1a6942361cfa65e0957871f4bde06c81639a32b1", size = 279659, upload-time = "2025-06-24T23:11:29.139Z" }, 394 - { url = "https://files.pythonhosted.org/packages/4d/db/bd6a9eefa7c90f23ea2ea98678e8f6aac15fedb9645ddaa8af977bcfdf2f/libipld-3.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f0156a9bf04b7f575b907b7a15b902dde2d8af129aeb161b3ab6940f3fd9c02", size = 276397, upload-time = "2025-06-24T23:11:30.54Z" }, 395 - { url = "https://files.pythonhosted.org/packages/02/a8/09606bc7139173d8543cf8206b3c7ff9238bd4c9b47a71565c50912f0323/libipld-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29cf371122648a688f87fe3307bcfe2c6a4aefa184ba44126f066975cfd26b46", size = 297682, upload-time = "2025-06-24T23:11:31.833Z" }, 396 - { url = "https://files.pythonhosted.org/packages/31/ad/a54d62baead5aecc9a2f48ab2b8ac81fbeb8df19c89416735387dd041175/libipld-3.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5463672cd0708d47bc8cfe1cc0dd95c55d5b7f3947027e0e9c6a13b1dc1b6d0", size = 304615, upload-time = "2025-06-24T23:11:32.8Z" }, 397 - { url = "https://files.pythonhosted.org/packages/c5/a2/3c7908d6aa865721e7e9c2f125e315614cee4e4ced4457d7b22cc8d8acc4/libipld-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27a1b9b9392679fb494214bfa350adf7447b43bc39e497b669307da1f6dc8dd5", size = 332042, upload-time = "2025-06-24T23:11:33.831Z" }, 398 - { url = "https://files.pythonhosted.org/packages/e1/c0/ecd838e32630439ca3d8ce2274db32c77f31d0265c01b6a3c00fd96367bb/libipld-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a83d944c71ed50772a7cef3f14e3ef3cf93145c82963b9e49a85cd9ee0ba9878", size = 344326, upload-time = "2025-06-24T23:11:34.768Z" }, 399 - { url = "https://files.pythonhosted.org/packages/98/79/9ef27cd284c66e7e9481e7fe529d1412ea751b4cad1578571bbc02826098/libipld-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb9fef573406f7134727e0561e42fd221721800ed01d47f1207916595b72e780", size = 299195, upload-time = "2025-06-24T23:11:35.973Z" }, 400 - { url = "https://files.pythonhosted.org/packages/a7/6e/2db9510cdc410b154169438449277637f35bbc571c330d60d262320e6d77/libipld-3.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:485b21bdddbe7a3bb8f33f1d0b9998343bd82a578406e31f85899b031602d34d", size = 323946, upload-time = "2025-06-24T23:11:37.815Z" }, 401 - { url = "https://files.pythonhosted.org/packages/63/fb/ac59473cbc7598db0e194b2b14b10953029813f204555e5c12405b265594/libipld-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fe6fa67a242755773f3e960163010bdbc797316ca782d387e6b128e0d3bca19", size = 477366, upload-time = "2025-06-24T23:11:38.798Z" }, 402 - { url = "https://files.pythonhosted.org/packages/f5/75/80915af5dc04785ff7a9468529a96d787723d24a9e76dbc31e0141bbcd23/libipld-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:38298cbea4f8308bb848c7f8c3d8e41cd2c9235ef8bca6adefd2a002e94287ff", size = 470106, upload-time = "2025-06-24T23:11:39.786Z" }, 403 - { url = "https://files.pythonhosted.org/packages/9e/17/832f1c91938a0e2d58905e86c7a2f21cd4b6334a3757221563bd9a8beb64/libipld-3.1.1-cp312-cp312-win32.whl", hash = "sha256:1bc228298e249baac85f702da7d1e23ee429529a078a6bdf09570168f53fcb0f", size = 173435, upload-time = "2025-06-24T23:11:41.072Z" }, 404 - { url = "https://files.pythonhosted.org/packages/14/62/1006fa794c6fe18040d06cebe2d593c20208c2a16a5eb01f7d4f48a5a3b5/libipld-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a393e1809c7b1aa67c6f6c5d701787298f507448a601b8ec825b6ae26084fbad", size = 179271, upload-time = "2025-06-24T23:11:42.155Z" }, 405 - { url = "https://files.pythonhosted.org/packages/bc/af/95b2673bd8ab8225a374bde34b4ac21ef9a725c910517e0dadc5ce26d4a7/libipld-3.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7ad7870d2ee609d74eec4ba6dbc2caef0357861b3e0944226272f0e91f016d37", size = 169727, upload-time = "2025-06-24T23:11:43.164Z" }, 406 - { url = "https://files.pythonhosted.org/packages/e5/25/52f27b9617efb0c2f60e71bbfd4f88167ca7acd3aed413999f16e22b3e54/libipld-3.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8cd7d7b8b2e0a6ab273b697259f291edbd7cb1b9200ed746a41dcd63fb52017a", size = 280260, upload-time = "2025-06-24T23:11:44.376Z" }, 407 - { url = "https://files.pythonhosted.org/packages/bb/14/123450261a35e869732ff610580df39a62164d9e0aab58334c182c9453f8/libipld-3.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0251c6daa8eceee2ce7dc4f03422f3f1acdd31b04ebda39cab5f8af3dae30943", size = 276684, upload-time = "2025-06-24T23:11:45.266Z" }, 408 - { url = "https://files.pythonhosted.org/packages/bd/3e/6dd2daf43ff735a3f53cbeaeac1edb3ba92fa2e48c64257800ede82442e6/libipld-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d4598b094286998f770f383eedbfc04c1018ec8ebe6746db0eff5b2059a484a", size = 297845, upload-time = "2025-06-24T23:11:46.143Z" }, 409 - { url = "https://files.pythonhosted.org/packages/83/23/e4f89d9bf854c58a5d6e2f2c667425669ed795956003b28de429b0740e0f/libipld-3.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7212411cbce495dfae24c2b6757a5c2f921797fe70ec0c026e1a2d19ae29e59a", size = 305200, upload-time = "2025-06-24T23:11:47.128Z" }, 410 - { url = "https://files.pythonhosted.org/packages/40/43/0b1e871275502e9799589d03a139730c0dfbb36d1922ab213b105ace59ee/libipld-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffc2f978adda8a8309b55510ceda9fe5dc2431d4ff202ff77d84eb57c77d072f", size = 332153, upload-time = "2025-06-24T23:11:48.437Z" }, 411 - { url = "https://files.pythonhosted.org/packages/94/18/5e9cff31d9450e98cc7b4025d1c90bde661ee099ea46cfcb1d8a893e6083/libipld-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99163cc7822abcb028c55860e5341c77200a3ae90f4c158c27e2118a07e8809d", size = 344391, upload-time = "2025-06-24T23:11:49.786Z" }, 412 - { url = "https://files.pythonhosted.org/packages/63/ca/4d938862912ab2f105710d1cc909ec65c71d0e63a90e3b494920c23a4383/libipld-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80f142cbd4fa89ef514a4dd43afbd4ed3c33ae7061f0e1e0763f7c1811dea389", size = 299448, upload-time = "2025-06-24T23:11:50.723Z" }, 413 - { url = "https://files.pythonhosted.org/packages/2a/08/f6020e53abe4c26d57fe29b001ba1a84b5b3ad2d618e135b82877e42b59a/libipld-3.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4596a6e2c5e81b14b1432f3a6120b1d785fc4f74402cef39accf0041999905e4", size = 324096, upload-time = "2025-06-24T23:11:51.646Z" }, 414 - { url = "https://files.pythonhosted.org/packages/df/0f/d3d9da8f1001e9856bc5cb171a838ca5102da7d959b870a0c5f5aa9ef82e/libipld-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0cd275603ab3cc2394d40455de6976f01b2d85b4095c074c0c1e2692013f5eaa", size = 477593, upload-time = "2025-06-24T23:11:52.565Z" }, 415 - { url = "https://files.pythonhosted.org/packages/59/df/57dcd84e55c02f74bb40a246dd849430994bbb476e91b05179d749993c9a/libipld-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:16c999b3af996865004ff2da8280d0c24b672d8a00f9e4cd3a468da8f5e63a5a", size = 470201, upload-time = "2025-06-24T23:11:53.544Z" }, 416 - { url = "https://files.pythonhosted.org/packages/80/af/aee0800b415b63dc5e259675c31a36d6c261afff8e288b56bc2867aa9310/libipld-3.1.1-cp313-cp313-win32.whl", hash = "sha256:5d34c40a27e8755f500277be5268a2f6b6f0d1e20599152d8a34cd34fb3f2700", size = 173730, upload-time = "2025-06-24T23:11:54.5Z" }, 417 - { url = "https://files.pythonhosted.org/packages/54/a3/7e447f27ee896f48332254bb38e1b6c1d3f24b13e5029977646de9408159/libipld-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:5edee5f2ea8183bb6a151f149c9798a4f1db69fe16307e860a84f8d41b53665a", size = 179409, upload-time = "2025-06-24T23:11:55.356Z" }, 418 - { url = "https://files.pythonhosted.org/packages/f2/0b/31d6097620c5cfaaaa0acb7760c29186029cd72c6ab81c537cc1ddfb34e5/libipld-3.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:7307876987d9e570dcaf17a15f0ba210f678b323860742d725cf6d8d8baeae1f", size = 169715, upload-time = "2025-06-24T23:11:56.41Z" }, 419 - ] 420 - 421 - [[package]] 422 - name = "mako" 423 - version = "1.3.10" 424 - source = { registry = "https://pypi.org/simple" } 425 - dependencies = [ 426 - { name = "markupsafe" }, 427 - ] 428 - sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } 429 - wheels = [ 430 - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, 431 - ] 432 - 433 - [[package]] 434 - name = "markupsafe" 435 - version = "3.0.2" 436 - source = { registry = "https://pypi.org/simple" } 437 - sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } 438 - wheels = [ 439 - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, 440 - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, 441 - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, 442 - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, 443 - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, 444 - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, 445 - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, 446 - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, 447 - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, 448 - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, 449 - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, 450 - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, 451 - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, 452 - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, 453 - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, 454 - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, 455 - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, 456 - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, 457 - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, 458 - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, 459 - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, 460 - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, 461 - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, 462 - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, 463 - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, 464 - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, 465 - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, 466 - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, 467 - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, 468 - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, 469 - ] 470 - 471 - [[package]] 472 - name = "pyasn1" 473 - version = "0.6.1" 474 - source = { registry = "https://pypi.org/simple" } 475 - sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } 476 - wheels = [ 477 - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, 478 - ] 479 - 480 - [[package]] 481 - name = "pycparser" 482 - version = "2.22" 483 - source = { registry = "https://pypi.org/simple" } 484 - sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } 485 - wheels = [ 486 - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, 487 - ] 488 - 489 - [[package]] 490 - name = "pydantic" 491 - version = "2.11.7" 492 - source = { registry = "https://pypi.org/simple" } 493 - dependencies = [ 494 - { name = "annotated-types" }, 495 - { name = "pydantic-core" }, 496 - { name = "typing-extensions" }, 497 - { name = "typing-inspection" }, 498 - ] 499 - 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" } 500 - wheels = [ 501 - { 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" }, 502 - ] 503 - 504 - [[package]] 505 - name = "pydantic-core" 506 - version = "2.33.2" 507 - source = { registry = "https://pypi.org/simple" } 508 - dependencies = [ 509 - { name = "typing-extensions" }, 510 - ] 511 - 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" } 512 - wheels = [ 513 - { 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" }, 514 - { 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" }, 515 - { 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" }, 516 - { 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" }, 517 - { 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" }, 518 - { 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" }, 519 - { 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" }, 520 - { 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" }, 521 - { 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" }, 522 - { 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" }, 523 - { 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" }, 524 - { 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" }, 525 - { 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" }, 526 - { 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" }, 527 - { 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" }, 528 - { 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" }, 529 - { 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" }, 530 - { 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" }, 531 - { 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" }, 532 - { 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" }, 533 - { 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" }, 534 - { 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" }, 535 - { 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" }, 536 - { 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" }, 537 - { 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" }, 538 - { 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" }, 539 - { 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" }, 540 - { 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" }, 541 - { 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" }, 542 - { 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" }, 543 - { 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" }, 544 - ] 545 - 546 - [[package]] 547 - name = "pydantic-settings" 548 - version = "2.10.1" 549 - source = { registry = "https://pypi.org/simple" } 550 - dependencies = [ 551 - { name = "pydantic" }, 552 - { name = "python-dotenv" }, 553 - { name = "typing-inspection" }, 554 - ] 555 - sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } 556 - wheels = [ 557 - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, 558 - ] 559 - 560 - [[package]] 561 - name = "python-dotenv" 562 - version = "1.1.1" 563 - source = { registry = "https://pypi.org/simple" } 564 - sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } 565 - wheels = [ 566 - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, 567 - ] 568 - 569 - [[package]] 570 - name = "python-jose" 571 - version = "3.5.0" 572 - source = { registry = "https://pypi.org/simple" } 573 - dependencies = [ 574 - { name = "ecdsa" }, 575 - { name = "pyasn1" }, 576 - { name = "rsa" }, 577 - ] 578 - sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } 579 - wheels = [ 580 - { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, 581 - ] 582 - 583 - [package.optional-dependencies] 584 - cryptography = [ 585 - { name = "cryptography" }, 586 - ] 587 - 588 - [[package]] 589 - name = "python-multipart" 590 - version = "0.0.20" 591 - source = { registry = "https://pypi.org/simple" } 592 - sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } 593 - wheels = [ 594 - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, 595 - ] 596 - 597 - [[package]] 598 - name = "rsa" 599 - version = "4.9.1" 600 - source = { registry = "https://pypi.org/simple" } 601 - dependencies = [ 602 - { name = "pyasn1" }, 603 - ] 604 - sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } 605 - wheels = [ 606 - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, 607 - ] 608 - 609 - [[package]] 610 - name = "ruff" 611 - version = "0.12.7" 612 - source = { registry = "https://pypi.org/simple" } 613 - sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } 614 - wheels = [ 615 - { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, 616 - { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, 617 - { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, 618 - { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, 619 - { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, 620 - { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, 621 - { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, 622 - { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, 623 - { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, 624 - { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, 625 - { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, 626 - { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, 627 - { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, 628 - { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, 629 - { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, 630 - { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, 631 - { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, 632 - ] 633 - 634 - [[package]] 635 - name = "six" 636 - version = "1.17.0" 637 - source = { registry = "https://pypi.org/simple" } 638 - 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" } 639 - wheels = [ 640 - { 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" }, 641 - ] 642 - 643 - [[package]] 644 - name = "sniffio" 645 - version = "1.3.1" 646 - source = { registry = "https://pypi.org/simple" } 647 - 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" } 648 - wheels = [ 649 - { 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" }, 650 - ] 651 - 652 - [[package]] 653 - name = "sqlalchemy" 654 - version = "2.0.42" 655 - source = { registry = "https://pypi.org/simple" } 656 - dependencies = [ 657 - { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, 658 - { name = "typing-extensions" }, 659 - ] 660 - sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972, upload-time = "2025-07-29T12:48:09.323Z" } 661 - wheels = [ 662 - { url = "https://files.pythonhosted.org/packages/61/66/ac31a9821fc70a7376321fb2c70fdd7eadbc06dadf66ee216a22a41d6058/sqlalchemy-2.0.42-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:09637a0872689d3eb71c41e249c6f422e3e18bbd05b4cd258193cfc7a9a50da2", size = 2132203, upload-time = "2025-07-29T13:29:19.291Z" }, 663 - { url = "https://files.pythonhosted.org/packages/fc/ba/fd943172e017f955d7a8b3a94695265b7114efe4854feaa01f057e8f5293/sqlalchemy-2.0.42-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3cb3ec67cc08bea54e06b569398ae21623534a7b1b23c258883a7c696ae10df", size = 2120373, upload-time = "2025-07-29T13:29:21.049Z" }, 664 - { url = "https://files.pythonhosted.org/packages/ea/a2/b5f7d233d063ffadf7e9fff3898b42657ba154a5bec95a96f44cba7f818b/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87e6a5ef6f9d8daeb2ce5918bf5fddecc11cae6a7d7a671fcc4616c47635e01", size = 3317685, upload-time = "2025-07-29T13:26:40.837Z" }, 665 - { url = "https://files.pythonhosted.org/packages/86/00/fcd8daab13a9119d41f3e485a101c29f5d2085bda459154ba354c616bf4e/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b718011a9d66c0d2f78e1997755cd965f3414563b31867475e9bc6efdc2281d", size = 3326967, upload-time = "2025-07-29T13:22:31.009Z" }, 666 - { url = "https://files.pythonhosted.org/packages/a3/85/e622a273d648d39d6771157961956991a6d760e323e273d15e9704c30ccc/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:16d9b544873fe6486dddbb859501a07d89f77c61d29060bb87d0faf7519b6a4d", size = 3255331, upload-time = "2025-07-29T13:26:42.579Z" }, 667 - { url = "https://files.pythonhosted.org/packages/3a/a0/2c2338b592c7b0a61feffd005378c084b4c01fabaf1ed5f655ab7bd446f0/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21bfdf57abf72fa89b97dd74d3187caa3172a78c125f2144764a73970810c4ee", size = 3291791, upload-time = "2025-07-29T13:22:32.454Z" }, 668 - { url = "https://files.pythonhosted.org/packages/41/19/b8a2907972a78285fdce4c880ecaab3c5067eb726882ca6347f7a4bf64f6/sqlalchemy-2.0.42-cp312-cp312-win32.whl", hash = "sha256:78b46555b730a24901ceb4cb901c6b45c9407f8875209ed3c5d6bcd0390a6ed1", size = 2096180, upload-time = "2025-07-29T13:16:08.952Z" }, 669 - { url = "https://files.pythonhosted.org/packages/48/1f/67a78f3dfd08a2ed1c7be820fe7775944f5126080b5027cc859084f8e223/sqlalchemy-2.0.42-cp312-cp312-win_amd64.whl", hash = "sha256:4c94447a016f36c4da80072e6c6964713b0af3c8019e9c4daadf21f61b81ab53", size = 2123533, upload-time = "2025-07-29T13:16:11.705Z" }, 670 - { url = "https://files.pythonhosted.org/packages/e9/7e/25d8c28b86730c9fb0e09156f601d7a96d1c634043bf8ba36513eb78887b/sqlalchemy-2.0.42-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941804f55c7d507334da38133268e3f6e5b0340d584ba0f277dd884197f4ae8c", size = 2127905, upload-time = "2025-07-29T13:29:22.249Z" }, 671 - { url = "https://files.pythonhosted.org/packages/e5/a1/9d8c93434d1d983880d976400fcb7895a79576bd94dca61c3b7b90b1ed0d/sqlalchemy-2.0.42-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d3d06a968a760ce2aa6a5889fefcbdd53ca935735e0768e1db046ec08cbf01", size = 2115726, upload-time = "2025-07-29T13:29:23.496Z" }, 672 - { url = "https://files.pythonhosted.org/packages/a2/cc/d33646fcc24c87cc4e30a03556b611a4e7bcfa69a4c935bffb923e3c89f4/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cf10396a8a700a0f38ccd220d940be529c8f64435c5d5b29375acab9267a6c9", size = 3246007, upload-time = "2025-07-29T13:26:44.166Z" }, 673 - { url = "https://files.pythonhosted.org/packages/67/08/4e6c533d4c7f5e7c4cbb6fe8a2c4e813202a40f05700d4009a44ec6e236d/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae6c2b05326d7c2c7c0519f323f90e0fb9e8afa783c6a05bb9ee92a90d0f04", size = 3250919, upload-time = "2025-07-29T13:22:33.74Z" }, 674 - { url = "https://files.pythonhosted.org/packages/5c/82/f680e9a636d217aece1b9a8030d18ad2b59b5e216e0c94e03ad86b344af3/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f50f7b20677b23cfb35b6afcd8372b2feb348a38e3033f6447ee0704540be894", size = 3180546, upload-time = "2025-07-29T13:26:45.648Z" }, 675 - { url = "https://files.pythonhosted.org/packages/7d/a2/8c8f6325f153894afa3775584c429cc936353fb1db26eddb60a549d0ff4b/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d88a1c0d66d24e229e3938e1ef16ebdbd2bf4ced93af6eff55225f7465cf350", size = 3216683, upload-time = "2025-07-29T13:22:34.977Z" }, 676 - { url = "https://files.pythonhosted.org/packages/39/44/3a451d7fa4482a8ffdf364e803ddc2cfcafc1c4635fb366f169ecc2c3b11/sqlalchemy-2.0.42-cp313-cp313-win32.whl", hash = "sha256:45c842c94c9ad546c72225a0c0d1ae8ef3f7c212484be3d429715a062970e87f", size = 2093990, upload-time = "2025-07-29T13:16:13.036Z" }, 677 - { url = "https://files.pythonhosted.org/packages/4b/9e/9bce34f67aea0251c8ac104f7bdb2229d58fb2e86a4ad8807999c4bee34b/sqlalchemy-2.0.42-cp313-cp313-win_amd64.whl", hash = "sha256:eb9905f7f1e49fd57a7ed6269bc567fcbbdac9feadff20ad6bd7707266a91577", size = 2120473, upload-time = "2025-07-29T13:16:14.502Z" }, 678 - { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072, upload-time = "2025-07-29T13:09:17.061Z" }, 679 - ] 680 - 681 - [[package]] 682 - name = "starlette" 683 - version = "0.47.2" 684 - source = { registry = "https://pypi.org/simple" } 685 - dependencies = [ 686 - { name = "anyio" }, 687 - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 688 - ] 689 - 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" } 690 - wheels = [ 691 - { 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" }, 692 - ] 693 - 694 - [[package]] 695 - name = "typing-extensions" 696 - version = "4.14.1" 697 - source = { registry = "https://pypi.org/simple" } 698 - 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" } 699 - wheels = [ 700 - { 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" }, 701 - ] 702 - 703 - [[package]] 704 - name = "typing-inspection" 705 - version = "0.4.1" 706 - source = { registry = "https://pypi.org/simple" } 707 - dependencies = [ 708 - { name = "typing-extensions" }, 709 - ] 710 - 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" } 711 - wheels = [ 712 - { 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" }, 713 - ] 714 - 715 - [[package]] 716 - name = "tzdata" 717 - version = "2025.2" 718 - source = { registry = "https://pypi.org/simple" } 719 - sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } 720 - wheels = [ 721 - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, 722 - ] 723 - 724 - [[package]] 725 - name = "tzlocal" 726 - version = "5.3.1" 727 - source = { registry = "https://pypi.org/simple" } 728 - dependencies = [ 729 - { name = "tzdata", marker = "sys_platform == 'win32'" }, 730 - ] 731 - sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } 732 - wheels = [ 733 - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, 734 - ] 735 - 736 - [[package]] 737 - name = "uvicorn" 738 - version = "0.35.0" 739 - source = { registry = "https://pypi.org/simple" } 740 - dependencies = [ 741 - { name = "click" }, 742 - { name = "h11" }, 743 - ] 744 - 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" } 745 - wheels = [ 746 - { 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" }, 747 - ] 748 - 749 - [[package]] 750 - name = "websockets" 751 - version = "13.1" 752 - source = { registry = "https://pypi.org/simple" } 753 - sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } 754 - wheels = [ 755 - { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" }, 756 - { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" }, 757 - { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" }, 758 - { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647, upload-time = "2024-09-21T17:33:00.495Z" }, 759 - { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592, upload-time = "2024-09-21T17:33:02.223Z" }, 760 - { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012, upload-time = "2024-09-21T17:33:03.288Z" }, 761 - { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311, upload-time = "2024-09-21T17:33:04.728Z" }, 762 - { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692, upload-time = "2024-09-21T17:33:05.829Z" }, 763 - { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686, upload-time = "2024-09-21T17:33:06.823Z" }, 764 - { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712, upload-time = "2024-09-21T17:33:07.877Z" }, 765 - { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145, upload-time = "2024-09-21T17:33:09.202Z" }, 766 - { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828, upload-time = "2024-09-21T17:33:10.987Z" }, 767 - { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487, upload-time = "2024-09-21T17:33:12.153Z" }, 768 - { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721, upload-time = "2024-09-21T17:33:13.909Z" }, 769 - { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609, upload-time = "2024-09-21T17:33:14.967Z" }, 770 - { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556, upload-time = "2024-09-21T17:33:17.113Z" }, 771 - { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993, upload-time = "2024-09-21T17:33:18.168Z" }, 772 - { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360, upload-time = "2024-09-21T17:33:19.233Z" }, 773 - { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745, upload-time = "2024-09-21T17:33:20.361Z" }, 774 - { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, 775 - { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, 776 - { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, 777 - { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, 778 - ]