-10
sandbox/flight-notifier/.dockerignore
-10
sandbox/flight-notifier/.dockerignore
-13
sandbox/flight-notifier/.env
-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
-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
-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
-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
sandbox/flight-notifier/flight_notifier.db
This is a binary file and will not be displayed.
-29
sandbox/flight-notifier/fly.toml
-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
-17
sandbox/flight-notifier/justfile
-31
sandbox/flight-notifier/pyproject.toml
-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
-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
-3
sandbox/flight-notifier/src/flight_notifier/__init__.py
sandbox/flight-notifier/src/flight_notifier/__pycache__/__init__.cpython-312.pyc
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
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
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
-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
-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
-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
-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
-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
-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
-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
-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
-
]