diff --git a/eta_prediction/README.md b/eta_prediction/README.md new file mode 100644 index 0000000..291a5b5 --- /dev/null +++ b/eta_prediction/README.md @@ -0,0 +1,59 @@ +# GTFS-RT Tools + +Utilities for exploring **GTFS-Realtime** feeds such as Vehicle Positions, Trip Updates, and Alerts. +This repository is part of experiments with the **bUCR Realtime feeds** and other open transit data sources. + +--- + +## Installation + +For installation and dependency management we are using [uv](https://github.com/astral-sh/uv), a fast drop-in replacement for pip and venv. + +1. **Install uv** (if you don’t already have it): + + ```bash + curl -LsSf https://astral.sh/uv/install.sh | sh + ``` + +2. **Clone this repository**: + + ```bash + git clone https://github.com/simovilab/gtfs-django.git + cd eta-prediction + ``` + +3. **Install dependencies** from `pyproject.toml`: + + ```bash + uv sync + ``` + +This will create a virtual environment and install: + +- [`gtfs-realtime-bindings`](https://github.com/MobilityData/gtfs-realtime-bindings) (protobuf definitions for GTFS-RT) +- [`requests`](https://docs.python-requests.org/) + +--- + +## Usage + +Example: fetch and print the first 10 vehicle positions. + +```bash +uv run gtfs_rt_bindings_VP.py +``` +--- + +## Switching Feeds + +Inside each script, change the `URL` variable to any GTFS-RT VehiclePositions endpoint. + +Examples: +- bUCR (default): + ``` + https://databus.bucr.digital/feed/realtime/vehicle_positions.pb + ``` +- MBTA: + ``` + https://cdn.mbta.com/realtime/VehiclePositions.pb + ``` diff --git a/eta_prediction/bytewax/README.md b/eta_prediction/bytewax/README.md new file mode 100644 index 0000000..8cd9e7e --- /dev/null +++ b/eta_prediction/bytewax/README.md @@ -0,0 +1,60 @@ +# Bytewax ETA Prediction Flow + +Low-latency ETA predictions are generated by streaming vehicle telemetry from MQTT into Redis, enriching the data with cached stops/shapes, and running the Bytewax flow that calls `eta_service`. This README explains how to bring all moving pieces online locally. + +## Prerequisites +- This repository cloned locally (paths below assume the root is `eta_prediction/`). +- [`uv`](https://github.com/astral-sh/uv) or another tool capable of running the provided `pyproject.toml` environments. +- Docker (or Podman) for running the MQTT + Redis stack provided by [`simovilab/databus-mqtt`](https://github.com/simovilab/databus-mqtt). + +## End-to-End Workflow + +### 1. Start the MQTT/Redis stack +```bash +git clone https://github.com/simovilab/databus-mqtt.git +cd databus-mqtt +docker compose up +``` +> Leave this terminal running; it launches the Mosquitto broker, Redis, and any helper services defined in that repo. + +### 2. Publish sample vehicle data +In another terminal inside the `databus-mqtt` repository: +```bash +uv run python publisher/publisher_example +``` +Wait for the publisher to log that it is connected—this continuously streams mock vehicle events into the MQTT broker. + +### 3. Bridge MQTT → Redis +Back in this repository: +```bash +cd bytewax/subscriber +uv run python mqtt2redis.py +``` +This subscriber listens to `transit/vehicles/bus/#`, validates each payload, and caches the latest vehicle state under `vehicle:*` keys in Redis. Keep it running. + +### 4. Seed mock stops and shapes +From `eta_prediction/bytewax/`: +```bash +uv run python mock_stops_and_shapes.py +``` +This loads placeholder stop sequences (`route_stops:*`) and encoded GTFS shapes (`route_shape:*`) into Redis. The Bytewax flow expects this cache to exist; in production it will be populated by another service. + +### 5. Run the Bytewax prediction flow +Still inside `bytewax/`: +```bash +uv run python -m bytewax.run pred2redis +``` +The flow polls Redis for new vehicle entries, enriches them with the cached stops/shapes, calls `estimate_stop_times`, and stores the resulting ETAs under `predictions:`. + +### 6. Monitor predictions +Optionally observe the cached ETAs from another terminal: +```bash +uv run python test_redis_predictions.py --continuous +# or monitor a single vehicle: +uv run python test_redis_predictions.py --vehicle BUS-004 --continuous +``` +The monitor subscribes to Redis and pretty-prints each `predictions:*` payload so you can confirm the pipeline is healthy. + +## Notes +- All scripts default to `localhost` for MQTT and Redis; adjust the constants in `mqtt2redis.py` or provide environment overrides if your services run elsewhere. +- If you restart Redis you will need to re-run `mock_stops_and_shapes.py` to repopulate the cache before restarting the Bytewax flow. diff --git a/eta_prediction/bytewax/mock_stops_and_shapes.py b/eta_prediction/bytewax/mock_stops_and_shapes.py new file mode 100644 index 0000000..c17e1e8 --- /dev/null +++ b/eta_prediction/bytewax/mock_stops_and_shapes.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +""" +Mock route stops and GTFS shapes data for testing ETA prediction flow. +This provides hardcoded stop sequences and shape polylines for routes when Redis/PostgreSQL cache is not available. +Replace with actual GTFS data in production. + +Stops are positioned around NYC area (40.7128, -74.0060) with ~150-250m spacing. +Shapes follow realistic paths with intermediate points between stops. +""" + +# Mock stop data for testing +# Format: route_id -> list of stops with stop_id, stop_sequence, lat, lon +# Stops are clustered around NYC coordinates with realistic 150-250m spacing +MOCK_ROUTE_STOPS = { + "Route-1": [ + {"stop_id": "stop_1_001", "stop_sequence": 1, "lat": 40.7128, "lon": -74.0060}, + {"stop_id": "stop_1_002", "stop_sequence": 2, "lat": 40.7143, "lon": -74.0058}, + {"stop_id": "stop_1_003", "stop_sequence": 3, "lat": 40.7158, "lon": -74.0056}, + {"stop_id": "stop_1_004", "stop_sequence": 4, "lat": 40.7173, "lon": -74.0054}, + {"stop_id": "stop_1_005", "stop_sequence": 5, "lat": 40.7188, "lon": -74.0052}, + {"stop_id": "stop_1_006", "stop_sequence": 6, "lat": 40.7203, "lon": -74.0050}, + {"stop_id": "stop_1_007", "stop_sequence": 7, "lat": 40.7218, "lon": -74.0048}, + {"stop_id": "stop_1_008", "stop_sequence": 8, "lat": 40.7233, "lon": -74.0046}, + ], + "Route-2": [ + {"stop_id": "stop_2_001", "stop_sequence": 1, "lat": 40.7100, "lon": -74.0065}, + {"stop_id": "stop_2_002", "stop_sequence": 2, "lat": 40.7115, "lon": -74.0063}, + {"stop_id": "stop_2_003", "stop_sequence": 3, "lat": 40.7130, "lon": -74.0061}, + {"stop_id": "stop_2_004", "stop_sequence": 4, "lat": 40.7145, "lon": -74.0059}, + {"stop_id": "stop_2_005", "stop_sequence": 5, "lat": 40.7160, "lon": -74.0057}, + {"stop_id": "stop_2_006", "stop_sequence": 6, "lat": 40.7175, "lon": -74.0055}, + {"stop_id": "stop_2_007", "stop_sequence": 7, "lat": 40.7190, "lon": -74.0053}, + ], + "Route-3": [ + {"stop_id": "stop_3_001", "stop_sequence": 1, "lat": 40.7080, "lon": -74.0075}, + {"stop_id": "stop_3_002", "stop_sequence": 2, "lat": 40.7098, "lon": -74.0071}, + {"stop_id": "stop_3_003", "stop_sequence": 3, "lat": 40.7116, "lon": -74.0067}, + {"stop_id": "stop_3_004", "stop_sequence": 4, "lat": 40.7134, "lon": -74.0063}, + {"stop_id": "stop_3_005", "stop_sequence": 5, "lat": 40.7152, "lon": -74.0059}, + ], + "Route-4": [ + {"stop_id": "stop_4_001", "stop_sequence": 1, "lat": 40.7150, "lon": -74.0070}, + {"stop_id": "stop_4_002", "stop_sequence": 2, "lat": 40.7165, "lon": -74.0067}, + {"stop_id": "stop_4_003", "stop_sequence": 3, "lat": 40.7180, "lon": -74.0064}, + {"stop_id": "stop_4_004", "stop_sequence": 4, "lat": 40.7195, "lon": -74.0061}, + {"stop_id": "stop_4_005", "stop_sequence": 5, "lat": 40.7210, "lon": -74.0058}, + {"stop_id": "stop_4_006", "stop_sequence": 6, "lat": 40.7225, "lon": -74.0055}, + ], + "Route-5": [ + {"stop_id": "stop_5_001", "stop_sequence": 1, "lat": 40.7050, "lon": -74.0040}, + {"stop_id": "stop_5_002", "stop_sequence": 2, "lat": 40.7067, "lon": -74.0044}, + {"stop_id": "stop_5_003", "stop_sequence": 3, "lat": 40.7084, "lon": -74.0048}, + {"stop_id": "stop_5_004", "stop_sequence": 4, "lat": 40.7101, "lon": -74.0052}, + {"stop_id": "stop_5_005", "stop_sequence": 5, "lat": 40.7118, "lon": -74.0056}, + {"stop_id": "stop_5_006", "stop_sequence": 6, "lat": 40.7135, "lon": -74.0060}, + {"stop_id": "stop_5_007", "stop_sequence": 7, "lat": 40.7152, "lon": -74.0064}, + {"stop_id": "stop_5_008", "stop_sequence": 8, "lat": 40.7169, "lon": -74.0068}, + {"stop_id": "stop_5_009", "stop_sequence": 9, "lat": 40.7186, "lon": -74.0072}, + ], + "Route-6": [ + {"stop_id": "stop_6_001", "stop_sequence": 1, "lat": 40.7128, "lon": -73.9980}, + {"stop_id": "stop_6_002", "stop_sequence": 2, "lat": 40.7143, "lon": -73.9985}, + {"stop_id": "stop_6_003", "stop_sequence": 3, "lat": 40.7158, "lon": -73.9990}, + {"stop_id": "stop_6_004", "stop_sequence": 4, "lat": 40.7173, "lon": -73.9995}, + {"stop_id": "stop_6_005", "stop_sequence": 5, "lat": 40.7188, "lon": -74.0000}, + {"stop_id": "stop_6_006", "stop_sequence": 6, "lat": 40.7203, "lon": -74.0005}, + ], + "Route-7": [ + {"stop_id": "stop_7_001", "stop_sequence": 1, "lat": 40.7070, "lon": -74.0095}, + {"stop_id": "stop_7_002", "stop_sequence": 2, "lat": 40.7087, "lon": -74.0090}, + {"stop_id": "stop_7_003", "stop_sequence": 3, "lat": 40.7104, "lon": -74.0085}, + {"stop_id": "stop_7_004", "stop_sequence": 4, "lat": 40.7121, "lon": -74.0080}, + {"stop_id": "stop_7_005", "stop_sequence": 5, "lat": 40.7138, "lon": -74.0075}, + {"stop_id": "stop_7_006", "stop_sequence": 6, "lat": 40.7155, "lon": -74.0070}, + {"stop_id": "stop_7_007", "stop_sequence": 7, "lat": 40.7172, "lon": -74.0065}, + ], + "Route-8": [ + {"stop_id": "stop_8_001", "stop_sequence": 1, "lat": 40.7180, "lon": -74.0100}, + {"stop_id": "stop_8_002", "stop_sequence": 2, "lat": 40.7195, "lon": -74.0095}, + {"stop_id": "stop_8_003", "stop_sequence": 3, "lat": 40.7210, "lon": -74.0090}, + {"stop_id": "stop_8_004", "stop_sequence": 4, "lat": 40.7225, "lon": -74.0085}, + {"stop_id": "stop_8_005", "stop_sequence": 5, "lat": 40.7240, "lon": -74.0080}, + ], + "Route-9": [ + {"stop_id": "stop_9_001", "stop_sequence": 1, "lat": 40.7040, "lon": -74.0020}, + {"stop_id": "stop_9_002", "stop_sequence": 2, "lat": 40.7057, "lon": -74.0026}, + {"stop_id": "stop_9_003", "stop_sequence": 3, "lat": 40.7074, "lon": -74.0032}, + {"stop_id": "stop_9_004", "stop_sequence": 4, "lat": 40.7091, "lon": -74.0038}, + {"stop_id": "stop_9_005", "stop_sequence": 5, "lat": 40.7108, "lon": -74.0044}, + {"stop_id": "stop_9_006", "stop_sequence": 6, "lat": 40.7125, "lon": -74.0050}, + {"stop_id": "stop_9_007", "stop_sequence": 7, "lat": 40.7142, "lon": -74.0056}, + {"stop_id": "stop_9_008", "stop_sequence": 8, "lat": 40.7159, "lon": -74.0062}, + ], + "Route-10": [ + {"stop_id": "stop_10_001", "stop_sequence": 1, "lat": 40.7160, "lon": -73.9960}, + {"stop_id": "stop_10_002", "stop_sequence": 2, "lat": 40.7175, "lon": -73.9966}, + {"stop_id": "stop_10_003", "stop_sequence": 3, "lat": 40.7190, "lon": -73.9972}, + {"stop_id": "stop_10_004", "stop_sequence": 4, "lat": 40.7205, "lon": -73.9978}, + {"stop_id": "stop_10_005", "stop_sequence": 5, "lat": 40.7220, "lon": -73.9984}, + {"stop_id": "stop_10_006", "stop_sequence": 6, "lat": 40.7235, "lon": -73.9990}, + ], +} + + +def _generate_shape_points(stops: list, points_per_segment: int = 3) -> list: + """ + Generate realistic shape points between stops with slight curves. + + Args: + stops: List of stop dictionaries with lat/lon + points_per_segment: Number of intermediate points between each stop pair + + Returns: + List of shape point dicts with shape_pt_lat, shape_pt_lon, shape_pt_sequence + """ + shape_points = [] + sequence = 1 + + for i in range(len(stops)): + # Add the stop location as a shape point + shape_points.append({ + "shape_pt_lat": stops[i]["lat"], + "shape_pt_lon": stops[i]["lon"], + "shape_pt_sequence": sequence, + "shape_dist_traveled": None # Will be calculated by ShapePolyline + }) + sequence += 1 + + # Add intermediate points between this stop and the next + if i < len(stops) - 1: + lat1, lon1 = stops[i]["lat"], stops[i]["lon"] + lat2, lon2 = stops[i + 1]["lat"], stops[i + 1]["lon"] + + for j in range(1, points_per_segment + 1): + # Linear interpolation with slight curve + t = j / (points_per_segment + 1) + + # Add small perpendicular offset for realistic road curves + # Alternating left/right curve + curve_factor = 0.00002 * (1 if i % 2 == 0 else -1) + perp_offset = curve_factor * (1 - 2 * abs(t - 0.5)) + + lat = lat1 + (lat2 - lat1) * t + lon = lon1 + (lon2 - lon1) * t + perp_offset + + shape_points.append({ + "shape_pt_lat": lat, + "shape_pt_lon": lon, + "shape_pt_sequence": sequence, + "shape_dist_traveled": None + }) + sequence += 1 + + return shape_points + + +# Generate mock shapes for each route +MOCK_ROUTE_SHAPES = {} +for route_id, stops in MOCK_ROUTE_STOPS.items(): + shape_id = f"shape_{route_id.lower().replace('-', '_')}" + MOCK_ROUTE_SHAPES[route_id] = { + "shape_id": shape_id, + "points": _generate_shape_points(stops, points_per_segment=3) + } + + +def get_route_stops(route_id: str) -> list: + """ + Get stops for a given route. + + Args: + route_id: Route identifier + + Returns: + List of stop dictionaries, or empty list if route not found + """ + return MOCK_ROUTE_STOPS.get(route_id, []) + + +def get_route_shape(route_id: str) -> dict: + """ + Get shape data for a given route. + + Args: + route_id: Route identifier + + Returns: + Dict with shape_id and points list, or None if route not found + """ + return MOCK_ROUTE_SHAPES.get(route_id) + + +def load_all_stops_to_redis(redis_client, key_prefix: str = "route_stops:"): + """ + Load all mock route stops into Redis for testing. + + Args: + redis_client: Redis client instance + key_prefix: Prefix for Redis keys (default: "route_stops:") + + Returns: + Number of routes loaded + """ + import json + + count = 0 + for route_id, stops in MOCK_ROUTE_STOPS.items(): + key = f"{key_prefix}{route_id}" + value = json.dumps(stops) + redis_client.set(key, value) + count += 1 + print(f"✓ Loaded {len(stops)} stops for {route_id} -> {key}") + + print(f"\n✓ Total: Loaded {count} routes to Redis") + return count + + +def load_all_shapes_to_redis(redis_client, key_prefix: str = "route_shape:"): + """ + Load all mock route shapes into Redis for testing. + + Args: + redis_client: Redis client instance + key_prefix: Prefix for Redis keys (default: "route_shape:") + + Returns: + Number of shapes loaded + """ + import json + + count = 0 + for route_id, shape_data in MOCK_ROUTE_SHAPES.items(): + key = f"{key_prefix}{route_id}" + value = json.dumps(shape_data) + redis_client.set(key, value) + count += 1 + print(f"✓ Loaded {len(shape_data['points'])} shape points for {route_id} -> {key}") + + print(f"\n✓ Total: Loaded {count} shapes to Redis") + return count + + +def load_all_to_redis(redis_client, + stops_key_prefix: str = "route_stops:", + shapes_key_prefix: str = "route_shape:"): + """ + Load both stops and shapes to Redis. + + Args: + redis_client: Redis client instance + stops_key_prefix: Prefix for stop keys + shapes_key_prefix: Prefix for shape keys + + Returns: + Tuple of (stops_count, shapes_count) + """ + print("Loading stops...") + stops_count = load_all_stops_to_redis(redis_client, stops_key_prefix) + + print("\nLoading shapes...") + shapes_count = load_all_shapes_to_redis(redis_client, shapes_key_prefix) + + return stops_count, shapes_count + + +def create_mock_shape_polyline(route_id: str): + """ + Create a ShapePolyline object from mock data for testing. + + Args: + route_id: Route identifier + + Returns: + ShapePolyline object or None if route not found or spatial module unavailable + """ + try: + from feature_engineering.spatial import ShapePolyline + except ImportError: + print("⚠️ ShapePolyline not available, install feature_engineering module") + return None + + shape_data = get_route_shape(route_id) + if not shape_data: + return None + + # Convert to format expected by ShapePolyline + points = [ + (pt["shape_pt_lat"], pt["shape_pt_lon"]) + for pt in shape_data["points"] + ] + + return ShapePolyline(points) + + +if __name__ == "__main__": + """ + Standalone script to populate Redis with mock route stops and shapes. + Usage: python mock_route_stops.py + """ + import redis + + print("="*70) + print("LOADING MOCK ROUTE DATA TO REDIS") + print("NYC AREA - 150-250m stop spacing with realistic shape polylines") + print("="*70 + "\n") + + try: + client = redis.Redis( + host="localhost", + port=6379, + db=0, + decode_responses=True + ) + client.ping() + print("✓ Connected to Redis at localhost:6379\n") + + stops_count, shapes_count = load_all_to_redis(client) + + print("\n" + "="*70) + print("SUMMARY") + print("="*70) + print(f"✓ Loaded {stops_count} route stop sequences") + print(f"✓ Loaded {shapes_count} route shape polylines") + print(f"✓ Total shape points: {sum(len(s['points']) for s in MOCK_ROUTE_SHAPES.values())}") + print("\nYour Bytewax flow can now fetch:") + print(" • Stops from Redis: route_stops:{route_id}") + print(" • Shapes from Redis: route_shape:{route_id}") + print("\nShape data enables:") + print(" • Accurate distance along route") + print(" • Cross-track error detection") + print(" • Progress ratio calculation") + print("="*70) + + except redis.ConnectionError as e: + print(f"✗ Failed to connect to Redis: {e}") + print("Make sure Redis is running: redis-server") + except Exception as e: + print(f"✗ Error: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/eta_prediction/bytewax/mqtt2redis.py b/eta_prediction/bytewax/mqtt2redis.py new file mode 100644 index 0000000..388cc4f --- /dev/null +++ b/eta_prediction/bytewax/mqtt2redis.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +""" +MQTT to Redis Subscriber Bridge for ETA Prediction Pipeline +Subscribes to MQTT vehicle topics and stores enriched data in Redis. + +Usage: + pip install paho-mqtt redis + python mqtt_to_redis_subscriber.py +""" + +import json +import time +import sys +from datetime import datetime, timezone +import paho.mqtt.client as mqtt +import redis + +# ============================================================================ +# Configuration +# ============================================================================ + +# MQTT Configuration +MQTT_HOST = "localhost" +MQTT_PORT = 1883 +MQTT_USER = "admin" +MQTT_PASS = "admin" +MQTT_TOPIC = "transit/vehicles/bus/#" # Subscribe to all bus topics + +# Redis Configuration +REDIS_HOST = "localhost" +REDIS_PORT = 6379 +REDIS_DB = 0 +REDIS_PASSWORD = None +REDIS_KEY_PREFIX = "vehicle:" # Keys will be like "vehicle:BUS-001" +REDIS_TTL = 300 # Time to live in seconds (5 minutes) + +# Optional: Publish to Redis channel for real-time subscribers +REDIS_PUBSUB_ENABLED = False +REDIS_PUBSUB_CHANNEL = "vehicle_updates" + +# Data validation - ensure all required fields are present +REQUIRED_FIELDS = [ + 'vehicle_id', + 'lat', + 'lon', + 'speed', + 'timestamp' +] + +# Optional fields that enhance predictions (but won't reject messages if missing) +RECOMMENDED_FIELDS = [ + 'route_id', + 'trip_id', + 'stop_id', + 'stop_lat', + 'stop_lon', + 'stop_sequence', + 'heading', + 'bearing' +] + +# ============================================================================ +# Redis Client +# ============================================================================ + +redis_client = None + +def connect_redis(): + """Connect to Redis""" + global redis_client + + try: + redis_client = redis.Redis( + host=REDIS_HOST, + port=REDIS_PORT, + db=REDIS_DB, + password=REDIS_PASSWORD, + decode_responses=True + ) + + # Test connection + redis_client.ping() + print(f"✓ Connected to Redis at {REDIS_HOST}:{REDIS_PORT}") + return True + + except redis.ConnectionError as e: + print(f"✗ Failed to connect to Redis: {e}") + print(f" Make sure Redis is running: docker-compose up -d redis") + return False + except Exception as e: + print(f"✗ Redis connection error: {e}") + return False + +def validate_vehicle_data(data): + """ + Validate that vehicle data contains required fields for ETA prediction. + Returns (is_valid, missing_fields, missing_recommended) + """ + missing_required = [field for field in REQUIRED_FIELDS if field not in data] + missing_recommended = [field for field in RECOMMENDED_FIELDS if field not in data] + + is_valid = len(missing_required) == 0 + + return is_valid, missing_required, missing_recommended + +def enrich_vehicle_data(data): + """ + Enrich vehicle data with additional metadata and transformations. + """ + # Ensure consistent field naming + if 'route' in data and 'route_id' not in data: + data['route_id'] = data['route'] + + # Add UTC timestamp if not present or normalize it + if 'timestamp' in data: + try: + # Parse and normalize to ISO format with timezone + ts = datetime.fromisoformat(data['timestamp'].replace('Z', '+00:00')) + data['timestamp'] = ts.isoformat() + except: + # If parsing fails, use current time + data['timestamp'] = datetime.now(timezone.utc).isoformat() + else: + data['timestamp'] = datetime.now(timezone.utc).isoformat() + + # Ensure heading and bearing are both present (some systems use one or the other) + if 'heading' in data and 'bearing' not in data: + data['bearing'] = data['heading'] + elif 'bearing' in data and 'heading' not in data: + data['heading'] = data['bearing'] + + # Add processing metadata + data['_mqtt_received_at'] = datetime.now(timezone.utc).isoformat() + data['_data_quality'] = 'complete' if all(f in data for f in RECOMMENDED_FIELDS) else 'partial' + + return data + +def store_in_redis(vehicle_id, data): + """Store vehicle data in Redis with enrichment""" + try: + # Validate data + is_valid, missing_required, missing_recommended = validate_vehicle_data(data) + + if not is_valid: + print(f"⚠️ Invalid data for {vehicle_id}: missing required fields {missing_required}") + return False + + # Warn about missing recommended fields + if missing_recommended: + print(f"ℹ️ {vehicle_id} missing recommended fields: {missing_recommended}") + + # Enrich data + enriched_data = enrich_vehicle_data(data) + + # Create Redis key + key = f"{REDIS_KEY_PREFIX}{vehicle_id}" + + # Convert data to JSON string + json_data = json.dumps(enriched_data) + + # Store with TTL (expires after REDIS_TTL seconds) + redis_client.setex(key, REDIS_TTL, json_data) + + # Optional: Publish to Redis Pub/Sub channel for real-time subscribers + if REDIS_PUBSUB_ENABLED: + redis_client.publish(REDIS_PUBSUB_CHANNEL, json_data) + + return True + + except Exception as e: + print(f"✗ Error storing data in Redis: {e}") + import traceback + traceback.print_exc() + return False + +# ============================================================================ +# MQTT Callbacks +# ============================================================================ + +message_count = 0 +error_count = 0 +warning_count = 0 +start_time = None + +def on_connect(client, userdata, flags, rc): + """Callback when connected to MQTT broker""" + global start_time + + if rc == 0: + print(f"✓ Connected to MQTT broker at {MQTT_HOST}:{MQTT_PORT}") + + # Subscribe to topic + client.subscribe(MQTT_TOPIC, qos=1) + print(f"✓ Subscribed to topic: {MQTT_TOPIC}") + print("\n" + "="*70) + print("MQTT → Redis Bridge Active (ETA Prediction Pipeline)") + print("="*70) + print("Features:") + print(" • Data validation (required & recommended fields)") + print(" • Timestamp normalization") + print(" • Field enrichment (heading/bearing consistency)") + print(" • Quality scoring") + print("="*70) + print(f"Listening for messages... (press Ctrl+C to stop)\n") + + start_time = time.time() + + else: + print(f"✗ Failed to connect to MQTT broker, return code: {rc}") + print(" Return codes: 0=Success, 1=Protocol version, 2=Invalid client ID") + print(" 3=Server unavailable, 4=Bad credentials, 5=Not authorized") + sys.exit(1) + +def on_message(client, userdata, msg): + """Callback when a message is received""" + global message_count, error_count, warning_count + + try: + # Parse the message + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + + # Get vehicle ID + vehicle_id = data.get('vehicle_id') + + if not vehicle_id: + print(f"⚠️ Message missing vehicle_id: {msg.topic}") + error_count += 1 + return + + # Add MQTT metadata + data['_mqtt_topic'] = msg.topic + + # Store in Redis (with validation and enrichment) + if store_in_redis(vehicle_id, data): + message_count += 1 + + # Detailed output for first few messages, then summary + if message_count <= 5: + print(f"✓ [{message_count}] Stored {vehicle_id}") + print(f" Route: {data.get('route_id', 'N/A')}") + print(f" Position: ({data.get('lat'):.4f}, {data.get('lon'):.4f})") + print(f" Speed: {data.get('speed')} km/h") + print(f" Next Stop: {data.get('stop_id', 'N/A')}") + print(f" Quality: {data.get('_data_quality', 'unknown')}") + elif message_count % 10 == 0: + elapsed = time.time() - start_time if start_time else 0 + rate = message_count / elapsed if elapsed > 0 else 0 + print(f"📊 [{message_count}] messages | " + f"{rate:.1f} msg/sec | " + f"{error_count} errors | " + f"{warning_count} warnings") + else: + # Compact output for routine messages + quality_icon = "✓" if data.get('_data_quality') == 'complete' else "⚠" + print(f"{quality_icon} [{message_count}] {vehicle_id} → " + f"{data.get('route_id', 'N/A'):10s} | " + f"Speed: {data.get('speed', 0):5.1f} km/h | " + f"Stop: {data.get('stop_id', 'N/A')}") + else: + error_count += 1 + + except json.JSONDecodeError as e: + print(f"✗ Failed to decode JSON from topic {msg.topic}: {e}") + error_count += 1 + except Exception as e: + print(f"✗ Error processing message: {e}") + import traceback + traceback.print_exc() + error_count += 1 + +def on_disconnect(client, userdata, rc): + """Callback when disconnected from MQTT broker""" + if rc != 0: + print(f"\n✗ Unexpected disconnection from MQTT broker (code: {rc})") + print(" Attempting to reconnect...") + +# ============================================================================ +# Main Function +# ============================================================================ + +def print_config(): + """Print current configuration""" + print("="*70) + print("MQTT to Redis Subscriber - ETA Prediction Pipeline") + print("="*70) + print(f"MQTT Broker: {MQTT_HOST}:{MQTT_PORT}") + print(f"MQTT Topic: {MQTT_TOPIC}") + print(f"Redis Server: {REDIS_HOST}:{REDIS_PORT}") + print(f"Redis Key: {REDIS_KEY_PREFIX}") + print(f"Redis TTL: {REDIS_TTL} seconds") + print(f"Pub/Sub: {'Enabled' if REDIS_PUBSUB_ENABLED else 'Disabled'}") + if REDIS_PUBSUB_ENABLED: + print(f"Pub/Sub Channel: {REDIS_PUBSUB_CHANNEL}") + print() + print("Required Fields: " + ", ".join(REQUIRED_FIELDS)) + print("Recommended: " + ", ".join(RECOMMENDED_FIELDS)) + print("="*70) + print() + +def print_stats(): + """Print final statistics""" + print("\n" + "="*70) + print("FINAL STATISTICS") + print("="*70) + print(f"Total messages processed: {message_count}") + print(f"Total errors: {error_count}") + print(f"Total warnings: {warning_count}") + + if message_count > 0: + success_rate = ((message_count / (message_count + error_count)) * 100) if (message_count + error_count) > 0 else 0 + print(f"Success rate: {success_rate:.1f}%") + + if start_time: + elapsed = time.time() - start_time + rate = message_count / elapsed if elapsed > 0 else 0 + print(f"Average rate: {rate:.2f} messages/second") + print(f"Running time: {elapsed:.1f} seconds") + print("="*70) + +def main(): + """Main function""" + print_config() + + # Connect to Redis first + print("Connecting to Redis...") + if not connect_redis(): + print("\n✗ Cannot start without Redis connection") + sys.exit(1) + + print() + + # Create MQTT client + print("Setting up MQTT client...") + mqtt_client = mqtt.Client(client_id="mqtt-redis-bridge-eta") + mqtt_client.username_pw_set(MQTT_USER, MQTT_PASS) + + # Set up callbacks + mqtt_client.on_connect = on_connect + mqtt_client.on_message = on_message + mqtt_client.on_disconnect = on_disconnect + + try: + # Connect to MQTT broker + print(f"Connecting to MQTT broker at {MQTT_HOST}:{MQTT_PORT}...") + mqtt_client.connect(MQTT_HOST, MQTT_PORT, 60) + + # Start the loop (blocking) + mqtt_client.loop_forever() + + except KeyboardInterrupt: + print_stats() + + except ConnectionRefusedError: + print(f"\n✗ Connection refused to MQTT broker at {MQTT_HOST}:{MQTT_PORT}") + print(" Make sure RabbitMQ is running: docker-compose up -d rabbitmq") + sys.exit(1) + + except Exception as e: + print(f"\n✗ Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + finally: + # Cleanup + mqtt_client.disconnect() + if redis_client: + redis_client.close() + print("\n✓ Subscriber stopped cleanly\n") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/eta_prediction/bytewax/pred2redis.py b/eta_prediction/bytewax/pred2redis.py new file mode 100644 index 0000000..e045e1a --- /dev/null +++ b/eta_prediction/bytewax/pred2redis.py @@ -0,0 +1,657 @@ +#!/usr/bin/env python3 +""" +Bytewax dataflow for low-latency ETA prediction processing. + +LOW-LATENCY DESIGN: +- All data loaded from Redis cache (no database calls) +- In-memory shape caching per worker +- Shape loading happens in enrichment step before inference +- Estimator receives pre-loaded shapes for zero I/O during prediction + +This flow: +1. Polls vehicle position data from Redis (where MQTT subscriber stores it) +2. Enriches with upcoming stops from Redis cache +3. Loads GTFS shapes from Redis cache with in-memory caching +4. Processes through estimate_stop_times() with pre-loaded shapes +5. Stores predictions back to Redis under "predictions:*" keys + +Usage: + pip install bytewax redis + python -m bytewax.run bytewax_eta_flow +""" + +import json +import time +from datetime import datetime, timedelta, timezone +from typing import Optional, Dict, Any, List, Set +from dataclasses import dataclass +from pathlib import Path + +import redis +import bytewax.operators as op +from bytewax.dataflow import Dataflow +from bytewax.inputs import FixedPartitionedSource, StatefulSourcePartition +from bytewax.outputs import DynamicSink, StatelessSinkPartition + +# Import the ETA estimator with proper path resolution +import sys +import os + +# Resolve paths relative to this file +current_dir = Path(__file__).resolve().parent +project_root = current_dir.parent + +# Add project directories to Python path +sys.path.insert(0, str(project_root)) +sys.path.insert(0, str(project_root / "eta_service")) +sys.path.insert(0, str(project_root / "feature_engineering")) +sys.path.insert(0, str(project_root / "models")) + +from eta_service.estimator import estimate_stop_times + +# Import ShapePolyline for shape processing +try: + from feature_engineering.spatial import ShapePolyline + SHAPE_SUPPORT = True +except ImportError: + SHAPE_SUPPORT = False + print("⚠️ ShapePolyline not available, shape-aware features disabled") + + +# ============================================================================ +# Configuration +# ============================================================================ + +@dataclass +class RedisConfig: + """Redis connection configuration""" + host: str = "localhost" + port: int = 6379 + db: int = 0 + password: Optional[str] = None + vehicle_key_pattern: str = "vehicle:*" # Pattern for vehicle position data + route_stops_key_prefix: str = "route_stops:" # Cache key for route stop sequences + route_shape_key_prefix: str = "route_shape:" # Cache key for route shapes + predictions_key_prefix: str = "predictions:" # Output key prefix for predictions + poll_interval_ms: int = 1000 # How often to poll Redis (milliseconds) + predictions_ttl: int = 300 # TTL for prediction cache (5 minutes) + + +# ============================================================================ +# Shape Loading & Caching (Zero Database Calls) +# ============================================================================ + +class ShapeCache: + """ + In-memory LRU-style cache for loaded shapes. + Persists across batches within a worker to avoid repeated Redis queries. + """ + + def __init__(self, max_size: int = 100): + self.cache: Dict[str, Any] = {} + self.access_order: List[str] = [] + self.max_size = max_size + self.hits = 0 + self.misses = 0 + + def get(self, route_id: str) -> Optional[Any]: + """Get cached shape for route""" + if route_id in self.cache: + # Move to end (most recently used) + self.access_order.remove(route_id) + self.access_order.append(route_id) + self.hits += 1 + return self.cache[route_id] + + self.misses += 1 + return None + + def set(self, route_id: str, shape: Any): + """Cache shape for route with LRU eviction""" + if route_id in self.cache: + # Update existing + self.access_order.remove(route_id) + self.access_order.append(route_id) + self.cache[route_id] = shape + else: + # Add new + if len(self.cache) >= self.max_size: + # Evict least recently used + oldest = self.access_order.pop(0) + del self.cache[oldest] + + self.cache[route_id] = shape + self.access_order.append(route_id) + + def stats(self) -> dict: + """Get cache statistics""" + total = self.hits + self.misses + hit_rate = (self.hits / total * 100) if total > 0 else 0 + return { + 'size': len(self.cache), + 'hits': self.hits, + 'misses': self.misses, + 'hit_rate_pct': round(hit_rate, 1) + } + + +def load_shape_from_redis(redis_client, route_id: str, shape_cache: ShapeCache, key_prefix: str = "route_shape:") -> Optional[Any]: + """ + Load shape data from Redis and convert to ShapePolyline. + Uses in-memory cache to avoid repeated Redis queries. + + Args: + redis_client: Redis client instance + route_id: Route identifier + shape_cache: ShapeCache instance for this worker + key_prefix: Redis key prefix + + Returns: + ShapePolyline object or None if not available + """ + if not SHAPE_SUPPORT: + return None + + # Check in-memory cache first (fastest path) + cached = shape_cache.get(route_id) + if cached is not None: + return cached + + # Load from Redis + try: + shape_key = f"{key_prefix}{route_id}" + shape_json = redis_client.get(shape_key) + + if shape_json: + shape_data = json.loads(shape_json) + points = [ + (pt["shape_pt_lat"], pt["shape_pt_lon"]) + for pt in shape_data["points"] + ] + + shape = ShapePolyline(points) + shape_cache.set(route_id, shape) + + return shape + except Exception as e: + print(f"⚠️ Error loading shape from Redis for route {route_id}: {e}") + + return None + + +# ============================================================================ +# Redis Input Source +# ============================================================================ + +class RedisVehiclePartition(StatefulSourcePartition): + """Partition that polls vehicle position data from Redis""" + + def __init__(self, redis_config: RedisConfig): + self.redis_config = redis_config + self.redis_client = None + self.shape_cache = ShapeCache(max_size=100) # One cache per worker + self._setup_client() + + def _setup_client(self): + """Initialize Redis client""" + try: + self.redis_client = redis.Redis( + host=self.redis_config.host, + port=self.redis_config.port, + db=self.redis_config.db, + password=self.redis_config.password, + decode_responses=True + ) + self.redis_client.ping() + print(f"✓ Connected to Redis at {self.redis_config.host}:{self.redis_config.port}") + print(f"✓ Polling pattern: {self.redis_config.vehicle_key_pattern}") + print(f"✓ Shape cache enabled with max_size=100\n") + except redis.ConnectionError as e: + print(f"✗ Failed to connect to Redis: {e}") + raise + + def next_batch(self): + """Get next batch of vehicle updates from Redis""" + batch = [] + + try: + keys = self.redis_client.keys(self.redis_config.vehicle_key_pattern) + + for key in keys: + try: + value = self.redis_client.get(key) + + if value: + data = json.loads(value) + data['_redis_key'] = key + data['_fetched_at'] = datetime.now(timezone.utc).isoformat() + data['_redis_client'] = self.redis_client + data['_redis_config'] = self.redis_config + data['_shape_cache'] = self.shape_cache # Pass cache to downstream + + ttl = self.redis_client.ttl(key) + if ttl > 0: + data['_ttl_seconds'] = ttl + + batch.append(data) + + except json.JSONDecodeError as e: + print(f"⚠️ Failed to decode JSON from key {key}: {e}") + except Exception as e: + print(f"⚠️ Error processing key {key}: {e}") + + if batch: + cache_stats = self.shape_cache.stats() + print(f"📦 Fetched {len(batch)} vehicle records | " + f"Shape cache: {cache_stats['hit_rate_pct']}% hit rate " + f"({cache_stats['hits']}/{cache_stats['hits'] + cache_stats['misses']})") + + except Exception as e: + print(f"✗ Error in next_batch: {e}") + + return batch + + def next_awake(self): + """Return when to wake up next (must be timezone-aware)""" + return datetime.now(timezone.utc) + timedelta(milliseconds=self.redis_config.poll_interval_ms) + + def snapshot(self): + """Save state for recovery""" + return None + + def close(self): + """Cleanup when source is closed""" + if self.redis_client: + cache_stats = self.shape_cache.stats() + print(f"\n✓ Worker closing. Final shape cache stats: {cache_stats}") + self.redis_client.close() + print("✓ Disconnected from Redis") + + +class RedisVehicleSource(FixedPartitionedSource): + """Source that creates Redis polling partitions""" + + def __init__(self, redis_config: RedisConfig): + self.redis_config = redis_config + + def list_parts(self): + """List available partitions""" + return ["redis-vehicles-0"] + + def build_part(self, step_id, for_part, resume_state): + """Build partition for reading""" + return RedisVehiclePartition(self.redis_config) + + +# ============================================================================ +# Processing Functions +# ============================================================================ + +def validate_vehicle_data(data: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """ + Validate vehicle data has required fields for ETA estimation. + Returns None if data is invalid (will be filtered out). + """ + required_fields = ['vehicle_id', 'lat', 'lon', 'speed', 'timestamp', 'route'] + + if not all(field in data for field in required_fields): + missing = [f for f in required_fields if f not in data] + print(f"⚠️ Invalid vehicle data: missing fields {missing}") + return None + + # Validate data types and ranges + try: + float(data['lat']) + float(data['lon']) + float(data['speed']) + except (ValueError, TypeError): + print(f"⚠️ Invalid vehicle data: non-numeric lat/lon/speed") + return None + + return data + + +def enrich_with_stops_and_shape(data: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """ + Enrich vehicle data with upcoming stops and pre-loaded shape from Redis cache. + + ZERO DATABASE CALLS - All data from Redis cache or in-memory cache. + + Shape loading priority: + 1. In-memory cache (zero latency) + 2. Redis cache (low latency) + 3. Mock data fallback + + Expected Redis cache structure: + + Stops - Key: "route_stops:{route_id}" + Value: JSON array of stops with fields: stop_id, stop_sequence, lat, lon + + Shapes - Key: "route_shape:{route_id}" + Value: JSON object with shape_id and points array + """ + route_id = data.get('route') + + if not route_id: + print(f"⚠️ Vehicle {data.get('vehicle_id')} missing route_id") + return None + + redis_client = data.get('_redis_client') + redis_config = data.get('_redis_config') + shape_cache = data.get('_shape_cache') + + if not redis_client or not redis_config or not shape_cache: + print(f"⚠️ Missing Redis client/config/cache for vehicle {data.get('vehicle_id')}") + return None + + try: + # Load stops from Redis + stops_key = f"{redis_config.route_stops_key_prefix}{route_id}" + stops_json = redis_client.get(stops_key) + upcoming_stops = None + + if stops_json: + upcoming_stops = json.loads(stops_json) + + if not upcoming_stops or not isinstance(upcoming_stops, list): + print(f"⚠️ No stops available for route {route_id}") + return None + + data['upcoming_stops'] = upcoming_stops + + # Load shape from Redis/cache (best-effort, not required) + shape = load_shape_from_redis( + redis_client, + route_id, + shape_cache, + redis_config.route_shape_key_prefix + ) + + # Store shape for inference + data['_shape'] = shape + data['_shape_available'] = shape is not None + + return data + + except json.JSONDecodeError as e: + print(f"⚠️ Failed to decode JSON for route {route_id}: {e}") + return None + except Exception as e: + print(f"⚠️ Error enriching with stops/shape: {e}") + import traceback + traceback.print_exc() + return None + + +def process_eta(data: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """ + Process vehicle data through the ETA estimator with pre-loaded shape. + + ZERO I/O DURING INFERENCE - Shape already loaded in enrichment step. + + Input: Vehicle position data enriched with upcoming stops and optional shape + Output: ETA predictions for upcoming stops + """ + try: + # Prepare vehicle position dict for estimator + vehicle_position = { + 'vehicle_id': data['vehicle_id'], + 'route': data['route'], + 'lat': float(data['lat']), + 'lon': float(data['lon']), + 'speed': float(data['speed']), # Assumed to be in m/s + 'heading': data.get('heading'), + 'timestamp': data['timestamp'] + } + + upcoming_stops = data.get('upcoming_stops', []) + + if not upcoming_stops: + print(f"⚠️ No upcoming stops for vehicle {data['vehicle_id']}") + return None + + # Get pre-loaded shape (or None) + shape = data.get('_shape') + trip_id = data.get('trip_id') + + # Call estimator with pre-loaded shape (zero I/O!) + result = estimate_stop_times( + vehicle_position=vehicle_position, + upcoming_stops=upcoming_stops, + route_id=data['route'], + trip_id=trip_id, + max_stops=5, + shape=shape # Pre-loaded shape, no database calls + ) + + # Add original vehicle data for context + result['_original_vehicle_data'] = { + 'vehicle_id': data['vehicle_id'], + 'route': data['route'], + 'lat': data['lat'], + 'lon': data['lon'], + 'speed': data['speed'], + 'timestamp': data['timestamp'], + 'trip_id': trip_id + } + + shape_status = "with shape" if result.get('shape_used') else "without shape" + print(f"✓ Processed ETA for vehicle {data['vehicle_id']} on route {data['route']} {shape_status}: " + f"{len(result['predictions'])} stops predicted") + + return result + + except Exception as e: + print(f"✗ Error processing ETA for vehicle {data.get('vehicle_id')}: {e}") + import traceback + traceback.print_exc() + return None + + +def format_for_redis(result: Dict[str, Any]) -> tuple[str, str]: + """ + Format ETA prediction result for Redis storage. + + Returns: (key, value) tuple + Key format: "predictions:{vehicle_id}" + Value: JSON string of prediction result + """ + vehicle_id = result.get('vehicle_id', 'unknown') + key = f"predictions:{vehicle_id}" + + # Add metadata + result['_stored_at'] = datetime.now(timezone.utc).isoformat() + + value = json.dumps(result, indent=2) + + return (key, value) + + +# ============================================================================ +# Redis Output Sink +# ============================================================================ + +class RedisETASinkPartition(StatelessSinkPartition): + """Partition that writes ETA predictions to Redis""" + + def __init__(self, config: RedisConfig, worker_index: int): + self.config = config + self.worker_index = worker_index + self.client = None + self.count = 0 + self.shape_count = 0 + self._setup_client() + + def _setup_client(self): + """Initialize Redis client""" + try: + self.client = redis.Redis( + host=self.config.host, + port=self.config.port, + db=self.config.db, + password=self.config.password, + decode_responses=True + ) + self.client.ping() + print(f"✓ Worker {self.worker_index}: Redis sink connected") + except redis.ConnectionError as e: + print(f"✗ Worker {self.worker_index}: Failed to connect to Redis: {e}") + raise + + def write_batch(self, items: List[tuple[str, str]]) -> None: + """Write batch of predictions to Redis""" + for key, value in items: + try: + # Write to Redis with TTL + self.client.setex( + key, + self.config.predictions_ttl, + value + ) + self.count += 1 + + # Track shape usage + try: + result = json.loads(value) + if result.get('shape_used'): + self.shape_count += 1 + except: + pass + + if self.count % 10 == 0: + shape_pct = (self.shape_count / self.count * 100) if self.count > 0 else 0 + print(f"[Worker {self.worker_index}] Stored {self.count} predictions " + f"({self.shape_count} with shapes, {shape_pct:.1f}%)") + + except Exception as e: + print(f"✗ Worker {self.worker_index}: Error writing to Redis: {e}") + + def close(self) -> None: + """Cleanup when sink is closed""" + if self.client: + self.client.close() + shape_pct = (self.shape_count / self.count * 100) if self.count > 0 else 0 + print(f"✓ Worker {self.worker_index}: Redis sink closed.") + print(f" Total predictions: {self.count}") + print(f" With shapes: {self.shape_count} ({shape_pct:.1f}%)") + + +class RedisETASink(DynamicSink): + """Dynamic sink that writes ETA predictions to Redis""" + + def __init__(self, config: RedisConfig): + self.config = config + + def build(self, step_id: str, worker_index: int, worker_count: int) -> StatelessSinkPartition: + """Build partition for writing""" + return RedisETASinkPartition(self.config, worker_index) + + +# ============================================================================ +# Dataflow Definition +# ============================================================================ + +def build_flow(): + """ + Build and return the Bytewax dataflow for low-latency ETA prediction. + + LOW-LATENCY DESIGN: + - All data from Redis cache (no database calls) + - In-memory shape caching per worker + - Pre-loaded shapes passed to estimator + - Zero I/O during inference + + Pipeline: + 1. Input: Poll vehicle positions from Redis + 2. Validate: Check required fields + 3. Enrich: Load stops and shapes from Redis/cache + 4. Process: Run ETA estimation with pre-loaded shapes + 5. Format: Prepare for Redis storage + 6. Output: Store predictions in Redis with TTL + """ + + # Create configuration + redis_config = RedisConfig( + host="localhost", + port=6379, + db=0, + password=None, + vehicle_key_pattern="vehicle:*", + route_stops_key_prefix="route_stops:", + route_shape_key_prefix="route_shape:", + predictions_key_prefix="predictions:", + poll_interval_ms=1000, + predictions_ttl=300 # 5 minutes + ) + + # Initialize dataflow + flow = Dataflow("eta-prediction-flow") + + # Step 1: Input from Redis (vehicle positions) + redis_source = RedisVehicleSource(redis_config) + stream = op.input("redis-vehicles", flow, redis_source) + + # Step 2: Validate vehicle data + stream = op.filter_map("validate", stream, validate_vehicle_data) + + # Step 3: Enrich with stops and pre-load shapes from Redis/cache + stream = op.filter_map("enrich-stops-shape", stream, enrich_with_stops_and_shape) + + # Step 4: Process through ETA estimator (zero I/O - shapes pre-loaded!) + stream = op.filter_map("estimate-eta", stream, process_eta) + + # Step 5: Format for Redis storage + stream = op.map("format-redis", stream, format_for_redis) + + # Step 6: Output to Redis + redis_sink = RedisETASink(redis_config) + op.output("redis-predictions", stream, redis_sink) + + return flow + + +# ============================================================================ +# Main Entry Point +# ============================================================================ + +# Bytewax looks for a variable called 'flow' at module level +flow = build_flow() + +if __name__ == "__main__": + print("\n" + "="*70) + print("LOW-LATENCY ETA PREDICTION DATAFLOW") + print("="*70) + print("Architecture:") + print(" • Zero database calls during inference") + print(" • All data from Redis cache (stops, shapes)") + print(" • In-memory LRU shape caching per worker") + print(" • Pre-loaded shapes passed to estimator") + print("\nThis flow:") + print(" 1. Reads vehicle positions from Redis (vehicle:*)") + print(" 2. Enriches with stops from Redis (route_stops:{route_id})") + print(" 3. Loads shapes from Redis with caching (route_shape:{route_id})") + print(" 4. Estimates ETAs with pre-loaded shapes (zero I/O)") + print(" 5. Stores predictions in Redis (predictions:*)") + print("\nShape Loading (Low-Latency Priority):") + print(" 1. In-memory worker cache (zero latency)") + print(" 2. Redis cache (low latency)") + print(" 3. Mock data fallback (testing)") + print("\nPerformance Features:") + print(" • LRU cache with 100 shape limit per worker") + print(" • Cache hit rate monitoring") + print(" • Shape-aware spatial features when available") + print(" • Graceful fallback to haversine distance") + print("\nPrerequisites:") + print(" - MQTT subscriber writing to Redis (vehicle:* keys)") + print(" - Route stops cached in Redis (route_stops:* keys)") + print(" - Route shapes cached in Redis (route_shape:* keys)") + print(" - Trained models in models/trained/ directory") + print("\nSetup:") + print(" 1. Load cache: python mock_route_stops.py") + print(" 2. Start flow: python -m bytewax.run bytewax_eta_flow") + print(" 3. Multiple workers: python -m bytewax.run bytewax_eta_flow -w 4") + print("\nMonitoring:") + print(" • Watch cache hit rates in logs") + print(" • Track shape usage percentage") + print(" • Monitor prediction throughput") + print("="*70 + "\n") \ No newline at end of file diff --git a/eta_prediction/bytewax/pyproject.toml b/eta_prediction/bytewax/pyproject.toml new file mode 100644 index 0000000..53e1b52 --- /dev/null +++ b/eta_prediction/bytewax/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "gtfsrt-tools" +version = "0.1.0" +description = "Utilities for exploring GTFS-Realtime feeds (Vehicle Positions, Trip Updates, Alerts)." +authors = [ + { name = "Jæ" } +] +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.12, <3.13" + +dependencies = [ + "bytewax>=0.21.1", + "celery>=5.5.3", + "django>=4.2.25", + "gtfs-realtime-bindings>=1.0.0", + "matplotlib>=3.9.4", + "pandas>=2.3.3", + "psycopg>=3.2.11", + "redis>=7.0.1", + "requests>=2.31", + "scikit-learn>=1.7.2", + "xgboost>=3.1.2", +] + +[tool.uv.workspace] +members = [ + "feature_engineering/proj", + "feature_engineering", +] diff --git a/eta_prediction/bytewax/subscriber/mqtt2redis.py b/eta_prediction/bytewax/subscriber/mqtt2redis.py new file mode 100644 index 0000000..388cc4f --- /dev/null +++ b/eta_prediction/bytewax/subscriber/mqtt2redis.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +""" +MQTT to Redis Subscriber Bridge for ETA Prediction Pipeline +Subscribes to MQTT vehicle topics and stores enriched data in Redis. + +Usage: + pip install paho-mqtt redis + python mqtt_to_redis_subscriber.py +""" + +import json +import time +import sys +from datetime import datetime, timezone +import paho.mqtt.client as mqtt +import redis + +# ============================================================================ +# Configuration +# ============================================================================ + +# MQTT Configuration +MQTT_HOST = "localhost" +MQTT_PORT = 1883 +MQTT_USER = "admin" +MQTT_PASS = "admin" +MQTT_TOPIC = "transit/vehicles/bus/#" # Subscribe to all bus topics + +# Redis Configuration +REDIS_HOST = "localhost" +REDIS_PORT = 6379 +REDIS_DB = 0 +REDIS_PASSWORD = None +REDIS_KEY_PREFIX = "vehicle:" # Keys will be like "vehicle:BUS-001" +REDIS_TTL = 300 # Time to live in seconds (5 minutes) + +# Optional: Publish to Redis channel for real-time subscribers +REDIS_PUBSUB_ENABLED = False +REDIS_PUBSUB_CHANNEL = "vehicle_updates" + +# Data validation - ensure all required fields are present +REQUIRED_FIELDS = [ + 'vehicle_id', + 'lat', + 'lon', + 'speed', + 'timestamp' +] + +# Optional fields that enhance predictions (but won't reject messages if missing) +RECOMMENDED_FIELDS = [ + 'route_id', + 'trip_id', + 'stop_id', + 'stop_lat', + 'stop_lon', + 'stop_sequence', + 'heading', + 'bearing' +] + +# ============================================================================ +# Redis Client +# ============================================================================ + +redis_client = None + +def connect_redis(): + """Connect to Redis""" + global redis_client + + try: + redis_client = redis.Redis( + host=REDIS_HOST, + port=REDIS_PORT, + db=REDIS_DB, + password=REDIS_PASSWORD, + decode_responses=True + ) + + # Test connection + redis_client.ping() + print(f"✓ Connected to Redis at {REDIS_HOST}:{REDIS_PORT}") + return True + + except redis.ConnectionError as e: + print(f"✗ Failed to connect to Redis: {e}") + print(f" Make sure Redis is running: docker-compose up -d redis") + return False + except Exception as e: + print(f"✗ Redis connection error: {e}") + return False + +def validate_vehicle_data(data): + """ + Validate that vehicle data contains required fields for ETA prediction. + Returns (is_valid, missing_fields, missing_recommended) + """ + missing_required = [field for field in REQUIRED_FIELDS if field not in data] + missing_recommended = [field for field in RECOMMENDED_FIELDS if field not in data] + + is_valid = len(missing_required) == 0 + + return is_valid, missing_required, missing_recommended + +def enrich_vehicle_data(data): + """ + Enrich vehicle data with additional metadata and transformations. + """ + # Ensure consistent field naming + if 'route' in data and 'route_id' not in data: + data['route_id'] = data['route'] + + # Add UTC timestamp if not present or normalize it + if 'timestamp' in data: + try: + # Parse and normalize to ISO format with timezone + ts = datetime.fromisoformat(data['timestamp'].replace('Z', '+00:00')) + data['timestamp'] = ts.isoformat() + except: + # If parsing fails, use current time + data['timestamp'] = datetime.now(timezone.utc).isoformat() + else: + data['timestamp'] = datetime.now(timezone.utc).isoformat() + + # Ensure heading and bearing are both present (some systems use one or the other) + if 'heading' in data and 'bearing' not in data: + data['bearing'] = data['heading'] + elif 'bearing' in data and 'heading' not in data: + data['heading'] = data['bearing'] + + # Add processing metadata + data['_mqtt_received_at'] = datetime.now(timezone.utc).isoformat() + data['_data_quality'] = 'complete' if all(f in data for f in RECOMMENDED_FIELDS) else 'partial' + + return data + +def store_in_redis(vehicle_id, data): + """Store vehicle data in Redis with enrichment""" + try: + # Validate data + is_valid, missing_required, missing_recommended = validate_vehicle_data(data) + + if not is_valid: + print(f"⚠️ Invalid data for {vehicle_id}: missing required fields {missing_required}") + return False + + # Warn about missing recommended fields + if missing_recommended: + print(f"ℹ️ {vehicle_id} missing recommended fields: {missing_recommended}") + + # Enrich data + enriched_data = enrich_vehicle_data(data) + + # Create Redis key + key = f"{REDIS_KEY_PREFIX}{vehicle_id}" + + # Convert data to JSON string + json_data = json.dumps(enriched_data) + + # Store with TTL (expires after REDIS_TTL seconds) + redis_client.setex(key, REDIS_TTL, json_data) + + # Optional: Publish to Redis Pub/Sub channel for real-time subscribers + if REDIS_PUBSUB_ENABLED: + redis_client.publish(REDIS_PUBSUB_CHANNEL, json_data) + + return True + + except Exception as e: + print(f"✗ Error storing data in Redis: {e}") + import traceback + traceback.print_exc() + return False + +# ============================================================================ +# MQTT Callbacks +# ============================================================================ + +message_count = 0 +error_count = 0 +warning_count = 0 +start_time = None + +def on_connect(client, userdata, flags, rc): + """Callback when connected to MQTT broker""" + global start_time + + if rc == 0: + print(f"✓ Connected to MQTT broker at {MQTT_HOST}:{MQTT_PORT}") + + # Subscribe to topic + client.subscribe(MQTT_TOPIC, qos=1) + print(f"✓ Subscribed to topic: {MQTT_TOPIC}") + print("\n" + "="*70) + print("MQTT → Redis Bridge Active (ETA Prediction Pipeline)") + print("="*70) + print("Features:") + print(" • Data validation (required & recommended fields)") + print(" • Timestamp normalization") + print(" • Field enrichment (heading/bearing consistency)") + print(" • Quality scoring") + print("="*70) + print(f"Listening for messages... (press Ctrl+C to stop)\n") + + start_time = time.time() + + else: + print(f"✗ Failed to connect to MQTT broker, return code: {rc}") + print(" Return codes: 0=Success, 1=Protocol version, 2=Invalid client ID") + print(" 3=Server unavailable, 4=Bad credentials, 5=Not authorized") + sys.exit(1) + +def on_message(client, userdata, msg): + """Callback when a message is received""" + global message_count, error_count, warning_count + + try: + # Parse the message + payload = msg.payload.decode('utf-8') + data = json.loads(payload) + + # Get vehicle ID + vehicle_id = data.get('vehicle_id') + + if not vehicle_id: + print(f"⚠️ Message missing vehicle_id: {msg.topic}") + error_count += 1 + return + + # Add MQTT metadata + data['_mqtt_topic'] = msg.topic + + # Store in Redis (with validation and enrichment) + if store_in_redis(vehicle_id, data): + message_count += 1 + + # Detailed output for first few messages, then summary + if message_count <= 5: + print(f"✓ [{message_count}] Stored {vehicle_id}") + print(f" Route: {data.get('route_id', 'N/A')}") + print(f" Position: ({data.get('lat'):.4f}, {data.get('lon'):.4f})") + print(f" Speed: {data.get('speed')} km/h") + print(f" Next Stop: {data.get('stop_id', 'N/A')}") + print(f" Quality: {data.get('_data_quality', 'unknown')}") + elif message_count % 10 == 0: + elapsed = time.time() - start_time if start_time else 0 + rate = message_count / elapsed if elapsed > 0 else 0 + print(f"📊 [{message_count}] messages | " + f"{rate:.1f} msg/sec | " + f"{error_count} errors | " + f"{warning_count} warnings") + else: + # Compact output for routine messages + quality_icon = "✓" if data.get('_data_quality') == 'complete' else "⚠" + print(f"{quality_icon} [{message_count}] {vehicle_id} → " + f"{data.get('route_id', 'N/A'):10s} | " + f"Speed: {data.get('speed', 0):5.1f} km/h | " + f"Stop: {data.get('stop_id', 'N/A')}") + else: + error_count += 1 + + except json.JSONDecodeError as e: + print(f"✗ Failed to decode JSON from topic {msg.topic}: {e}") + error_count += 1 + except Exception as e: + print(f"✗ Error processing message: {e}") + import traceback + traceback.print_exc() + error_count += 1 + +def on_disconnect(client, userdata, rc): + """Callback when disconnected from MQTT broker""" + if rc != 0: + print(f"\n✗ Unexpected disconnection from MQTT broker (code: {rc})") + print(" Attempting to reconnect...") + +# ============================================================================ +# Main Function +# ============================================================================ + +def print_config(): + """Print current configuration""" + print("="*70) + print("MQTT to Redis Subscriber - ETA Prediction Pipeline") + print("="*70) + print(f"MQTT Broker: {MQTT_HOST}:{MQTT_PORT}") + print(f"MQTT Topic: {MQTT_TOPIC}") + print(f"Redis Server: {REDIS_HOST}:{REDIS_PORT}") + print(f"Redis Key: {REDIS_KEY_PREFIX}") + print(f"Redis TTL: {REDIS_TTL} seconds") + print(f"Pub/Sub: {'Enabled' if REDIS_PUBSUB_ENABLED else 'Disabled'}") + if REDIS_PUBSUB_ENABLED: + print(f"Pub/Sub Channel: {REDIS_PUBSUB_CHANNEL}") + print() + print("Required Fields: " + ", ".join(REQUIRED_FIELDS)) + print("Recommended: " + ", ".join(RECOMMENDED_FIELDS)) + print("="*70) + print() + +def print_stats(): + """Print final statistics""" + print("\n" + "="*70) + print("FINAL STATISTICS") + print("="*70) + print(f"Total messages processed: {message_count}") + print(f"Total errors: {error_count}") + print(f"Total warnings: {warning_count}") + + if message_count > 0: + success_rate = ((message_count / (message_count + error_count)) * 100) if (message_count + error_count) > 0 else 0 + print(f"Success rate: {success_rate:.1f}%") + + if start_time: + elapsed = time.time() - start_time + rate = message_count / elapsed if elapsed > 0 else 0 + print(f"Average rate: {rate:.2f} messages/second") + print(f"Running time: {elapsed:.1f} seconds") + print("="*70) + +def main(): + """Main function""" + print_config() + + # Connect to Redis first + print("Connecting to Redis...") + if not connect_redis(): + print("\n✗ Cannot start without Redis connection") + sys.exit(1) + + print() + + # Create MQTT client + print("Setting up MQTT client...") + mqtt_client = mqtt.Client(client_id="mqtt-redis-bridge-eta") + mqtt_client.username_pw_set(MQTT_USER, MQTT_PASS) + + # Set up callbacks + mqtt_client.on_connect = on_connect + mqtt_client.on_message = on_message + mqtt_client.on_disconnect = on_disconnect + + try: + # Connect to MQTT broker + print(f"Connecting to MQTT broker at {MQTT_HOST}:{MQTT_PORT}...") + mqtt_client.connect(MQTT_HOST, MQTT_PORT, 60) + + # Start the loop (blocking) + mqtt_client.loop_forever() + + except KeyboardInterrupt: + print_stats() + + except ConnectionRefusedError: + print(f"\n✗ Connection refused to MQTT broker at {MQTT_HOST}:{MQTT_PORT}") + print(" Make sure RabbitMQ is running: docker-compose up -d rabbitmq") + sys.exit(1) + + except Exception as e: + print(f"\n✗ Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + finally: + # Cleanup + mqtt_client.disconnect() + if redis_client: + redis_client.close() + print("\n✓ Subscriber stopped cleanly\n") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/eta_prediction/bytewax/subscriber/pyproject.toml b/eta_prediction/bytewax/subscriber/pyproject.toml new file mode 100644 index 0000000..83d3476 --- /dev/null +++ b/eta_prediction/bytewax/subscriber/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "databus-mqtt" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "paho-mqtt>=2.1.0", + "redis>=7.0.0", +] + +[project.optional-dependencies] +dev = [] + +[dependency-groups] +dev = [ + "ruff>=0.14.3", +] diff --git a/eta_prediction/bytewax/subscriber/uv.lock b/eta_prediction/bytewax/subscriber/uv.lock new file mode 100644 index 0000000..b50c5f1 --- /dev/null +++ b/eta_prediction/bytewax/subscriber/uv.lock @@ -0,0 +1,71 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "databus-mqtt" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "paho-mqtt" }, + { name = "redis" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "paho-mqtt", specifier = ">=2.1.0" }, + { name = "redis", specifier = ">=7.0.0" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.14.3" }] + +[[package]] +name = "paho-mqtt" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" }, + { url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" }, + { url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" }, + { url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" }, + { url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" }, +] diff --git a/eta_prediction/bytewax/test_redis_predictions.py b/eta_prediction/bytewax/test_redis_predictions.py new file mode 100644 index 0000000..aa48aae --- /dev/null +++ b/eta_prediction/bytewax/test_redis_predictions.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +Simple tester to verify ETA predictions are being stored in Redis. + +This script: +1. Connects to Redis +2. Reads predictions under 'predictions:*' keys +3. Prints predictions as they appear / get updated + +Usage: + python test_redis_predictions.py # One-time snapshot + python test_redis_predictions.py --continuous # Keep monitoring + python test_redis_predictions.py --vehicle bus_42 # Specific vehicle +""" + +import json +import time +import argparse +from datetime import datetime +from typing import Dict, Any +import redis + + +class PredictionMonitor: + """Monitor ETA predictions in Redis and print them.""" + + def __init__(self, redis_host: str = "localhost", redis_port: int = 6379, redis_db: int = 0): + self.client = redis.Redis( + host=redis_host, + port=redis_port, + db=redis_db, + decode_responses=True, + ) + # Track last-seen "version" per key so we also catch updates to existing keys + self.last_seen: Dict[str, str] = {} + + def connect(self) -> bool: + """Test Redis connection""" + try: + self.client.ping() + print("✓ Connected to Redis") + return True + except redis.ConnectionError as e: + print(f"✗ Failed to connect to Redis: {e}") + return False + + def _print_prediction(self, key: str, data: Any): + """Pretty-print a single prediction entry.""" + print("=" * 80) + print(f"{datetime.now().isoformat()} | KEY: {key}") + print("-" * 80) + + # If JSON-parsed dict, pretty-print it + if isinstance(data, dict): + print(json.dumps(data, indent=2, sort_keys=True)) + else: + # Raw string + print(data) + + print("=" * 80) + print() + + def snapshot(self, pattern: str): + """One-time snapshot of all predictions matching the pattern.""" + keys = self.client.keys(pattern) + print(f"\n📊 Found {len(keys)} prediction keys in Redis (pattern: '{pattern}')\n") + + if not keys: + print("No predictions found.") + return + + for key in sorted(keys): + value = self.client.get(key) + if not value: + continue + + try: + data = json.loads(value) + except json.JSONDecodeError: + data = value # print raw + + self._print_prediction(key, data) + + def monitor(self, pattern: str, interval: int = 2): + """Continuously monitor Redis for new or updated predictions.""" + print(f"\n🔍 Monitoring Redis for predictions (pattern: '{pattern}')...") + print("Press Ctrl+C to stop.\n") + + try: + while True: + keys = self.client.keys(pattern) + + for key in keys: + value = self.client.get(key) + if not value: + continue + + # Try to parse JSON so we can use 'computed_at' as a version + computed_at = None + parsed = None + try: + parsed = json.loads(value) + if isinstance(parsed, dict): + computed_at = str(parsed.get("computed_at") or "") + except json.JSONDecodeError: + parsed = value + + # Fallback: if no computed_at, use the raw string as version + version = computed_at or value + + # Only print if we've never seen this version for this key + if self.last_seen.get(key) == version: + continue + + self.last_seen[key] = version + self._print_prediction(key, parsed) + + time.sleep(interval) + + except KeyboardInterrupt: + print("\n\n✓ Monitoring stopped") + + +def main(): + parser = argparse.ArgumentParser( + description="Test ETA predictions stored in Redis", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python test_redis_predictions.py # One-time snapshot + python test_redis_predictions.py --continuous # Keep monitoring + python test_redis_predictions.py --vehicle bus_42 # Specific vehicle + python test_redis_predictions.py -c -i 5 # Monitor every 5 seconds + """, + ) + + parser.add_argument( + "--continuous", + "-c", + action="store_true", + help="Continuously monitor for predictions", + ) + + parser.add_argument( + "--interval", + "-i", + type=int, + default=2, + help="Polling interval in seconds for continuous mode (default: 2)", + ) + + parser.add_argument( + "--vehicle", + "-v", + type=str, + help="Monitor specific vehicle ID (uses predictions: pattern)", + ) + + parser.add_argument( + "--host", + type=str, + default="localhost", + help="Redis host (default: localhost)", + ) + + parser.add_argument( + "--port", + type=int, + default=6379, + help="Redis port (default: 6379)", + ) + + parser.add_argument( + "--db", + type=int, + default=0, + help="Redis database (default: 0)", + ) + + args = parser.parse_args() + + pattern = f"predictions:{args.vehicle}" if args.vehicle else "predictions:*" + + monitor = PredictionMonitor( + redis_host=args.host, + redis_port=args.port, + redis_db=args.db, + ) + + if not monitor.connect(): + return 1 + + if args.continuous: + monitor.monitor(pattern=pattern, interval=args.interval) + else: + monitor.snapshot(pattern=pattern) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/eta_prediction/bytewax/uv.lock b/eta_prediction/bytewax/uv.lock new file mode 100644 index 0000000..5eb0c8a --- /dev/null +++ b/eta_prediction/bytewax/uv.lock @@ -0,0 +1,665 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, +] + +[[package]] +name = "billiard" +version = "4.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/50/cc2b8b6e6433918a6b9a3566483b743dcd229da1e974be9b5f259db3aad7/billiard-4.2.3.tar.gz", hash = "sha256:96486f0885afc38219d02d5f0ccd5bec8226a414b834ab244008cbb0025b8dcb", size = 156450, upload-time = "2025-11-16T17:47:30.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/cc/38b6f87170908bd8aaf9e412b021d17e85f690abe00edf50192f1a4566b9/billiard-4.2.3-py3-none-any.whl", hash = "sha256:989e9b688e3abf153f307b68a1328dfacfb954e30a4f920005654e276c69236b", size = 87042, upload-time = "2025-11-16T17:47:29.005Z" }, +] + +[[package]] +name = "bytewax" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/eb/ee253e60ab676e2a70aa306cffee67d6a6a40993c8467d3b83ea8bb91e99/bytewax-0.21.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:62f5edef91908e555b70f3794271cd29ff29132a63b0b5cd367c5f53695a3c14", size = 6140409, upload-time = "2024-11-25T20:09:56.772Z" }, + { url = "https://files.pythonhosted.org/packages/38/ef/4dd07083f32f9b019101837a10dc97806fbe3455c70f8cd976eb987dedab/bytewax-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3ab20069f323ffe508249d72c8c708cac05fddea11b74943103c623676785b6", size = 6041464, upload-time = "2024-11-25T20:09:58.244Z" }, + { url = "https://files.pythonhosted.org/packages/20/1c/41b84d0de3949d75dd52b4b04341bed1c72250c4c1e017b527d7f7570f0b/bytewax-0.21.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8da9bfceb3299df8cf4e8e7efbbda924dbbfea990dff50c8602a059dc8b9cf", size = 7953966, upload-time = "2024-11-25T20:09:59.709Z" }, + { url = "https://files.pythonhosted.org/packages/87/f2/57829eea0d188bd1d54e2555a11ecfd3589ec66d710c8163fb7010a0c82e/bytewax-0.21.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba078e8bc7c6f1e7d7c99c4c07433977717b949112fc53377a0b0f3289398072", size = 7681915, upload-time = "2024-11-25T20:10:01.819Z" }, + { url = "https://files.pythonhosted.org/packages/1f/66/47d1dbdd2a035074b2be3eac983d15742cc2749fee24aa3cc0c505277622/bytewax-0.21.1-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:a118a61a5fc5a1849605e062f80e897922dbdadd37790cf8379e8f1e000d463c", size = 7983932, upload-time = "2024-11-25T20:10:03.284Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/82686047e63fa709dc084399230c430c4fd2f2f2de3cf8be8d339ebd3f21/bytewax-0.21.1-cp312-none-win_amd64.whl", hash = "sha256:9b3c44984fe76da1fbac941392103610f884523a733a43fcd74543927c179854", size = 5183365, upload-time = "2024-11-25T20:10:04.676Z" }, +] + +[[package]] +name = "celery" +version = "5.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "django" +version = "5.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/a2/933dbbb3dd9990494960f6e64aca2af4c0745b63b7113f59a822df92329e/django-5.2.8.tar.gz", hash = "sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f", size = 10849032, upload-time = "2025-11-05T14:07:32.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/3d/a035a4ee9b1d4d4beee2ae6e8e12fe6dee5514b21f62504e22efcbd9fb46/django-5.2.8-py3-none-any.whl", hash = "sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f", size = 8289692, upload-time = "2025-11-05T14:07:28.761Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" }, + { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" }, + { url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" }, + { url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" }, +] + +[[package]] +name = "gtfs-realtime-bindings" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/55/ed46db9267d2615851bfba3c22b6c0e7f88751efb5d5d1468291935c7f65/gtfs-realtime-bindings-1.0.0.tar.gz", hash = "sha256:2e8ced8904400cc93ab7e8520adb6934cfa601edacc6f593fc2cb4448662bb47", size = 6197, upload-time = "2023-02-23T17:53:20.8Z" } + +[[package]] +name = "gtfsrt-tools" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "bytewax" }, + { name = "celery" }, + { name = "django" }, + { name = "gtfs-realtime-bindings" }, + { name = "matplotlib" }, + { name = "pandas" }, + { name = "psycopg" }, + { name = "redis" }, + { name = "requests" }, + { name = "scikit-learn" }, + { name = "xgboost" }, +] + +[package.metadata] +requires-dist = [ + { name = "bytewax", specifier = ">=0.21.1" }, + { name = "celery", specifier = ">=5.5.3" }, + { name = "django", specifier = ">=4.2.25" }, + { name = "gtfs-realtime-bindings", specifier = ">=1.0.0" }, + { name = "matplotlib", specifier = ">=3.9.4" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "psycopg", specifier = ">=3.2.11" }, + { name = "redis", specifier = ">=7.0.1" }, + { name = "requests", specifier = ">=2.31" }, + { name = "scikit-learn", specifier = ">=1.7.2" }, + { name = "xgboost", specifier = ">=3.1.2" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, +] + +[[package]] +name = "kombu" +version = "5.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/c4/120d2dfd92dff2c776d68f361ff8705fdea2ca64e20b612fab0fd3f581ac/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:50a36e01c4a090b9f9c47d92cec54964de6b9fcb3362d0e19b8ffc6323c21b60", size = 296766525, upload-time = "2025-11-18T05:49:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/44dbb46b3d1b0ec61afda8e84837870f2f9ace33c564317d59b70bc19d3e/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:485776daa8447da5da39681af455aa3b2c2586ddcf4af8772495e7c532c7e5ab", size = 296782137, upload-time = "2025-11-18T05:49:34.248Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" }, + { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, +] + +[[package]] +name = "psycopg" +version = "3.2.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/05/d4a05988f15fcf90e0088c735b1f2fc04a30b7fc65461d6ec278f5f2f17a/psycopg-3.2.13.tar.gz", hash = "sha256:309adaeda61d44556046ec9a83a93f42bbe5310120b1995f3af49ab6d9f13c1d", size = 160626, upload-time = "2025-11-21T22:34:32.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/14/f2724bd1986158a348316e86fdd0837a838b14a711df3f00e47fba597447/psycopg-3.2.13-py3-none-any.whl", hash = "sha256:a481374514f2da627157f767a9336705ebefe93ea7a0522a6cbacba165da179a", size = 206797, upload-time = "2025-11-21T22:29:39.733Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", size = 120112, upload-time = "2025-11-28T07:10:18.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb", size = 45933, upload-time = "2025-11-28T07:10:19.73Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "xgboost" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/64/42310363ecd814de5930981672d20da3d35271721ad2d2b4970b4092825b/xgboost-3.1.2.tar.gz", hash = "sha256:0f94496db277f5c227755e1f3ec775c59bafae38f58c94aa97c5198027c93df5", size = 1237438, upload-time = "2025-11-20T18:33:29.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/1e/efdd603db8cb37422b01d925f9cce1baaac46508661c73f6aafd5b9d7c51/xgboost-3.1.2-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:b44f6ee43a28b998e289ab05285bd65a65d7999c78cf60b215e523d23dc94c5d", size = 2377854, upload-time = "2025-11-20T18:06:21.217Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c6/ed928cb106f56ab73b3f4edb5287c1352251eb9225b5932d3dd5e2803f60/xgboost-3.1.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:09690f7430504fcd3b3e62bf826bb1282bb49873b68b07120d2696ab5638df41", size = 2211078, upload-time = "2025-11-20T18:06:47.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/2f/5418f4b1deaf0886caf81c5e056299228ac2fc09b965a2dfe5e4496331c8/xgboost-3.1.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f9b83f39340e5852bbf3e918318e7feb7a2a700ac7e8603f9bc3a06787f0d86b", size = 4953319, upload-time = "2025-11-20T18:28:29.851Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/c60fcc137fa685533bb31e721de3ecc88959d393830d59d0204c5cbd2c85/xgboost-3.1.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:24879ac75c0ee21acae0101f791bc43303f072a86d70fdfc89dae10a0008767f", size = 115885060, upload-time = "2025-11-20T18:32:00.773Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/41847e45ff075f3636c95d1000e0b75189aed4f1ae18c36812575bb42b4b/xgboost-3.1.2-py3-none-win_amd64.whl", hash = "sha256:e627c50003269b4562aa611ed348dff8cb770e11a9f784b3888a43139a0f5073", size = 71979118, upload-time = "2025-11-20T18:27:55.23Z" }, +] diff --git a/eta_prediction/datasets/sample.parquet b/eta_prediction/datasets/sample.parquet new file mode 100644 index 0000000..ff28853 Binary files /dev/null and b/eta_prediction/datasets/sample.parquet differ diff --git a/eta_prediction/eta_service/README.md b/eta_prediction/eta_service/README.md new file mode 100644 index 0000000..a229f18 --- /dev/null +++ b/eta_prediction/eta_service/README.md @@ -0,0 +1,231 @@ +# ETA Service + +Real-time arrival time estimation service for transit vehicles. + +## Overview + +The `eta_service` provides a production-ready interface for generating ETA predictions from live vehicle positions. It bridges the real-time data stream (MQTT/Redis) with trained ML models. + +## Current Status: MVP (Phase 1) + +**What's Working:** +- ✅ Basic `estimate_stop_times()` function +- ✅ Temporal + spatial feature extraction +- ✅ Model registry integration +- ✅ Predictions for next N stops +- ✅ Error handling for missing models/data + +**Not Yet Implemented:** +- ⏳ Redis caching (upcoming stops, predictions) +- ⏳ Persistent route-specific model overrides +- ⏳ Shape-aware progress (currently geometric fallback only) +- ⏳ Weather integrations (currently constants) +- ⏳ Confidence intervals +- ⏳ Database integration for stop sequences + +--- + +## Quick Start + +```python +from eta_service import estimate_stop_times +from datetime import datetime, timezone + +# Vehicle position from MQTT/Redis (matches your MQTT format) +vehicle_position = { + 'vehicle_id': 'vehicle_42', + 'route': '1', + 'lat': 9.9281, + 'lon': -84.0907, + 'speed': 10.5, # m/s (will be converted to km/h internally) + 'heading': 90, + 'timestamp': datetime.now(timezone.utc).isoformat() +} + +# Upcoming stops (from cached route data) +upcoming_stops = [ + {'stop_id': 'stop_001', 'stop_sequence': 5, 'total_stop_sequence': 20, 'lat': 9.9291, 'lon': -84.0897}, + {'stop_id': 'stop_002', 'stop_sequence': 6, 'total_stop_sequence': 20, 'lat': 9.9301, 'lon': -84.0887}, + {'stop_id': 'stop_003', 'stop_sequence': 7, 'total_stop_sequence': 20, 'lat': 9.9311, 'lon': -84.0877}, +] + +# Get predictions +result = estimate_stop_times( + vehicle_position=vehicle_position, + upcoming_stops=upcoming_stops, + route_id='1', + trip_id='trip_001', + max_stops=3 +) + +# Access predictions +print(f"Vehicle: {result['vehicle_id']}") +print(f"Model: {result['model_type']}") +for pred in result['predictions']: + if not pred.get('error'): + print(f"Stop {pred['stop_id']}: {pred['eta_formatted']} ({pred['eta_minutes']:.1f} min)") +``` + +--- + +## Function Reference + +### `estimate_stop_times()` + +```python +estimate_stop_times( + vehicle_position: dict, + upcoming_stops: list[dict], + route_id: str = None, + trip_id: str = None, + model_key: str = None, + max_stops: int = 3, +) -> dict +``` + +**Parameters:** +- `vehicle_position`: Vehicle data with: + - `vehicle_id` (str) + - `lat`, `lon` (float) + - `speed` (float, in m/s) - converted to km/h internally + - `heading` (int, degrees) - optional + - `timestamp` (str, ISO format) +- `upcoming_stops`: List of stops with `stop_id`, `stop_sequence`, `lat`, `lon` +- `route_id`: Route identifier (optional, passed to models) +- `trip_id`: Trip identifier (optional, for context) +- `model_key`: Specific model to use (if `None`, uses best from registry) +- `max_stops`: Maximum stops to predict (default: 3) + +**Returns:** +```python +{ + 'vehicle_id': str, + 'route_id': str, + 'trip_id': str, + 'computed_at': str, # ISO timestamp + 'model_key': str, + 'model_type': str, # 'historical_mean', 'ewma', 'polyreg_distance', 'polyreg_time' + 'predictions': [ + { + 'stop_id': str, + 'stop_sequence': int, + 'distance_to_stop_m': float, + 'eta_seconds': float, + 'eta_minutes': float, + 'eta_formatted': str, # e.g., "5m 23s" + 'eta_timestamp': str, # ISO timestamp + }, + ... + ] +} +``` + +**Important Notes:** +- Speed must be in **m/s** (matches your MQTT format) +- Each model type has different feature requirements, handled internally +- Models use appropriate features based on their training configuration +- Missing features are filled with sensible defaults + +--- + +## Testing + +Run the test suite: + +```bash +cd eta_prediction/ +python eta_service/test_estimator.py +``` + +Test individual scenarios: + +```python +from eta_service.test_estimator import test_basic_prediction +test_basic_prediction() +``` + +--- + +## Integration with Data Pipeline + +### Expected Flow (When Complete): + +``` +MQTT Broker (raw vehicle data) + ↓ +Redis Subscriber + ↓ +Bytewax Stream Processor + ↓ +estimate_stop_times() ← YOU ARE HERE + ↓ +Redis (predictions cache) + ↓ +API / Dashboard +``` + +### Current MVP Flow: + +``` +Mock/Real Vehicle Position + ↓ +estimate_stop_times() + ↓ +Return predictions dict +``` + +--- + +## Directory Structure + +``` +eta_service/ +├── __init__.py # Module exports +├── estimator.py # Main estimation logic (MVP) +├── test_estimator.py # Test suite +├── cache.py # (TODO: Redis operations) +├── config.py # (TODO: Configuration) +└── README.md # This file +``` + +--- + +## Next Steps (Phase 2) + +1. **Redis Integration** + - Cache upcoming stops by route + - Store predictions with TTL + - Implement prediction retrieval API + +2. **Enhanced Model Selection** + - Route-specific model lookup + - Fallback hierarchy + - Model performance monitoring + +3. **Additional Features** + - Shape-derived progress metrics + - Weather integration + - Confidence intervals + +4. **Performance Optimization** + - Batch predictions for multiple vehicles + - Model warm-up/preloading + - Feature extraction caching + +--- + +## Dependencies + +- `feature_engineering` module (temporal, spatial) +- `models` module (registry, trained models) +- Python 3.10+ +- Standard library: `math`, `datetime`, `pathlib` + +--- + +## Notes + +- All timestamps are UTC +- Distances in meters, speeds in km/h +- Models must be trained and registered before use +- Function is designed to be called from stream processors (Bytewax, Flink, etc.) diff --git a/eta_prediction/eta_service/__init__.py b/eta_prediction/eta_service/__init__.py new file mode 100644 index 0000000..5122ad2 --- /dev/null +++ b/eta_prediction/eta_service/__init__.py @@ -0,0 +1,11 @@ + +""" +ETA Service Module + +Main interface for real-time ETA predictions. +""" + +from .estimator import estimate_stop_times + +__version__ = "0.1.0" +__all__ = ["estimate_stop_times"] \ No newline at end of file diff --git a/eta_prediction/eta_service/estimator.py b/eta_prediction/eta_service/estimator.py new file mode 100644 index 0000000..506da33 --- /dev/null +++ b/eta_prediction/eta_service/estimator.py @@ -0,0 +1,476 @@ +""" +ETA Service - Low-Latency Implementation with Direct Shape Support +Estimates stop arrival times from vehicle positions with optional pre-loaded shapes. +No database calls during inference for maximum performance. +""" + +import sys +from pathlib import Path +from datetime import datetime, timezone +import math + +# Add parent directories to path for imports +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) +sys.path.insert(0, str(project_root / "feature_engineering")) +sys.path.insert(0, str(project_root / "models")) + +from feature_engineering.temporal import extract_temporal_features + +# Import registry with proper path resolution +import os +from models.common.registry import get_registry + + +# Set model registry directory before importing estimator +MODELS_DIR = project_root / "models" / "trained" +os.environ['MODEL_REGISTRY_DIR'] = str(MODELS_DIR) + +print(f"🔧 Project root: {project_root}") +print(f"🔧 Model registry: {MODELS_DIR}") +print(f"🔧 Registry exists: {MODELS_DIR.exists()}") + +# Verify registry contents if it exists +if MODELS_DIR.exists(): + registry_files = list(MODELS_DIR.glob("*.pkl")) + print(f"🔧 Found {len(registry_files)} .pkl model files") + if (MODELS_DIR / "registry.json").exists(): + print(f"🔧 registry.json found ✓") + else: + print(f"⚠️ registry.json NOT found") +else: + print(f"⚠️ WARNING: Model registry directory does not exist!") +print() + +# Import shape-aware spatial features +try: + from feature_engineering.spatial import ( + ShapePolyline, + calculate_distance_features_with_shape + ) + SHAPE_SUPPORT = True +except ImportError: + SHAPE_SUPPORT = False + print("⚠️ Shape-aware spatial features not available, using fallback") + + +def haversine_distance(lat1, lon1, lat2, lon2): + """Calculate distance between two points in meters.""" + R = 6371000 # Earth radius in meters + phi1, phi2 = math.radians(lat1), math.radians(lat2) + dphi = math.radians(lat2 - lat1) + dlambda = math.radians(lon2 - lon1) + + a = math.sin(dphi/2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda/2)**2 + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + + return R * c + + +def _progress_features_fallback(vehicle_position, stop, next_stop, total_segments_hint): + """ + Fallback: Approximate distance/progress metrics without shapes. + Used when shape data is unavailable for low-latency inference. + """ + vp_lat = vehicle_position["lat"] + vp_lon = vehicle_position["lon"] + stop_lat = stop["lat"] + stop_lon = stop["lon"] + + distance_to_stop = haversine_distance(vp_lat, vp_lon, stop_lat, stop_lon) + + progress_on_segment = 0.0 + if next_stop: + next_lat = next_stop["lat"] + next_lon = next_stop["lon"] + segment_length = haversine_distance(stop_lat, stop_lon, next_lat, next_lon) + if segment_length > 0: + distance_to_next = haversine_distance(vp_lat, vp_lon, next_lat, next_lon) + progress_on_segment = max(0.0, min(1.0, 1.0 - (distance_to_next / segment_length))) + + stop_seq = ( + stop.get("stop_sequence") + or stop.get("sequence") + or stop.get("stop_order") + or 1 + ) + total_segments = ( + stop.get("total_stop_sequence") + or total_segments_hint + or stop_seq + ) + completed = max(float(stop_seq) - 1.0, 0.0) + denom = max(float(total_segments), 1.0) + progress_ratio = max(0.0, min(1.0, (completed + progress_on_segment) / denom)) + + return { + 'distance_to_stop_m': distance_to_stop, + 'progress_on_segment': progress_on_segment, + 'progress_ratio': progress_ratio, + 'cross_track_error': None, + 'shape_progress': None, + 'shape_distance_to_stop': None + } + + +def _progress_features_with_shape(vehicle_position, stop, next_stop, shape, vehicle_stop_order, total_segments): + """ + Shape-aware: Calculate distance/progress using pre-loaded GTFS shape polyline. + Returns enhanced spatial features including cross-track error and shape-based distances. + """ + features = calculate_distance_features_with_shape( + vehicle_position=vehicle_position, + stop=stop, + next_stop=next_stop, + shape=shape, + vehicle_stop_order=vehicle_stop_order, + total_segments=total_segments + ) + + return { + 'distance_to_stop_m': features.get('distance_to_stop', 0.0), + 'progress_on_segment': features.get('progress_on_segment', 0.0), + 'progress_ratio': features.get('progress_ratio', 0.0), + 'cross_track_error': features.get('cross_track_error'), + 'shape_progress': features.get('shape_progress'), + 'shape_distance_to_stop': features.get('shape_distance_to_stop') + } + + +def _predict_with_model(model_key, model_type, features, distance_m): + """ + Call the appropriate predict_eta function based on model type. + """ + + if model_type == 'historical_mean': + from models.historical_mean.predict import predict_eta + return predict_eta( + model_key=model_key, + route_id=features.get('route_id', 'unknown'), + stop_sequence=features.get('stop_sequence', 0), + hour=features.get('hour', 0), + day_of_week=features.get('day_of_week', 0), + is_peak_hour=features.get('is_peak_hour', False) + ) + + elif model_type == 'ewma': + from models.ewma.predict import predict_eta + return predict_eta( + model_key=model_key, + route_id=features.get('route_id', 'unknown'), + stop_sequence=features.get('stop_sequence', 0), + hour=features.get('hour', 0) + ) + + elif model_type == 'polyreg_distance': + from models.polyreg_distance.predict import predict_eta + return predict_eta( + model_key=model_key, + distance_to_stop=distance_m + ) + + elif model_type == 'polyreg_time': + from models.polyreg_time.predict import predict_eta + return predict_eta( + model_key=model_key, + distance_to_stop=distance_m, + progress_on_segment=features.get('progress_on_segment'), + progress_ratio=features.get('progress_ratio'), + hour=features.get('hour', 0), + day_of_week=features.get('day_of_week', 0), + is_peak_hour=features.get('is_peak_hour', False), + is_weekend=features.get('is_weekend', False), + is_holiday=features.get('is_holiday', False), + temperature_c=features.get('temperature_c', 25.0), + precipitation_mm=features.get('precipitation_mm', 0.0), + wind_speed_kmh=features.get('wind_speed_kmh') + ) + + elif model_type == 'xgboost': + from models.xgb.predict import predict_eta + return predict_eta( + model_key=model_key, + distance_to_stop=distance_m, + progress_on_segment=features.get('progress_on_segment'), + progress_ratio=features.get('progress_ratio'), + hour=features.get('hour', 0), + day_of_week=features.get('day_of_week', 0), + is_peak_hour=features.get('is_peak_hour', False), + is_weekend=features.get('is_weekend', False), + is_holiday=features.get('is_holiday', False), + temperature_c=features.get('temperature_c', 25.0), + precipitation_mm=features.get('precipitation_mm', 0.0), + wind_speed_kmh=features.get('wind_speed_kmh', None) + ) + + else: + raise ValueError(f"Unknown model type: {model_type}") + + +def estimate_stop_times( + vehicle_position: dict, + upcoming_stops: list[dict], + route_id: str = None, + trip_id: str = None, + model_key: str = None, + model_type: str = None, + prefer_route_model: bool = True, + max_stops: int = 3, + shape: object = None, +) -> dict: + """ + Estimate arrival times for upcoming stops based on vehicle position. + + LOW-LATENCY DESIGN: No database calls during inference! + All data (stops, shapes) must be pre-loaded and passed as arguments. + + Args: + vehicle_position: Dict with vehicle_id, route, lat, lon, speed, timestamp + upcoming_stops: List of dicts with stop_id, stop_sequence, lat, lon + route_id: Optional route override + trip_id: Optional trip ID for metadata + model_key: Optional explicit model to use + model_type: Optional model type filter + prefer_route_model: If True, prefer route-specific models over global + max_stops: Maximum number of stops to predict + shape: Optional pre-loaded ShapePolyline object for shape-aware features. + If None, falls back to haversine-based distance calculations. + Pass a ShapePolyline instance from the cache for best performance. + + Returns: + Dict with predictions, model info, and metadata + """ + + # Validate inputs + if not vehicle_position or not upcoming_stops: + return { + 'vehicle_id': vehicle_position.get('vehicle_id', 'unknown') if vehicle_position else 'unknown', + 'route_id': route_id, + 'trip_id': trip_id, + 'computed_at': datetime.now(timezone.utc).isoformat(), + 'model_key': None, + 'predictions': [], + 'error': 'Missing vehicle position or stops' + } + + stops_to_predict = upcoming_stops[:max_stops] + + # Parse timestamp + vp_timestamp_str = vehicle_position['timestamp'] + if vp_timestamp_str.endswith('Z'): + vp_timestamp_str = vp_timestamp_str.replace('Z', '+00:00') + vp_timestamp = datetime.fromisoformat(vp_timestamp_str) + + # Extract temporal features + temporal_features = extract_temporal_features( + vp_timestamp, + tz='America/Costa_Rica', + region='CR' + ) + + # Determine route + if route_id is None: + route_id = vehicle_position.get('route', 'unknown') + + # Validate shape if provided + shape_available = shape is not None and SHAPE_SUPPORT + if shape and not SHAPE_SUPPORT: + print("[ETA Service] Shape provided but spatial module unavailable, using fallback") + shape = None + + # Load registry + registry = get_registry() + model_scope = 'unknown' + + # ============================================================ + # SMART MODEL SELECTION + # ============================================================ + if model_key is None: + if prefer_route_model and route_id and route_id != 'unknown': + model_key = registry.get_best_model( + model_type=model_type, + route_id=route_id, + metric='test_mae_seconds' + ) + + if model_key: + model_scope = 'route' + else: + model_key = registry.get_best_model( + model_type=model_type, + route_id='global', + metric='test_mae_seconds' + ) + model_scope = 'global' + else: + model_key = registry.get_best_model( + model_type=model_type, + route_id='global', + metric='test_mae_seconds' + ) + model_scope = 'global' + + # Last fallback + if model_key is None: + model_key = registry.get_best_model(model_type=model_type) + + if model_key is None: + return { + 'vehicle_id': vehicle_position['vehicle_id'], + 'route_id': route_id, + 'trip_id': trip_id, + 'computed_at': datetime.now(timezone.utc).isoformat(), + 'model_key': None, + 'predictions': [], + 'error': 'No trained models found for model_type' + } + + # Load metadata + try: + model_metadata = registry.load_metadata(model_key) + actual_model_type = model_metadata.get('model_type', 'unknown') + model_route_id = model_metadata.get('route_id') + + if model_route_id not in (None, 'global'): + model_scope = 'route' + elif model_scope == 'unknown': + model_scope = 'global' + + except Exception as e: + return { + 'vehicle_id': vehicle_position['vehicle_id'], + 'route_id': route_id, + 'trip_id': trip_id, + 'computed_at': datetime.now(timezone.utc).isoformat(), + 'model_key': model_key, + 'predictions': [], + 'error': f'Failed to load model metadata: {str(e)}' + } + + # ============================================================ + # PREDICT STOP ETAs (SHAPE-AWARE IF AVAILABLE) + # ============================================================ + predictions = [] + approx_total_segments = max( + ( + stop.get('total_stop_sequence') + or stop.get('stop_sequence') + or stop.get('sequence') + or 0 + ) + for stop in stops_to_predict + ) if stops_to_predict else 0 + if approx_total_segments <= 0: + approx_total_segments = max(len(stops_to_predict), 1) + + for idx, stop in enumerate(stops_to_predict): + next_stop = stops_to_predict[idx + 1] if idx + 1 < len(stops_to_predict) else None + + stop_sequence_value = ( + stop.get('stop_sequence') + or stop.get('sequence') + or stop.get('stop_order') + or idx + 1 + ) + + # Calculate spatial features (shape-aware if available) + if shape_available: + spatial_features = _progress_features_with_shape( + vehicle_position, + stop, + next_stop, + shape, + vehicle_stop_order=stop_sequence_value, + total_segments=approx_total_segments + ) + else: + spatial_features = _progress_features_fallback( + vehicle_position, + stop, + next_stop, + approx_total_segments + ) + + distance_m = spatial_features['distance_to_stop_m'] + + # Build features + progress_on_segment = spatial_features['progress_on_segment'] + if progress_on_segment is None: + progress_on_segment = 0.0 + + progress_ratio = spatial_features['progress_ratio'] + if progress_ratio is None: + # If we truly cannot infer progress, treat as at start of segment + progress_ratio = 0.0 + + features = { + 'route_id': route_id, + 'stop_sequence': stop_sequence_value, + 'distance_to_stop': distance_m, + 'progress_on_segment': progress_on_segment, + 'progress_ratio': progress_ratio, + 'hour': temporal_features['hour'], + 'day_of_week': temporal_features['day_of_week'], + 'is_weekend': temporal_features['is_weekend'], + 'is_holiday': temporal_features['is_holiday'], + 'is_peak_hour': temporal_features['is_peak_hour'], + 'temperature_c': 25.0, + 'precipitation_mm': 0.0, + 'wind_speed_kmh': None, + } + + try: + result = _predict_with_model(model_key, actual_model_type, features, distance_m) + + eta_seconds = result.get('eta_seconds', 0.0) + eta_minutes = eta_seconds / 60.0 + eta_formatted = result.get( + 'eta_formatted', + f"{int(eta_minutes)}m {int(eta_seconds % 60)}s" + ) + + eta_ts = datetime.fromtimestamp(vp_timestamp.timestamp() + eta_seconds, tz=timezone.utc) + + prediction = { + 'stop_id': stop['stop_id'], + 'stop_sequence': stop_sequence_value, + 'distance_to_stop_m': round(distance_m, 1), + 'eta_seconds': round(eta_seconds, 1), + 'eta_minutes': round(eta_minutes, 2), + 'eta_formatted': eta_formatted, + 'eta_timestamp': eta_ts.isoformat(), + } + + # Add shape-based metrics if available + if spatial_features.get('cross_track_error') is not None: + prediction['cross_track_error_m'] = round(spatial_features['cross_track_error'], 1) + if spatial_features.get('shape_progress') is not None: + prediction['shape_progress'] = round(spatial_features['shape_progress'], 3) + if spatial_features.get('shape_distance_to_stop') is not None: + prediction['shape_distance_to_stop_m'] = round(spatial_features['shape_distance_to_stop'], 1) + + predictions.append(prediction) + + except Exception as e: + predictions.append({ + 'stop_id': stop['stop_id'], + 'stop_sequence': stop_sequence_value, + 'distance_to_stop_m': round(distance_m, 1), + 'eta_seconds': None, + 'eta_minutes': None, + 'eta_formatted': None, + 'eta_timestamp': None, + 'error': str(e), + }) + + return { + 'vehicle_id': vehicle_position['vehicle_id'], + 'route_id': route_id, + 'trip_id': trip_id, + 'computed_at': datetime.now(timezone.utc).isoformat(), + 'model_key': model_key, + 'model_type': actual_model_type, + 'model_scope': model_scope, + 'shape_used': shape_available, + 'predictions': predictions + } diff --git a/eta_prediction/eta_service/test_estimator.py b/eta_prediction/eta_service/test_estimator.py new file mode 100644 index 0000000..b36b656 --- /dev/null +++ b/eta_prediction/eta_service/test_estimator.py @@ -0,0 +1,560 @@ +""" +Enhanced test script for estimate_stop_times() with route-specific model support +Run from project root: python eta_service/test_estimator.py +""" + +import sys +from pathlib import Path +from datetime import datetime, timezone +import json + +# Setup paths +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from eta_service.estimator import estimate_stop_times +from models.common.registry import get_registry + + +def get_sample_vp_data(route='1'): + """Mock vehicle position in MQTT/Redis format.""" + return { + 'vehicle_id': f'vehicle_{route}_42', + 'route': route, + 'lat': 9.9281, + 'lon': -84.0907, + 'speed': 10.5, # m/s + 'heading': 90, + 'timestamp': datetime.now(timezone.utc).isoformat() + } + + +def get_sample_stops(): + """Mock upcoming stops.""" + return [ + {'stop_id': 'stop_001', 'stop_sequence': 5, 'total_stop_sequence': 20, 'lat': 9.9281, 'lon': -84.0907}, + {'stop_id': 'stop_002', 'stop_sequence': 6, 'total_stop_sequence': 20, 'lat': 9.9306, 'lon': -84.0882}, + {'stop_id': 'stop_003', 'stop_sequence': 7, 'total_stop_sequence': 20, 'lat': 9.9331, 'lon': -84.0857}, + {'stop_id': 'stop_004', 'stop_sequence': 8, 'total_stop_sequence': 20, 'lat': 9.9356, 'lon': -84.0832}, + ] + + +def test_basic_prediction(): + """Test with mock data using best model.""" + + print("=" * 70) + print("TEST 1: Basic Prediction with Auto-Selected Model") + print("=" * 70) + + vp_data = get_sample_vp_data() + stops = get_sample_stops() + + print(f"\n[VEHICLE POSITION]") + print(f" Vehicle: {vp_data['vehicle_id']}") + print(f" Route: {vp_data['route']}") + print(f" Location: ({vp_data['lat']}, {vp_data['lon']})") + print(f" Speed: {vp_data['speed']} m/s ({vp_data['speed'] * 3.6:.1f} km/h)") + + # Get predictions + result = estimate_stop_times( + vehicle_position=vp_data, + upcoming_stops=stops, + route_id='1', + trip_id='trip_001', + prefer_route_model=True, + max_stops=3 + ) + + # Display results + print(f"\n[PREDICTIONS]") + print(f" Model Type: {result.get('model_type', 'N/A')}") + print(f" Model Scope: {result.get('model_scope', 'N/A')}") + print(f" Model Key: {result['model_key']}") + print(f" Computed at: {result['computed_at']}") + + if result.get('error'): + print(f"\n ERROR: {result['error']}") + return False + + print(f"\n Upcoming Stops ({len(result['predictions'])}):") + print(" " + "-" * 66) + + for i, pred in enumerate(result['predictions'], 1): + if pred.get('error'): + print(f"\n {i}. Stop {pred['stop_id']}: ERROR - {pred['error']}") + else: + print(f"\n {i}. Stop {pred['stop_id']} (sequence {pred['stop_sequence']})") + print(f" Distance: {pred['distance_to_stop_m']:,.1f} m") + print(f" ETA: {pred['eta_formatted']} ({pred['eta_seconds']:.0f}s)") + print(f" Arrival: {pred['eta_timestamp']}") + + print("\n" + "=" * 70) + return True + + +def test_route_specific_models(): + """Test route-specific vs global model selection.""" + + print("\n\nTEST 2: Route-Specific vs Global Model Selection") + print("=" * 70) + + registry = get_registry() + + # Get available routes with trained models + routes = registry.get_routes() + print(f"\nFound {len(routes)} routes with trained models: {routes}") + + # Also check for global models + global_models = [k for k, info in registry.registry.items() + if info.get('route_id') is None] + print(f"Found {len(global_models)} global models") + + vp_data = get_sample_vp_data() + stops = get_sample_stops()[:2] # Just 2 stops for speed + + test_routes = routes[:3] if len(routes) >= 3 else routes # Test up to 3 routes + + print(f"\n{'-' * 70}") + print("Testing route-specific model preference:") + print(f"{'-' * 70}") + + for route_id in test_routes: + print(f"\nRoute {route_id}:") + + # Test 1: With prefer_route_model=True + result_route = estimate_stop_times( + vehicle_position=get_sample_vp_data(route_id), + upcoming_stops=stops, + route_id=route_id, + prefer_route_model=True, + max_stops=2 + ) + + # Test 2: With prefer_route_model=False (force global) + result_global = estimate_stop_times( + vehicle_position=get_sample_vp_data(route_id), + upcoming_stops=stops, + route_id=route_id, + prefer_route_model=False, + max_stops=2 + ) + + print(f" Route-specific: {result_route.get('model_scope', 'N/A')}") + if result_route.get('predictions') and not result_route['predictions'][0].get('error'): + print(f" ETA: {result_route['predictions'][0]['eta_formatted']}") + + print(f" Global forced: {result_global.get('model_scope', 'N/A')}") + if result_global.get('predictions') and not result_global['predictions'][0].get('error'): + print(f" ETA: {result_global['predictions'][0]['eta_formatted']}") + + # Compare if both succeeded + if (result_route.get('predictions') and result_global.get('predictions') and + not result_route['predictions'][0].get('error') and + not result_global['predictions'][0].get('error')): + + eta_route = result_route['predictions'][0]['eta_seconds'] + eta_global = result_global['predictions'][0]['eta_seconds'] + diff = eta_route - eta_global + diff_pct = (diff / eta_global * 100) if eta_global > 0 else 0 + + print(f" Difference: {diff:+.1f}s ({diff_pct:+.1f}%)") + + print("\n" + "=" * 70) + return True + + +def test_all_model_types(): + """Test with each model type explicitly.""" + + print("\n\nTEST 3: Testing All Model Types") + print("=" * 70) + + registry = get_registry() + df = registry.list_models() + + if df.empty: + print("No models found in registry!") + return False + + # Group by model type + model_types = df['model_type'].unique() + + print(f"\nFound {len(model_types)} model types:") + for mtype in model_types: + count = len(df[df['model_type'] == mtype]) + route_count = len(df[(df['model_type'] == mtype) & (df['route_id'] != 'global')]) + global_count = len(df[(df['model_type'] == mtype) & (df['route_id'] == 'global')]) + print(f" - {mtype}: {count} models ({route_count} route-specific, {global_count} global)") + + vp_data = get_sample_vp_data() + stops = get_sample_stops()[:2] # Just 2 stops for speed + + results_by_type = {} + + print(f"\n{'-' * 70}") + print("Testing each model type:") + print(f"{'-' * 70}") + + for model_type in model_types: + print(f"\n{model_type.upper()}:") + + # Test with auto-selection for this type + result = estimate_stop_times( + vehicle_position=vp_data, + upcoming_stops=stops, + route_id='1', + trip_id='trip_001', + model_type=model_type, + prefer_route_model=True, + max_stops=2 + ) + + if result.get('error'): + print(f" ✗ Failed: {result['error']}") + else: + first_pred = result['predictions'][0] if result['predictions'] else None + if first_pred and not first_pred.get('error'): + print(f" ✓ Success!") + print(f" Scope: {result.get('model_scope', 'unknown')}") + print(f" Model: {result['model_key'][:60]}...") + print(f" First stop ETA: {first_pred['eta_formatted']}") + results_by_type[model_type] = first_pred['eta_seconds'] + else: + print(f" ✗ Prediction failed") + + # Summary comparison + if results_by_type: + print(f"\n{'-' * 70}") + print("COMPARISON (First Stop ETA):") + print(f"{'-' * 70}") + for mtype, eta_sec in sorted(results_by_type.items(), key=lambda x: x[1]): + print(f" {mtype:20s}: {eta_sec:6.0f}s ({eta_sec/60:5.1f} min)") + + print("\n" + "=" * 70) + return True + + +def test_xgboost_models(): + """Dedicated test to validate XGBoost ETA estimations.""" + + print("\n\nTEST 4: XGBoost Models (Route-Specific & Global)") + print("=" * 70) + + registry = get_registry() + + # List all xgboost models + df = registry.list_models(model_type='xgboost') + if df.empty: + print("\nNo xgboost models found in registry. Skipping test.") + print("=" * 70) + return True + + print(f"\nFound {len(df)} xgboost models in registry:") + routes = registry.get_routes(model_type='xgboost') + print(f" Routes with xgboost models: {routes}") + + # Separate route-specific and global models + route_df = df[df['route_id'] != 'global'] + global_df = df[df['route_id'] == 'global'] + + vp_data = get_sample_vp_data() + stops = get_sample_stops()[:2] + + # Test route-specific xgboost if available + if not route_df.empty: + print(f"\n--- Route-specific XGBoost ---") + test_routes = routes[:3] if len(routes) >= 3 else routes + for route_id in test_routes: + print(f"\nRoute {route_id}:") + result = estimate_stop_times( + vehicle_position=get_sample_vp_data(route_id), + upcoming_stops=stops, + route_id=route_id, + trip_id=f"trip_xgb_{route_id}", + model_type='xgboost', + prefer_route_model=True, + max_stops=2, + ) + if result.get('error'): + print(f" ✗ Error: {result['error']}") + else: + print(f" ✓ Scope: {result.get('model_scope', 'unknown')}") + print(f" Model: {result.get('model_key', '')[:60]}...") + if result['predictions'] and not result['predictions'][0].get('error'): + first = result['predictions'][0] + print(f" First stop ETA: {first['eta_formatted']} ({first['eta_seconds']:.0f}s)") + else: + print(" ✗ Prediction failed for first stop.") + else: + print("\nNo route-specific xgboost models found.") + + # Test global xgboost if available + if not global_df.empty: + print(f"\n--- Global XGBoost ---") + result = estimate_stop_times( + vehicle_position=vp_data, + upcoming_stops=stops, + route_id='1', + trip_id='trip_xgb_global', + model_type='xgboost', + prefer_route_model=False, # force global + max_stops=2, + ) + if result.get('error'): + print(f" ✗ Error: {result['error']}") + else: + print(f" ✓ Scope: {result.get('model_scope', 'unknown')}") + print(f" Model: {result.get('model_key', '')[:60]}...") + if result['predictions'] and not result['predictions'][0].get('error'): + first = result['predictions'][0] + print(f" First stop ETA: {first['eta_formatted']} ({first['eta_seconds']:.0f}s)") + else: + print(" ✗ Prediction failed for first stop.") + else: + print("\nNo global xgboost models found.") + + print("\n" + "=" * 70) + return True + + +def test_route_performance_comparison(): + """Compare predictions across different routes with varying training data.""" + + print("\n\nTEST 5: Route Performance Comparison") + print("=" * 70) + + registry = get_registry() + + # Get routes with their metadata + routes = registry.get_routes(model_type='polyreg_time') + + print("ROUTES: ", routes) + + if not routes: + print("No route-specific polyreg_time models found.") + return True + + print(f"\nComparing polyreg_time models across {len(routes)} routes:\n") + + stops = get_sample_stops()[:1] # Single stop for comparison + results = [] + + for route_id in routes: + # Get best model for this route + model_key = registry.get_best_model( + model_type='polyreg_time', + route_id=route_id, + metric='test_mae_seconds' + ) + + if not model_key: + continue + + # Get metadata + try: + metadata = registry.load_metadata(model_key) + n_trips = metadata.get('n_trips', 0) + test_mae_min = metadata.get('metrics', {}).get('test_mae_minutes', None) + except: + continue + + # Make prediction + result = estimate_stop_times( + vehicle_position=get_sample_vp_data(route_id), + upcoming_stops=stops, + route_id=route_id, + model_key=model_key, + max_stops=1 + ) + + if result.get('predictions') and not result['predictions'][0].get('error'): + eta = result['predictions'][0]['eta_seconds'] + results.append({ + 'route_id': route_id, + 'n_trips': n_trips, + 'test_mae_min': test_mae_min, + 'prediction_eta': eta + }) + + if results: + # Sort by number of trips + results.sort(key=lambda x: x['n_trips'], reverse=True) + + print(f"{'Route':8s} {'Trips':>6s} {'Test MAE':>10s} {'Prediction':>12s}") + print("-" * 45) + + for r in results: + mae_str = f"{r['test_mae_min']:.2f} min" if r['test_mae_min'] else "N/A" + eta_str = f"{r['prediction_eta']:.0f}s ({r['prediction_eta']/60:.1f}m)" + print(f"{r['route_id']:8s} {r['n_trips']:6d} {mae_str:>10s} {eta_str:>12s}") + + print(f"\n{'='*70}") + print("Insight: Routes with more training trips should have lower MAE") + print("and potentially more accurate predictions for similar conditions.") + print(f"{'='*70}") + + print("\n" + "=" * 70) + return True + + +def test_different_distances(): + """Test predictions at various distances.""" + + print("\n\nTEST 6: Predictions at Different Distances") + print("=" * 70) + + vp_data = get_sample_vp_data() + + # Create stops at various distances + test_distances = [100, 500, 1000, 2000, 5000] # meters + + print("\nTesting stop distances:") + + registry = get_registry() + # Try to find a polyreg_distance model for this test + df = registry.list_models(route_id='Green-E', model_type='xgboost') + # df = registry.list_models(model_type='polyreg_distance') + + if df.empty: + print(" No polyreg_distance models found. Using best model.") + model_key = None + else: + model_key = df.iloc[0]['model_key'] + print(f" Using model: {model_key[:60]}...") + + model_key = None + for dist_m in test_distances: + # Calculate approximate lat/lon offset + lat_offset = dist_m / 111320.0 # Rough: 1 degree lat ≈ 111.32 km + + stops = [{ + 'stop_id': f'stop_at_{dist_m}m', + 'stop_sequence': 1, + 'lat': vp_data['lat'] + lat_offset, + 'lon': vp_data['lon'] + }] + + result = estimate_stop_times( + vehicle_position=vp_data, + upcoming_stops=stops, + route_id='1', + model_key=model_key, + max_stops=1 + ) + + if result.get('error') or not result['predictions']: + print(f" {dist_m:5d}m: ERROR") + else: + pred = result['predictions'][0] + if pred.get('error'): + print(f" {dist_m:5d}m: {pred['error']}") + else: + print(f" {dist_m:5d}m → {pred['eta_formatted']:8s} ({pred['eta_seconds']:6.0f}s)") + + print("\n" + "=" * 70) + return True + + +def test_edge_cases(): + """Test error handling.""" + + print("\n\nTEST 7: Edge Cases & Error Handling") + print("=" * 70) + + # Test 1: No stops + print("\n7a. Empty stops list:") + result = estimate_stop_times( + vehicle_position=get_sample_vp_data(), + upcoming_stops=[], + route_id='1' + ) + print(f" Expected error: {result.get('error', 'N/A')}") + print(f" ✓ Handled gracefully" if result.get('error') else " ✗ Should have errored") + + # Test 2: Invalid model key + print("\n7b. Invalid model key:") + result = estimate_stop_times( + vehicle_position=get_sample_vp_data(), + upcoming_stops=get_sample_stops()[:1], + route_id='1', + model_key='nonexistent_model_12345' + ) + print(f" Expected error: {result.get('error', 'N/A')}") + print(f" ✓ Handled gracefully" if result.get('error') else " ✗ Should have errored") + + # Test 3: Missing optional fields + print("\n7c. Minimal vehicle position:") + minimal_vp = { + 'vehicle_id': 'minimal_bus', + 'lat': 9.9281, + 'lon': -84.0907, + 'timestamp': datetime.now(timezone.utc).isoformat() + # Missing: speed, heading, route + } + result = estimate_stop_times( + vehicle_position=minimal_vp, + upcoming_stops=get_sample_stops()[:1], + route_id='1' + ) + if result.get('error'): + print(f" Error: {result['error']}") + else: + print(f" ✓ Handled missing fields, got ETA: {result['predictions'][0].get('eta_formatted', 'N/A')}") + + # Test 4: Route with no trained model + print("\n7d. Route without trained model:") + result = estimate_stop_times( + vehicle_position=get_sample_vp_data('999'), + upcoming_stops=get_sample_stops()[:1], + route_id='999', + prefer_route_model=True, + max_stops=1 + ) + if result.get('error'): + print(f" Error: {result['error']}") + else: + print(f" ✓ Fell back to: {result.get('model_scope', 'N/A')} model") + if result['predictions'] and not result['predictions'][0].get('error'): + print(f" ETA: {result['predictions'][0]['eta_formatted']}") + + print("\n" + "=" * 70) + return True + + +if __name__ == '__main__': + print("\n" + "=" * 70) + print("ETA ESTIMATOR TEST SUITE - Route-Specific Edition") + print("=" * 70 + "\n") + + tests = [ + ("Basic Prediction", test_basic_prediction), + ("Route-Specific Models", test_route_specific_models), + ("All Model Types", test_all_model_types), + ("XGBoost Models", test_xgboost_models), # NEW + ("Route Performance Comparison", test_route_performance_comparison), + ("Different Distances", test_different_distances), + ("Edge Cases", test_edge_cases), + ] + + passed = 0 + failed = 0 + + for test_name, test_func in tests: + try: + if test_func(): + passed += 1 + else: + failed += 1 + except Exception as e: + print(f"\n✗ {test_name} CRASHED: {str(e)}") + import traceback + traceback.print_exc() + failed += 1 + + print("\n" + "=" * 70) + print(f"FINAL RESULTS: {passed}/{len(tests)} tests passed") + if failed == 0: + print("🎉 All tests passed!") + else: + print(f"⚠️ {failed} test(s) failed or crashed") + print("=" * 70 + "\n") diff --git a/eta_prediction/feature_engineering/README.md b/eta_prediction/feature_engineering/README.md new file mode 100644 index 0000000..99df0cd --- /dev/null +++ b/eta_prediction/feature_engineering/README.md @@ -0,0 +1,131 @@ + +# Feature Engineering Module + +Transforms GTFS Schedule + Realtime telemetry into model-ready ETA features. The package is structured so every feature family (temporal, spatial, weather) can be invoked on its own or orchestrated via the dataset builder. + +--- + +## 1. Architecture & Data Flow + +1. **Ingest** – Pull raw `VehiclePosition`, `Trip`, `StopTime`, and `Stop` records from the Django ORM that fronts the GTFS replicas (`dataset_builder.py`). +2. **Trip Stitching** – Join telemetry rows with trip metadata (route, direction, headsign) and ordered stop sequences so every position knows which stops remain. +3. **Target Construction** – For each VehiclePosition, locate up to `max_stops_ahead` downstream stops, estimate distance with a haversine helper, and mine subsequent VehiclePositions to timestamp the actual arrival that forms the regression target. +4. **Label Validation** – Compute `time_to_arrival_seconds`, drop negative or >2h horizons, and standardize timestamps. +5. **Feature Enrichment** – Attach temporal, spatial, and weather tensors. Each step is isolated so failures yield NaNs without aborting the run. +6. **Finalize** – Select the canonical column set, fill missing headers with NaNs, and return a tidy DataFrame ready for persistence. `save_dataset` writes Snappy-compressed Parquet when needed. + +``` +VehiclePosition ➜ Trip metadata ➜ Stops in sequence ➜ Distance/target labeling + ➜ {Temporal | Spatial | Weather} features ➜ Training frame +``` + +--- + +## 2. Dataset Builder (`dataset_builder.py`) + +**Signature:** +`build_vp_training_dataset(route_ids=None, start_date=None, end_date=None, distance_threshold=50.0, max_stops_ahead=5, attach_weather=True, tz_for_temporal="America/Costa_Rica", pg_conn=None) -> pd.DataFrame` + +**Inputs** + +- `route_ids` – subset of GTFS routes; omit for all. +- `start_date` / `end_date` – UTC datetimes bounding telemetry ingestion. +- `distance_threshold` – meters for “arrived” when mining actual arrival. +- `max_stops_ahead` – number of downstream stops to label per VehiclePosition. +- `attach_weather` – fetch Open‑Meteo hourly observations per VP position. +- `tz_for_temporal` – timezone used by temporal bins/peaks. +- `pg_conn` – optional psycopg connection used for shape loading; defaults to the Django connection when omitted. + +**Outputs** + +Each row represents a `(VehiclePosition, downstream stop)` pair with: + +| Category | Columns | +| --- | --- | +| Identity | `trip_id`, `route_id`, `vehicle_id`, `stop_id`, `stop_sequence` | +| Telemetry | `vp_ts`, `vp_lat`, `vp_lon`, `vp_bearing`, `vp_speed` | +| Geometry | `stop_lat`, `stop_lon`, `distance_to_stop`, `progress_on_segment`, `progress_ratio` | +| Targets | `actual_arrival`, `time_to_arrival_seconds` | +| Temporal | `hour`, `day_of_week`, `is_weekend`, `is_holiday`, `is_peak_hour` | +| Weather | `temperature_c`, `precipitation_mm`, `wind_speed_kmh` | + +`progress_ratio` approximates how far along the route the vehicle is by combining the ordinal of the closest stop with its progress within the current stop-to-stop segment. + +**Helper routines** + +- `haversine_distance` computes great-circle distances using a 6371 km Earth radius. +- `find_actual_arrival_time` scans future VehiclePositions for the first observation within `distance_threshold` meters (falls back to closest approach within 200 m). + +**Usage Example** + +```python +from datetime import datetime, timezone +from feature_engineering.dataset_builder import build_vp_training_dataset, save_dataset + +df = build_vp_training_dataset( + route_ids=["Green-B"], + start_date=datetime(2023, 10, 1, tzinfo=timezone.utc), + end_date=datetime(2023, 10, 2, tzinfo=timezone.utc), + max_stops_ahead=3, +) +save_dataset(df, "datasets/green_line_oct01.parquet") +``` + +Run inside the Django project context so ORM models resolve and DB settings are loaded. + +--- + +## 3. Temporal Features (`temporal.py`) + +`extract_temporal_features(timestamp, tz="America/New_York", region="US_MA") -> Dict[str, object>` + +- Naive timestamps are assumed UTC, converted to the requested tz via `zoneinfo`. +- Peak-hour logic (weekdays 07–09 & 16–19) complements coarse bins (`_tod_bin`) defined as morning/midday/afternoon/evening. +- Holiday detection relies on `holidays` with regional fallbacks; absence of the package silently downgrades to `False`. + +Unit tests validate timezone handling, binning, weekend overrides, and holiday stubs (`tests/test_temporal.py`). + +--- + +## 4. Spatial Utilities (`spatial.py`) + +- `calculate_distance_features(vehicle_position, stop, next_stop)` returns distances from vehicle to current/next stop, bearing alignment, and a normalized `progress_on_segment` proxy. +- `get_route_features(route_id, conn=None, stops_in_order=None)` aggregates route length and average stop spacing either via Postgres (`sch_pipeline_routestop`/`sch_pipeline_stop`) or an in-memory stop list. +- `distance_postgis_m` exposes `ST_DistanceSphere` for higher-precision metrics when PostGIS is available. + +Use these helpers when deriving spatial covariates outside the dataset builder. + +--- + +## 5. Weather Adapter (`weather.py`) + +`fetch_weather(lat, lon, timestamp) -> dict` pulls hourly Open‑Meteo aggregates aligned to the VehiclePosition timestamp. + +- Timestamps are normalized to the hour in UTC, forming the cache key `weather:{lat}:{lon}:{iso_hour}` stored in the configured Django cache for 1 hour. +- Only the relevant hour is requested via `start`/`end` query params to minimize payloads; API timeouts default to 8 seconds. +- Missing API data or coverage gaps yield a dict of `None`s, keeping the dataset builder resilient. + +--- + +## 6. Dependencies & Environment + +- **Python stack:** pandas, numpy, psycopg, requests, django, holidays (optional). +- **Database:** Postgres schemas `rt_pipeline_*` and `sch_pipeline_*` accessed either through the Django ORM or raw psycopg connections. Ensure indexes on `VehiclePosition(ts, route_id)` and `TripUpdate(route_id, stop_sequence, ts)` for performant scans. +- **Caching:** Uses the configured Django cache backend for weather responses. +- **Timezone & locale:** Default tz is `America/Costa_Rica` in the dataset builder (override via `tz_for_temporal`), but temporal helpers accept any IANA tz string. + +--- + +## 7. Testing & Validation + +Automated tests cover: +- Temporal calculations (`tests/test_temporal.py`) +- Spatial distances/progress (`tests/test_spatial.py`) +- Weather fetching/memoization (`tests/test_weather.py`) + +To perform tests, from the `eta_prediction` directory, run: +```bash +$ uv run pytest feature_engineering +``` +--- + diff --git a/eta_prediction/feature_engineering/__init__.py b/eta_prediction/feature_engineering/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eta_prediction/feature_engineering/dataset_builder.py b/eta_prediction/feature_engineering/dataset_builder.py new file mode 100644 index 0000000..dc1efd5 --- /dev/null +++ b/eta_prediction/feature_engineering/dataset_builder.py @@ -0,0 +1,562 @@ +from __future__ import annotations + +from datetime import datetime, date, time, timedelta +from typing import Optional, List, Any, Dict +import numpy as np +import pandas as pd +from math import radians, cos, sin, asin, sqrt +from pathlib import Path + +from django.db import connection as django_connection +from django.db.models import F, Q +from sch_pipeline.models import StopTime, Stop, Trip +from rt_pipeline.models import VehiclePosition +from feature_engineering.temporal import extract_temporal_features +from feature_engineering.spatial import calculate_distance_features_with_shape, load_shape_for_trip +from feature_engineering.weather import fetch_weather + + +def haversine_distance(lat1, lon1, lat2, lon2): + """ + Calculate the great circle distance in meters between two points + on the earth (specified in decimal degrees) + """ + # Convert decimal degrees to radians + lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) + + # Haversine formula + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 + c = 2 * asin(sqrt(a)) + r = 6371000 # Radius of earth in meters + return c * r + + +def find_actual_arrival_time(vp_df_for_trip, stop_lat, stop_lon, distance_threshold=50): + """ + Find the ts when a vehicle arrived at a stop. + + Strategy: + 1. Calculate distance from each VP to the stop + 2. Find the first VP within distance_threshold meters + 3. Return its ts as arrival time + + Args: + vp_df_for_trip: DataFrame of VehiclePositions for a specific trip, sorted by ts + stop_lat: Stop lat + stop_lon: Stop lon + distance_threshold: Distance in meters to consider "arrived" (default 50m) + + Returns: + datetime or None if vehicle never arrived + """ + if vp_df_for_trip.empty: + return None + + # Calculate distances to stop + distances = vp_df_for_trip.apply( + lambda row: haversine_distance(row['lat'], row['lon'], stop_lat, stop_lon), + axis=1 + ) + + # Find first position within threshold + arrived_mask = distances <= distance_threshold + if arrived_mask.any(): + first_arrival_idx = arrived_mask.idxmax() # First True index + return vp_df_for_trip.loc[first_arrival_idx, 'ts'] + + # Alternative: If never within threshold, find closest approach + closest_idx = distances.idxmin() + min_distance = distances.min() + + # Only use closest approach if reasonably close (e.g., within 200m) + if min_distance <= 200: + return vp_df_for_trip.loc[closest_idx, 'ts'] + + return None + + +def build_vp_training_dataset( + route_ids: Optional[List[str]] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + distance_threshold: float = 50.0, + max_stops_ahead: int = 5, + attach_weather: bool = True, + use_shapes: bool = True, + tz_for_temporal: str = "America/New_York", + pg_conn: Optional[Any] = None, +) -> pd.DataFrame: + """ + Build training dataset from VehiclePosition data only. + + For each VehiclePosition: + - Identify current trip and route + - Find remaining stops in trip + - Calculate distance to each remaining stop (up to max_stops_ahead) + - Find actual arrival times from subsequent VehiclePositions + - Extract temporal, operational, and weather features + + Args: + route_ids: List of route IDs to include (None = all routes) + start_date: Start of date range + end_date: End of date range + distance_threshold: Distance in meters to consider "arrived at stop" + max_stops_ahead: Maximum number of future stops to include per VP + attach_weather: Whether to fetch weather data + use_shapes: Whether to use GTFS shapes for accurate progress calculation + tz_for_temporal: Timezone for temporal features + pg_conn: PostgreSQL connection for shape loading (defaults to Django connection) + + Returns: + DataFrame with columns: + - trip_id, route_id, vehicle_id + - vp_ts, vp_lat, vp_lon + - stop_id, stop_sequence, stop_lat, stop_lon + - distance_to_stop (meters), progress_on_segment, progress_ratio + - scheduled_arrival, actual_arrival, delay_seconds + - temporal features (hour, day_of_week, etc.) + - operational features (headway, congestion proxies) + - weather features (temperature, precipitation, wind_speed) + """ + + print("=" * 70) + print("VP-BASED DATASET BUILDER") + if use_shapes: + print(" Shape-informed progress: ENABLED") + print("=" * 70) + + # Setup database connection for shape loading + if use_shapes and pg_conn is None: + try: + django_connection.ensure_connection() + pg_conn = django_connection.connection + print(" Using Django database connection for shape loading") + except Exception as exc: + print(f" WARNING: Could not establish DB connection ({exc})") + print(" Shape features will be disabled") + use_shapes = False + + # ============================================================ + # STEP 1: Fetch VehiclePositions + # ============================================================ + print("\nStep 1: Fetching VehiclePositions...") + + vp_qs = VehiclePosition.objects.exclude( + Q(trip_id__isnull=True) | + Q(lat__isnull=True) | + Q(lon__isnull=True) + ) + + if start_date: + print(f" Filtering start_date >= {start_date}") + vp_qs = vp_qs.filter(ts__gte=start_date) + + if end_date: + print(f" Filtering end_date < {end_date}") + vp_qs = vp_qs.filter(ts__lt=end_date) + + # Filter by route if specified + if route_ids: + print(f" Filtering for routes: {route_ids}") + trip_ids_for_routes = list( + Trip.objects.filter(route_id__in=route_ids) + .values_list("trip_id", flat=True) + ) + vp_qs = vp_qs.filter(trip_id__in=trip_ids_for_routes) + + print(f" Fetching VehiclePosition records...") + vp_data = vp_qs.values( + 'trip_id', + 'vehicle_id', + 'ts', + 'lat', + 'lon', + 'bearing', + 'speed', + ).order_by('trip_id', 'ts') + + vp_df = pd.DataFrame.from_records(vp_data) + print(f" Retrieved {len(vp_df):,} VehiclePosition records") + + if vp_df.empty: + print("\nWARNING: No VehiclePosition data found!") + return pd.DataFrame() + + # ============================================================ + # STEP 2: Get trip metadata + # ============================================================ + print("\nStep 2: Getting trip metadata...") + + trip_ids = vp_df['trip_id'].unique() + trips_data = Trip.objects.filter(trip_id__in=trip_ids).values( + 'trip_id', 'route_id', 'direction_id', 'trip_headsign', 'service_id' + ) + trips_df = pd.DataFrame.from_records(trips_data) + + vp_df = vp_df.merge(trips_df, on='trip_id', how='left') + print(f" Added route info for {len(vp_df):,} records") + + # Drop VPs without route info + initial_count = len(vp_df) + vp_df = vp_df.dropna(subset=['route_id']) + print(f" Dropped {initial_count - len(vp_df):,} VPs without route info") + + # ============================================================ + # STEP 2b: Load shapes for trips (if enabled) + # ============================================================ + shape_cache: Dict[str, Any] = {} + + if use_shapes: + print("\nStep 2b: Loading GTFS shapes for trips...") + unique_trip_ids = vp_df['trip_id'].unique() + loaded_count = 0 + failed_count = 0 + + for trip_id in unique_trip_ids: + try: + shape = load_shape_for_trip(trip_id, pg_conn) + if shape is not None: + shape_cache[trip_id] = shape + loaded_count += 1 + except Exception as exc: + # Silently skip trips without shapes + failed_count += 1 + continue + + print(f" Loaded shapes for {loaded_count:,} trips") + if failed_count > 0: + print(f" Skipped {failed_count:,} trips without shapes") + + # ============================================================ + # STEP 3: Get stop sequences for each trip + # ============================================================ + print("\nStep 3: Loading stop sequences for trips...") + + stoptimes_data = StopTime.objects.filter( + trip_id__in=trip_ids + ).values( + 'trip_id', + 'stop_sequence', + 'stop_id', + 'arrival_time', + ).order_by('trip_id', 'stop_sequence') + + st_df = pd.DataFrame.from_records(stoptimes_data) + print(f" Retrieved {len(st_df):,} StopTime records") + + if st_df.empty: + print("\nWARNING: No StopTime data found!") + return pd.DataFrame() + + # Get stop coordinates + stop_ids = st_df['stop_id'].unique() + # remove any NaN / None values and coerce to STRING + stop_ids = [str(s) for s in stop_ids if pd.notna(s)] + + stops_data = Stop.objects.filter(stop_id__in=stop_ids).values( + 'stop_id', 'stop_lat', 'stop_lon', 'stop_name' + ) + stops_df = pd.DataFrame.from_records(stops_data) + + st_df = st_df.merge(stops_df, on='stop_id', how='left') + print(f" Added coordinates for {st_df['stop_lat'].notna().sum():,} stops") + + # ============================================================ + # STEP 4: For each VP, find remaining stops and distances + # ============================================================ + print(f"\nStep 4: Calculating distances to remaining stops (max {max_stops_ahead})...") + + training_rows = [] + total_vps = len(vp_df) + + # Group VPs by trip for efficient processing + vp_grouped = vp_df.groupby('trip_id') + st_grouped = st_df.groupby('trip_id') + + processed_trips = 0 + for trip_id, trip_vps in vp_grouped: + processed_trips += 1 + if processed_trips % 100 == 0: + print(f" Processing trip {processed_trips}/{len(vp_grouped)} ({len(training_rows):,} rows generated)") + + # Get stop sequence for this trip + if trip_id not in st_grouped.groups: + continue + + trip_stops = ( + st_grouped.get_group(trip_id) + .sort_values('stop_sequence') + .reset_index(drop=True) + ) + trip_stops['stop_order'] = trip_stops.index + trip_stops['next_stop_id'] = trip_stops['stop_id'].shift(-1) + trip_stops['next_stop_lat'] = trip_stops['stop_lat'].shift(-1) + trip_stops['next_stop_lon'] = trip_stops['stop_lon'].shift(-1) + trip_total_segments = max(len(trip_stops) - 1, 1) + + # Get shape for this trip (if available) + trip_shape = shape_cache.get(trip_id) + + # For each VP in this trip + for vp_idx, vp_row in trip_vps.iterrows(): + vp_lat = vp_row['lat'] + vp_lon = vp_row['lon'] + vp_ts = vp_row['ts'] + vp_position = { + 'lat': float(vp_lat), + 'lon': float(vp_lon), + 'bearing': vp_row.get('bearing'), + } + + # Find which stops are ahead of this VP + # Strategy: Calculate distance to all stops, take the closest N + distances_to_stops = trip_stops.apply( + lambda stop: haversine_distance(vp_lat, vp_lon, stop['stop_lat'], stop['stop_lon']), + axis=1 + ) + + trip_stops_with_dist = trip_stops.copy() + trip_stops_with_dist['distance_to_stop'] = distances_to_stops + + # Sort by stop_sequence and take upcoming stops + # A stop is "upcoming" if it hasn't been passed yet + # Simple heuristic: stops that are ahead in sequence from the closest stop + closest_stop_idx = distances_to_stops.idxmin() + closest_stop_seq = trip_stops.loc[closest_stop_idx, 'stop_sequence'] + closest_stop_order = trip_stops.loc[closest_stop_idx, 'stop_order'] + + # Get stops with sequence >= closest (upcoming stops) + upcoming_stops = trip_stops_with_dist[ + trip_stops_with_dist['stop_sequence'] >= closest_stop_seq + ].head(max_stops_ahead) + + # For each upcoming stop, find actual arrival time + for stop_idx, stop_row in upcoming_stops.iterrows(): + # Find actual arrival from future VPs + future_vps = trip_vps[trip_vps['ts'] > vp_ts] + + stop_payload = { + 'stop_id': stop_row['stop_id'], + 'lat': stop_row['stop_lat'], + 'lon': stop_row['stop_lon'], + 'stop_order': stop_row.get('stop_order'), + 'total_segments': trip_total_segments, + } + next_stop_payload = None + if pd.notna(stop_row['next_stop_id']): + next_stop_payload = { + 'stop_id': stop_row['next_stop_id'], + 'lat': stop_row['next_stop_lat'], + 'lon': stop_row['next_stop_lon'], + } + + # Calculate spatial features (with shape if available) + spatial_feats = calculate_distance_features_with_shape( + vp_position, + stop_payload, + next_stop_payload, + shape=trip_shape if use_shapes else None, + vehicle_stop_order=int(closest_stop_order) if pd.notna(closest_stop_order) else None, + total_segments=trip_total_segments + ) + + actual_arrival = find_actual_arrival_time( + future_vps, + stop_row['stop_lat'], + stop_row['stop_lon'], + distance_threshold=distance_threshold + ) + + # Only include if we found an actual arrival + if actual_arrival is not None: + row_data = { + 'trip_id': trip_id, + 'route_id': vp_row['route_id'], + 'vehicle_id': vp_row['vehicle_id'], + 'vp_ts': vp_ts, + 'vp_lat': vp_lat, + 'vp_lon': vp_lon, + 'vp_bearing': vp_row.get('bearing'), + 'vp_speed': vp_row.get('speed'), + 'stop_id': stop_row['stop_id'], + 'stop_sequence': stop_row['stop_sequence'], + 'stop_lat': stop_row['stop_lat'], + 'stop_lon': stop_row['stop_lon'], + 'progress_on_segment': spatial_feats.get('progress_on_segment'), + 'progress_ratio': spatial_feats.get('progress_ratio'), + 'distance_to_stop': spatial_feats.get('distance_to_stop'), + 'scheduled_arrival': stop_row['arrival_time'], + 'actual_arrival': actual_arrival, + } + + # Add shape-specific features if available + if use_shapes and trip_shape is not None: + row_data.update({ + 'distance_to_stop': spatial_feats.get('shape_distance_to_stop'), + 'cross_track_error': spatial_feats.get('cross_track_error'), + }) + + training_rows.append(row_data) + + print(f" Generated {len(training_rows):,} training samples") + + if not training_rows: + print("\nWARNING: No training rows generated!") + return pd.DataFrame() + + df = pd.DataFrame(training_rows) + + # ============================================================ + # STEP 5: Compute time_to_arrival_seconds + # ============================================================ + print("\nStep 5: Computing time_to_arrival_seconds...") + + # Convert timestamps to datetime + df['vp_ts'] = pd.to_datetime(df['vp_ts'], utc=True) + df['actual_arrival'] = pd.to_datetime(df['actual_arrival'], utc=True) + + # Compute time to arrival (prediction target) + df['time_to_arrival_seconds'] = ( + df['actual_arrival'] - df['vp_ts'] + ).dt.total_seconds() + + # Filter out negative or unrealistic values + initial_count = len(df) + df = df[ + (df['time_to_arrival_seconds'] >= 0) & + (df['time_to_arrival_seconds'] <= 7200) # Max 2 hours ahead + ] + print(f" Dropped {initial_count - len(df):,} rows with invalid time_to_arrival") + print(f" Remaining: {len(df):,} rows") + + # ============================================================ + # STEP 6: Temporal features + # ============================================================ + print("\nStep 6: Extracting temporal features...") + + temporal_data = [] + for idx, ts in enumerate(df['vp_ts']): + if idx % 10000 == 0 and idx > 0: + print(f" Processed {idx:,}/{len(df):,} temporal features") + + try: + feats = extract_temporal_features(ts, tz=tz_for_temporal, region="US_MA") + temporal_data.append(feats) + except Exception: + temporal_data.append({}) + + tf = pd.DataFrame(temporal_data) + keep_temporal = ["hour", "day_of_week", "is_weekend", "is_holiday", "is_peak_hour"] + for k in keep_temporal: + if k in tf.columns: + df[k] = tf[k].values + else: + df[k] = np.nan + + # ============================================================ + # STEP 7: Operational features + # ============================================================ + # Commented out for now - uncomment if you want operational features + # print("\nStep 7: Computing operational features...") + # ... (operational feature code) + + # Use VP speed directly if available + if 'vp_speed' in df.columns: + df['current_speed_kmh'] = df['vp_speed'] * 3.6 # m/s to km/h + else: + df['current_speed_kmh'] = np.nan + + # ============================================================ + # STEP 8: Weather features + # ============================================================ + def _get_weather_for_row(row): + """Wraps fetch_weather with error handling for use in df.apply.""" + try: + w = fetch_weather( + float(row['vp_lat']), + float(row['vp_lon']), + row['vp_ts'].to_pydatetime() + ) + return w or {} + except Exception: + return {} + + if attach_weather: + print("\nStep 8: Fetching weather data...") + + weather_series = df.apply(_get_weather_for_row, axis=1) + wx_df = pd.json_normalize(weather_series) + + target_keys = ["temperature_c", "precipitation_mm", "wind_speed_kmh"] + for k in target_keys: + if k in wx_df.columns: + df[k] = wx_df[k].values + else: + df[k] = np.nan + + print(f" Finished fetching weather for {len(df):,} rows.") + else: + df["temperature_c"] = np.nan + df["precipitation_mm"] = np.nan + df["wind_speed_kmh"] = np.nan + + # ============================================================ + # STEP 9: Final column selection + # ============================================================ + print("\nStep 9: Preparing final dataset...") + + wanted_cols = [ + "trip_id", "route_id", "vehicle_id", "stop_id", "stop_sequence", + "vp_ts", "vp_lat", "vp_lon", "vp_bearing", + "stop_lat", "stop_lon", + "distance_to_stop", "progress_on_segment", "progress_ratio", + "actual_arrival", "time_to_arrival_seconds", + "hour", "day_of_week", "is_weekend", "is_holiday", "is_peak_hour", + "current_speed_kmh", + "temperature_c", "precipitation_mm", "wind_speed_kmh", + ] + + # Add shape features if they were computed + # if use_shapes: + # wanted_cols.extend([ + # "shape_progress", + # "shape_distance_to_stop", + # "cross_track_error", + # ]) + + for c in wanted_cols: + if c not in df.columns: + df[c] = np.nan + + result = df[wanted_cols].reset_index(drop=True) + + print("\n" + "=" * 70) + print(f"✓ Final dataset: {len(result):,} rows × {len(wanted_cols)} columns") + print(f" Unique trips: {result['trip_id'].nunique():,}") + print(f" Unique routes: {result['route_id'].nunique():,}") + print(f" Unique stops: {result['stop_id'].nunique():,}") + print(f" Avg time_to_arrival: {result['time_to_arrival_seconds'].mean():.1f}s ({result['time_to_arrival_seconds'].mean()/60:.1f} min)") + + # if use_shapes: + # shape_coverage = result['shape_progress'].notna().sum() / len(result) * 100 + # print(f" Shape coverage: {shape_coverage}%") + # if shape_coverage > 0: + # print(f" Avg cross-track error: {result['cross_track_error'].mean():.1f}m") + + print("=" * 70) + + return result + + +def save_dataset(df: pd.DataFrame, output_path: str): + """Save to parquet with compression, creating directories if needed.""" + output_path = Path(output_path) + + # Create parent directory if it doesn't exist + output_path.parent.mkdir(parents=True, exist_ok=True) + + print(f"\nSaving to {output_path}...") + df.to_parquet(output_path, compression="snappy", index=False) + print(f"✓ Saved {len(df):,} rows") diff --git a/eta_prediction/feature_engineering/pyproject.toml b/eta_prediction/feature_engineering/pyproject.toml new file mode 100644 index 0000000..8e1026d --- /dev/null +++ b/eta_prediction/feature_engineering/pyproject.toml @@ -0,0 +1,35 @@ +[project] +name = "feature_engineering" +version = "0.1.0" +description = "Feature engineering utilities for GTFS realtime and schedule data (part of gtfs-django repository)" +readme = "README.md" +requires-python = ">=3.10" +authors = [ + { name = "maintainer", email = "maintainer@example.com" } +] +license = { text = "MIT" } + +dependencies = [ + "numpy>=1.26,<2", + "pandas>=2.2,<3", + "scikit-learn>=1.3,<2", + "scipy>=1.11,<2", + "joblib>=1.3,<2", + "tqdm>=4.66,<5", + "pyproj>=3.6,<4", + "shapely>=2.1,<3", + "geopandas>=0.14,<1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4,<8", + "black>=24.3,<25", + "isort>=5.13,<6", + "mypy>=1.11,<2", + "pre-commit>=3.4,<4", +] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/eta_prediction/feature_engineering/spatial.py b/eta_prediction/feature_engineering/spatial.py new file mode 100644 index 0000000..1149ae2 --- /dev/null +++ b/eta_prediction/feature_engineering/spatial.py @@ -0,0 +1,372 @@ +# feature_engineering/spatial.py - Shape-informed progress extension +from __future__ import annotations +import math +from typing import Dict, List, Tuple, Optional + +EARTH_RADIUS_M = 6_371_000.0 + +def _deg2rad(x: float) -> float: + return x * math.pi / 180.0 + +def _haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """Great-circle distance in meters.""" + φ1, φ2 = _deg2rad(lat1), _deg2rad(lat2) + dφ = φ2 - φ1 + dλ = _deg2rad(lon2 - lon1) + a = math.sin(dφ / 2) ** 2 + math.cos(φ1) * math.cos(φ2) * math.sin(dλ / 2) ** 2 + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + return EARTH_RADIUS_M * c + + +class ShapePolyline: + """ + Represents a route shape as an ordered sequence of (lat, lon) points. + Provides methods to project vehicle positions onto the polyline and compute + accurate progress along the route. + """ + + def __init__(self, points: List[Tuple[float, float]]): + """ + Args: + points: List of (lat, lon) tuples in route order + """ + if len(points) < 2: + raise ValueError("Shape must have at least 2 points") + + self.points = points + self._segment_lengths = self._compute_segment_lengths() + self._cumulative_distances = self._compute_cumulative_distances() + self.total_length = self._cumulative_distances[-1] + + def _compute_segment_lengths(self) -> List[float]: + """Compute distance for each segment between consecutive points.""" + lengths = [] + for i in range(len(self.points) - 1): + lat1, lon1 = self.points[i] + lat2, lon2 = self.points[i + 1] + lengths.append(_haversine_m(lat1, lon1, lat2, lon2)) + return lengths + + def _compute_cumulative_distances(self) -> List[float]: + """Compute cumulative distance from start to each point.""" + cumulative = [0.0] + for length in self._segment_lengths: + cumulative.append(cumulative[-1] + length) + return cumulative + + def project_point(self, lat: float, lon: float) -> Dict: + """ + Project a point onto the polyline, finding the closest position. + + Args: + lat, lon: Point to project + + Returns: + { + 'distance_along_shape': meters from shape start, + 'cross_track_distance': perpendicular distance from shape (meters), + 'closest_segment_idx': index of nearest segment, + 'progress': normalized progress [0, 1] + } + """ + min_dist = float('inf') + best_segment_idx = 0 + best_projection_dist = 0.0 + + # Check each segment + for i in range(len(self.points) - 1): + lat1, lon1 = self.points[i] + lat2, lon2 = self.points[i + 1] + + # Project point onto this segment + proj_info = self._project_onto_segment( + lat, lon, lat1, lon1, lat2, lon2 + ) + + if proj_info['distance'] < min_dist: + min_dist = proj_info['distance'] + best_segment_idx = i + best_projection_dist = proj_info['distance_along_segment'] + + # Calculate total distance along shape to projection point + distance_along_shape = ( + self._cumulative_distances[best_segment_idx] + best_projection_dist + ) + + # Normalized progress + progress = distance_along_shape / self.total_length if self.total_length > 0 else 0.0 + + return { + 'distance_along_shape': distance_along_shape, + 'cross_track_distance': min_dist, + 'closest_segment_idx': best_segment_idx, + 'progress': min(1.0, max(0.0, progress)) + } + + def _project_onto_segment( + self, + lat: float, lon: float, + lat1: float, lon1: float, + lat2: float, lon2: float + ) -> Dict: + """ + Project point onto a single segment, finding perpendicular distance + and position along segment. + + Uses simplified planar approximation (accurate for segments < 10km). + """ + # Convert to approximate planar coordinates (meters from segment start) + # This is accurate enough for typical transit segment lengths + avg_lat = (lat1 + lat2) / 2 + meters_per_deg_lat = 111320.0 + meters_per_deg_lon = 111320.0 * math.cos(_deg2rad(avg_lat)) + + # Segment vector in meters + seg_x = (lon2 - lon1) * meters_per_deg_lon + seg_y = (lat2 - lat1) * meters_per_deg_lat + seg_length_sq = seg_x**2 + seg_y**2 + + if seg_length_sq < 1e-6: # Degenerate segment + dist = _haversine_m(lat, lon, lat1, lon1) + return {'distance': dist, 'distance_along_segment': 0.0} + + # Vector from segment start to point + dx = (lon - lon1) * meters_per_deg_lon + dy = (lat - lat1) * meters_per_deg_lat + + # Project onto segment: dot product / length^2 + t = (dx * seg_x + dy * seg_y) / seg_length_sq + t = max(0.0, min(1.0, t)) # Clamp to segment + + # Closest point on segment + proj_x = lon1 + t * (lon2 - lon1) + proj_y = lat1 + t * (lat2 - lat1) + + # Distance from point to projection + dist = _haversine_m(lat, lon, proj_y, proj_x) + + # Distance along segment to projection + seg_length = math.sqrt(seg_length_sq) + distance_along_segment = t * seg_length + + return { + 'distance': dist, + 'distance_along_segment': distance_along_segment + } + + def get_distance_between_stops( + self, + stop1_lat: float, + stop1_lon: float, + stop2_lat: float, + stop2_lon: float + ) -> float: + """ + Get shape distance between two stops (more accurate than haversine). + """ + proj1 = self.project_point(stop1_lat, stop1_lon) + proj2 = self.project_point(stop2_lat, stop2_lon) + return abs(proj2['distance_along_shape'] - proj1['distance_along_shape']) + + +def calculate_distance_features_with_shape( + vehicle_position: Dict, + stop: Dict, + next_stop: Optional[Dict], + shape: Optional[ShapePolyline] = None, + vehicle_stop_order: Optional[int] = None, + total_segments: Optional[int] = None, +) -> Dict: + """ + Enhanced version that uses shape data when available. + + Args: + vehicle_position: {'lat': float, 'lon': float, 'bearing': Optional[float]} + stop: {'stop_id': str, 'lat': float, 'lon': float, 'stop_order': Optional[int]} + next_stop: {'stop_id': str, 'lat': float, 'lon': float} or None + shape: ShapePolyline instance or None + vehicle_stop_order: 0-based index of the closest upstream stop (optional) + total_segments: Total number of stop-to-stop segments in trip (optional) + + Returns: + Same as calculate_distance_features() but with additional shape-based fields: + - 'shape_progress': accurate progress along route shape [0, 1] + - 'shape_distance_to_stop': along-shape distance to stop (meters) + - 'cross_track_error': perpendicular distance from route (meters) + - 'progress_ratio': coarse fallback progress when shape is missing + """ + vlat, vlon = float(vehicle_position["lat"]), float(vehicle_position["lon"]) + slat, slon = float(stop["lat"]), float(stop["lon"]) + + # Base features (always computed) + result = { + 'distance_to_stop': _haversine_m(vlat, vlon, slat, slon), + 'distance_to_next_stop': None, + 'progress_on_segment': None, + 'progress_ratio': None, + 'shape_progress': None, + 'shape_distance_to_stop': None, + 'cross_track_error': None, + } + + if next_stop is not None: + nlat, nlon = float(next_stop["lat"]), float(next_stop["lon"]) + seg_len = _haversine_m(slat, slon, nlat, nlon) + if seg_len == 0.0: + result['distance_to_next_stop'] = 0.0 + else: + result['distance_to_next_stop'] = _haversine_m(vlat, vlon, nlat, nlon) + + # Simple progress proxy if we have next stop but no shape + if result['progress_on_segment'] is None and next_stop is not None and result['distance_to_next_stop'] is not None: + nlat, nlon = float(next_stop["lat"]), float(next_stop["lon"]) + seg_len = _haversine_m(slat, slon, nlat, nlon) + if seg_len > 0: + progress = 1.0 - (result['distance_to_next_stop'] / seg_len) + result['progress_on_segment'] = max(0.0, min(1.0, progress)) + else: + result['progress_on_segment'] = 0.0 + + # Shape-based features (if shape available) + if shape is not None: + vehicle_proj = shape.project_point(vlat, vlon) + stop_proj = shape.project_point(slat, slon) + + # Distance along shape from vehicle to stop + shape_dist_to_stop = stop_proj['distance_along_shape'] - vehicle_proj['distance_along_shape'] + + result.update({ + 'shape_progress': vehicle_proj['progress'], + 'shape_distance_to_stop': max(0, shape_dist_to_stop), # Don't allow negative + 'cross_track_error': vehicle_proj['cross_track_distance'], + 'progress_ratio': vehicle_proj['progress'], + }) + + # If we have next_stop, compute segment progress along shape + if next_stop is not None: + next_proj = shape.project_point(nlat, nlon) + segment_length = next_proj['distance_along_shape'] - stop_proj['distance_along_shape'] + + if segment_length > 0: + # How far past current stop along shape + past_stop = vehicle_proj['distance_along_shape'] - stop_proj['distance_along_shape'] + result['progress_on_segment'] = max(0.0, min(1.0, past_stop / segment_length)) + else: + result['progress_on_segment'] = 0.0 + # Fallback progress_ratio using stop order metadata + if result['progress_ratio'] is None: + order = vehicle_stop_order + if order is None: + order = stop.get("vehicle_stop_order") or stop.get("stop_order") + segments = total_segments + if segments is None: + segments = stop.get("total_segments") + if order is not None and segments: + completed_segments = max(float(order), 0.0) + progress_within = result['progress_on_segment'] or 0.0 + denom = max(float(segments), 1.0) + ratio = (completed_segments + progress_within) / denom + result['progress_ratio'] = max(0.0, min(1.0, ratio)) + + return result + + +# ==================== Helper: Load shapes from GTFS ==================== + +def load_shape_from_gtfs(shape_id: str, conn) -> ShapePolyline: + """ + Load a shape polyline from GTFS shapes table. + + Args: + shape_id: GTFS shape_id + conn: Database connection (psycopg2/asyncpg) + + Returns: + ShapePolyline instance + """ + with conn.cursor() as cur: + cur.execute( + """ + SELECT shape_pt_lat, shape_pt_lon + FROM sch_pipeline_shape + WHERE shape_id = %s + ORDER BY shape_pt_sequence + """, + (shape_id,) + ) + rows = cur.fetchall() + + if not rows: + raise ValueError(f"No shape found for shape_id: {shape_id}") + + points = [(float(row[0]), float(row[1])) for row in rows] + return ShapePolyline(points) + + +def load_shape_for_trip(trip_id: str, conn) -> Optional[ShapePolyline]: + """ + Load shape for a trip, returns None if trip has no shape. + """ + with conn.cursor() as cur: + cur.execute( + """ + SELECT shape_id + FROM sch_pipeline_trip + WHERE trip_id = %s + """, + (trip_id,) + ) + row = cur.fetchone() + + if not row or not row[0]: + return None + + return load_shape_from_gtfs(row[0], conn) + + +# ==================== Usage Example ==================== + +def example_usage(): + """ + Example showing how to use shape-informed features in practice. + """ + # 1. Load shape once per trip (cache this!) + from psycopg2 import connect + conn = connect("postgresql://user:pass@localhost/gtfs") + + shape = load_shape_for_trip("trip_123", conn) + + # 2. For each vehicle position update + vehicle_position = { + 'lat': 42.3601, + 'lon': -71.0589, + 'bearing': 180.0 + } + + current_stop = { + 'stop_id': 'stop_A', + 'lat': 42.3598, + 'lon': -71.0592 + } + + next_stop = { + 'stop_id': 'stop_B', + 'lat': 42.3620, + 'lon': -71.0580 + } + + # 3. Get shape-informed features + features = calculate_distance_features_with_shape( + vehicle_position, + current_stop, + next_stop, + shape=shape + ) + + print(f"Shape progress: {features['shape_progress']:.2%}") + print(f"Distance to stop (along shape): {features['shape_distance_to_stop']:.0f}m") + print(f"Cross-track error: {features['cross_track_error']:.1f}m") + print(f"Segment progress: {features['progress_on_segment']:.2%}") + +if __name__ == "__main__": + example_usage() diff --git a/eta_prediction/feature_engineering/temporal.py b/eta_prediction/feature_engineering/temporal.py new file mode 100644 index 0000000..5b32a58 --- /dev/null +++ b/eta_prediction/feature_engineering/temporal.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Dict, Optional + +try: + import zoneinfo # py3.9+ +except ImportError: # pragma: no cover + from backports import zoneinfo # type: ignore + +def _get_holiday_calendar(region: str): + """ + Try to build a holiday calendar. Falls back to empty set if 'holidays' isn't installed. + region: + - 'US_MA' → U.S. w/ Massachusetts state holidays (good for MBTA) + - 'CR' → Costa Rica + """ + try: + import holidays + except Exception: + return None + + if region.upper() == "US_MA": + return holidays.US(state="MA") + if region.upper() == "CR": + # Requires holidays>=0.52 which includes CostaRica + try: + return holidays.CostaRica() + except Exception: + return None + # Fallback: US federal only + return holidays.US() + +def _to_local(dt: datetime, tz: str) -> datetime: + """Ensure timezone-aware datetime localized to tz.""" + tzinfo = zoneinfo.ZoneInfo(tz) + if dt.tzinfo is None: + # assume input is UTC if naive (common for feeds); adjust if your code stores local + return dt.replace(tzinfo=zoneinfo.ZoneInfo("UTC")).astimezone(tzinfo) + return dt.astimezone(tzinfo) + +def _tod_bin(hour: int) -> str: + """ + Map hour→time-of-day bin. + Spec requires: 'morning' | 'midday' | 'afternoon' | 'evening'. + We bucket: + 05–09 → morning + 10–13 → midday + 14–17 → afternoon + 18–04 → evening (covers late night/overnight to keep labels strict) + """ + if 5 <= hour <= 9: + return "morning" + if 10 <= hour <= 13: + return "midday" + if 14 <= hour <= 17: + return "afternoon" + return "evening" + +def extract_temporal_features( + timestamp: datetime, + *, + tz: str = "America/New_York", # MBTA default + region: str = "US_MA", # MBTA default (US + Massachusetts) +) -> Dict[str, object]: + """ + Returns: + - hour: 0-23 + - day_of_week: 0-6 (Monday=0) + - is_weekend: bool + - is_holiday: bool (US-MA by default; set region='CR' for Costa Rica) + - time_of_day_bin: 'morning'|'midday'|'afternoon'|'evening' + - is_peak_hour: bool (7-9am, 4-7pm; weekdays only) + """ + dt_local = _to_local(timestamp, tz) + hour = dt_local.hour + dow = dt_local.weekday() # Monday=0 + is_weekend = dow >= 5 + + cal = _get_holiday_calendar(region) + # Holidays lib checks by date() + is_holiday = bool(cal and (dt_local.date() in cal)) + + # Peak windows (commuter assumption), weekdays only + is_peak_hour = (dow < 5) and ((7 <= hour <= 9) or (16 <= hour <= 19)) + + return { + "hour": hour, + "day_of_week": dow, + "is_weekend": is_weekend, + "is_holiday": is_holiday, + "time_of_day_bin": _tod_bin(hour), + "is_peak_hour": is_peak_hour, + } diff --git a/eta_prediction/feature_engineering/tests/__init__.py b/eta_prediction/feature_engineering/tests/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/eta_prediction/feature_engineering/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/eta_prediction/feature_engineering/tests/test_spatial.py b/eta_prediction/feature_engineering/tests/test_spatial.py new file mode 100644 index 0000000..cc2ee1d --- /dev/null +++ b/eta_prediction/feature_engineering/tests/test_spatial.py @@ -0,0 +1,124 @@ +# tests/test_spatial.py +import pytest + +from feature_engineering.spatial import ( + calculate_distance_features_with_shape, + ShapePolyline, +) + + +def approx_equal(a, b, tol): + return abs(a - b) <= tol + + +def test_basic_distance_and_progress_without_shape(): + vp = {"lat": 0.0, "lon": 0.0} + stop = {"stop_id": "A", "lat": 0.0, "lon": 1.0, "stop_order": 2} + next_stop = {"stop_id": "B", "lat": 0.0, "lon": 2.0} + + feats = calculate_distance_features_with_shape( + vp, + stop, + next_stop, + shape=None, + vehicle_stop_order=2, + total_segments=10, + ) + + assert approx_equal(feats["distance_to_stop"], 111_320, 600) + assert 0.0 <= feats["progress_on_segment"] <= 1.0 + assert 0.15 <= feats["progress_ratio"] <= 0.25 # 2/10 with some within segment progress + + +def test_progress_clamped_when_outside_segment(): + stop = {"stop_id": "A", "lat": 0.0, "lon": 0.0, "stop_order": 5} + next_stop = {"stop_id": "B", "lat": 0.0, "lon": 1.0} + + vp_before = {"lat": 0.0, "lon": -1.0} + feats_before = calculate_distance_features_with_shape( + vp_before, + stop, + next_stop, + shape=None, + vehicle_stop_order=5, + total_segments=8, + ) + assert feats_before["progress_on_segment"] == 0.0 + + vp_past = {"lat": 0.0, "lon": 3.0} + feats_past = calculate_distance_features_with_shape( + vp_past, + stop, + next_stop, + shape=None, + vehicle_stop_order=5, + total_segments=8, + ) + assert feats_past["progress_on_segment"] == 0.0 + + +def test_zero_length_segment_defaults(): + stop = {"stop_id": "S", "lat": 9.9, "lon": -84.0, "stop_order": 3} + next_stop = {"stop_id": "S", "lat": 9.9, "lon": -84.0} + vp = {"lat": 9.9, "lon": -84.001} + + feats = calculate_distance_features_with_shape( + vp, + stop, + next_stop, + shape=None, + vehicle_stop_order=3, + total_segments=5, + ) + + assert feats["progress_on_segment"] == 0.0 + assert feats["distance_to_next_stop"] == 0.0 + assert approx_equal(feats["progress_ratio"], 3 / 5, 0.01) + + +def test_shape_based_progress_used_when_available(): + shape = ShapePolyline([ + (0.0, 0.0), + (0.0, 1.0), + (0.0, 2.0), + ]) + vp = {"lat": 0.0, "lon": 0.5} + stop = {"stop_id": "B", "lat": 0.0, "lon": 1.0, "stop_order": 1} + next_stop = {"stop_id": "C", "lat": 0.0, "lon": 2.0} + + feats = calculate_distance_features_with_shape( + vp, + stop, + next_stop, + shape=shape, + vehicle_stop_order=1, + total_segments=2, + ) + + assert 0.24 <= feats["shape_progress"] <= 0.26 + assert feats["progress_ratio"] == pytest.approx(feats["shape_progress"]) + assert feats["shape_distance_to_stop"] > 0 + assert feats["cross_track_error"] < 50 + + +def test_shape_progress_overrides_progress_on_segment(): + shape = ShapePolyline([ + (0.0, 0.0), + (0.5, 0.5), + (1.0, 1.0), + ]) + vp = {"lat": 0.25, "lon": 0.25} + stop = {"stop_id": "B", "lat": 0.5, "lon": 0.5, "stop_order": 1} + next_stop = {"stop_id": "C", "lat": 1.0, "lon": 1.0} + + feats = calculate_distance_features_with_shape( + vp, + stop, + next_stop, + shape=shape, + vehicle_stop_order=1, + total_segments=2, + ) + + assert feats["progress_on_segment"] == pytest.approx(0.0) + assert 0.24 <= feats["shape_progress"] <= 0.26 diff --git a/eta_prediction/feature_engineering/tests/test_temporal.py b/eta_prediction/feature_engineering/tests/test_temporal.py new file mode 100644 index 0000000..6bda9fd --- /dev/null +++ b/eta_prediction/feature_engineering/tests/test_temporal.py @@ -0,0 +1,108 @@ +# feature_engineering/tests/test_temporal.py +from datetime import datetime, timezone, date +import pytest + +import feature_engineering.temporal as temporal + + +class DummyCalendar: + """Minimal calendar stub that supports `date in cal` checks.""" + def __init__(self, holiday_dates): + self._dates = set(holiday_dates) + + def __contains__(self, d): + return d in self._dates + + +def test_naive_timestamp_assumed_utc_and_converted_to_mbta_local(): + # Oct 8, 2025 12:30:00 UTC → America/New_York (EDT, UTC-4) = 08:30 + ts = datetime(2025, 10, 8, 12, 30, 0) # naive -> treated as UTC + feats = temporal.extract_temporal_features(ts, tz="America/New_York", region="US_MA") + assert feats["hour"] == 8 + assert feats["day_of_week"] == 2 # 0=Mon -> Wed=2 + assert feats["is_weekend"] is False + # 08:30 → morning bin per spec + assert feats["time_of_day_bin"] == "morning" + # Weekday 8am → peak + assert feats["is_peak_hour"] is True + + +def test_aware_utc_timestamp_converts_to_local_consistently(): + # Same instant as previous test, but explicitly aware in UTC + ts = datetime(2025, 10, 8, 12, 30, 0, tzinfo=timezone.utc) + feats = temporal.extract_temporal_features(ts, tz="America/New_York", region="US_MA") + assert feats["hour"] == 8 + assert feats["day_of_week"] == 2 + assert feats["is_peak_hour"] is True + +@pytest.mark.parametrize( + "hour,expected_bin", + [ + (5, "morning"), + (9, "morning"), + (10, "midday"), + (13, "midday"), + (14, "afternoon"), + (17, "afternoon"), + (18, "evening"), + (23, "evening"), + (0, "evening"), + (3, "evening"), + (4, "evening"), + ], +) + +def test_time_of_day_bins(hour, expected_bin): + # Build a timestamp that is ALREADY in the target local tz (America/New_York). + # That way, `hour` is the local hour we want to assert on. + ny_tz = temporal.zoneinfo.ZoneInfo("America/New_York") + local_ts = datetime(2025, 9, 30, hour, 0, 0, tzinfo=ny_tz) # Tuesday + feats = temporal.extract_temporal_features(local_ts, tz="America/New_York", region="US_MA") + assert feats["time_of_day_bin"] == expected_bin + +def test_peak_hours_windows_and_weekend_rule(): + # Weekday 17:00 local → peak + ts_local_peak = datetime(2025, 9, 30, 17, 0, 0, tzinfo=temporal.zoneinfo.ZoneInfo("America/New_York")) # Tue + feats = temporal.extract_temporal_features(ts_local_peak, tz="America/New_York", region="US_MA") + assert feats["is_peak_hour"] is True + + # Weekday 20:00 local → not peak + ts_local_offpeak = datetime(2025, 9, 30, 20, 0, 0, tzinfo=temporal.zoneinfo.ZoneInfo("America/New_York")) + feats2 = temporal.extract_temporal_features(ts_local_offpeak, tz="America/New_York", region="US_MA") + assert feats2["is_peak_hour"] is False + + # Weekend 08:00 local → not peak even though within 7–9 + ts_weekend = datetime(2025, 10, 4, 8, 0, 0, tzinfo=temporal.zoneinfo.ZoneInfo("America/New_York")) # Sat + feats3 = temporal.extract_temporal_features(ts_weekend, tz="America/New_York", region="US_MA") + assert feats3["day_of_week"] == 5 and feats3["is_weekend"] is True + assert feats3["is_peak_hour"] is False + + +def test_cr_timezone_conversion_and_weekday(): + # Costa Rica is UTC-6 all year (no DST). 14:00 UTC -> 08:00 local + ts_utc = datetime(2025, 8, 12, 14, 0, 0, tzinfo=timezone.utc) # Tue + feats = temporal.extract_temporal_features(ts_utc, tz="America/Costa_Rica", region="CR") + assert feats["hour"] == 8 + assert feats["day_of_week"] == 1 # Tuesday + assert feats["is_weekend"] is False + + +def test_holiday_true_via_monkeypatched_calendar(monkeypatch): + # Pick an arbitrary date and mark it as a holiday through a stub calendar + holiday_d = date(2025, 7, 4) + def fake_get_cal(region: str): + return DummyCalendar({holiday_d}) + + monkeypatch.setattr(temporal, "_get_holiday_calendar", fake_get_cal) + + ts_local = datetime(2025, 7, 4, 9, 0, 0, tzinfo=temporal.zoneinfo.ZoneInfo("America/New_York")) + feats = temporal.extract_temporal_features(ts_local, tz="America/New_York", region="US_MA") + assert feats["is_holiday"] is True + + +def test_holiday_false_when_calendar_missing(monkeypatch): + # Simulate absence/failure of holidays package: calendar returns None + monkeypatch.setattr(temporal, "_get_holiday_calendar", lambda region: None) + ts_local = datetime(2025, 7, 4, 9, 0, 0, tzinfo=temporal.zoneinfo.ZoneInfo("America/New_York")) + feats = temporal.extract_temporal_features(ts_local, tz="America/New_York", region="US_MA") + assert feats["is_holiday"] is False diff --git a/eta_prediction/feature_engineering/tests/test_weather.py b/eta_prediction/feature_engineering/tests/test_weather.py new file mode 100644 index 0000000..558eb03 --- /dev/null +++ b/eta_prediction/feature_engineering/tests/test_weather.py @@ -0,0 +1,192 @@ +from datetime import datetime, timezone, timedelta +from unittest.mock import Mock, patch +import pytest + +from feature_engineering.weather import fetch_weather, OPEN_METEO_URL + + +class FakeCache: + """Tiny in-memory cache with the subset of the Django cache API we use.""" + def __init__(self): + self._d = {} + def get(self, key, default=None): + return self._d.get(key, default) + def set(self, key, value, timeout=None): + self._d[key] = value + def clear(self): + self._d.clear() + + +def _mock_response(payload: dict, status_code: int = 200): + resp = Mock() + resp.status_code = status_code + resp.json = Mock(return_value=payload) + resp.raise_for_status = Mock() + if status_code >= 400: + resp.raise_for_status.side_effect = Exception(f"HTTP {status_code}") + return resp + + +@pytest.fixture() +def fake_cache(monkeypatch): + # Patch the cache object inside the weather module (so no Django needed) + from feature_engineering import weather + fc = FakeCache() + monkeypatch.setattr(weather, "cache", fc, raising=True) + return fc + + +def test_fetch_weather_success_exact_hour(fake_cache): + fake_cache.clear() + lat, lon = 9.935, -84.091 + ts = datetime(2025, 1, 1, 12, 34, tzinfo=timezone.utc) + target_str = ts.replace(minute=0, second=0, microsecond=0).strftime("%Y-%m-%dT%H:00") + + payload = { + "hourly": { + "time": [target_str], + "temperature_2m": [23.4], + "precipitation": [0.2], + "wind_speed_10m": [12.0], + "weather_code": [80], + "visibility": [9800.0], + } + } + + with patch("feature_engineering.weather.requests.get") as mget: + mget.return_value = _mock_response(payload) + out = fetch_weather(lat, lon, ts) + + assert out == { + "temperature_c": 23.4, + "precipitation_mm": 0.2, + "wind_speed_kmh": 12.0, + "weather_code": 80, + "visibility_m": 9800.0, + } + + assert mget.call_count == 1 + args, kwargs = mget.call_args + assert args[0] == OPEN_METEO_URL + params = kwargs["params"] + assert params["latitude"] == lat + assert params["longitude"] == lon + assert params["timezone"] == "UTC" + assert "temperature_2m" in params["hourly"] + assert "start" in params and "end" in params + + start_dt = datetime.fromisoformat(params["start"].replace("Z", "+00:00")) + end_dt = datetime.fromisoformat(params["end"].replace("Z", "+00:00")) + assert start_dt.strftime("%Y-%m-%dT%H:00") == target_str + assert end_dt - start_dt == timedelta(hours=1) + + +def test_fetch_weather_naive_timestamp_treated_as_utc(fake_cache): + fake_cache.clear() + lat, lon = 9.0, -84.0 + naive_ts = datetime(2025, 1, 1, 7, 15) # treated as UTC in implementation + target_str = naive_ts.replace(minute=0, second=0, microsecond=0).strftime("%Y-%m-%dT%H:00") + + payload = { + "hourly": { + "time": [target_str], + "temperature_2m": [30.0], + "precipitation": [0.0], + "wind_speed_10m": [5.0], + "weather_code": [0], + "visibility": [10000.0], + } + } + + with patch("feature_engineering.weather.requests.get") as mget: + mget.return_value = _mock_response(payload) + out = fetch_weather(lat, lon, naive_ts) + + assert out["temperature_c"] == 30.0 + assert mget.call_count == 1 + + +def test_fetch_weather_caches_result(fake_cache): + fake_cache.clear() + lat, lon = 10.0, -84.0 + ts = datetime(2025, 2, 2, 3, 59, tzinfo=timezone.utc) + target_str = ts.replace(minute=0, second=0, microsecond=0).strftime("%Y-%m-%dT%H:00") + + payload = { + "hourly": { + "time": [target_str], + "temperature_2m": [21.1], + "precipitation": [1.0], + "wind_speed_10m": [8.0], + "weather_code": [51], + "visibility": [7500.0], + } + } + + with patch("feature_engineering.weather.requests.get") as mget: + mget.return_value = _mock_response(payload) + + out1 = fetch_weather(lat, lon, ts) + assert mget.call_count == 1 + assert out1["temperature_c"] == 21.1 + + # Second call should hit the cache (no extra HTTP) + mget.side_effect = Exception("Should not be called due to cache") + out2 = fetch_weather(lat, lon, ts) + assert out2 == out1 + assert mget.call_count == 1 + + +def test_fetch_weather_missing_hour_returns_nones(fake_cache): + fake_cache.clear() + lat, lon = 9.5, -83.9 + ts = datetime(2025, 3, 3, 10, 10, tzinfo=timezone.utc) + wrong_hour = ts.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + + payload = { + "hourly": { + "time": [wrong_hour.strftime("%Y-%m-%dT%H:00")], + "temperature_2m": [25.0], + "precipitation": [0.0], + "wind_speed_10m": [10.0], + "weather_code": [1], + "visibility": [9000.0], + } + } + + with patch("feature_engineering.weather.requests.get") as mget: + mget.return_value = _mock_response(payload) + out = fetch_weather(lat, lon, ts) + + assert out == { + "temperature_c": None, + "precipitation_mm": None, + "wind_speed_kmh": None, + "weather_code": None, + "visibility_m": None, + } + + +def test_fetch_weather_network_error_returns_nones_and_caches(fake_cache): + fake_cache.clear() + lat, lon = 9.9, -84.2 + ts = datetime(2025, 4, 4, 6, 0, tzinfo=timezone.utc) + + with patch("feature_engineering.weather.requests.get") as mget: + from requests import RequestException + mget.side_effect = RequestException("boom") + out1 = fetch_weather(lat, lon, ts) + + assert out1 == { + "temperature_c": None, + "precipitation_mm": None, + "wind_speed_kmh": None, + "weather_code": None, + "visibility_m": None, + } + + # Second call should return from cache without HTTP + with patch("feature_engineering.weather.requests.get") as mget2: + out2 = fetch_weather(lat, lon, ts) + assert mget2.call_count == 0 + assert out2 == out1 diff --git a/eta_prediction/feature_engineering/weather.py b/eta_prediction/feature_engineering/weather.py new file mode 100644 index 0000000..da43c37 --- /dev/null +++ b/eta_prediction/feature_engineering/weather.py @@ -0,0 +1,103 @@ +from datetime import datetime, timedelta, timezone +import requests +from django.core.cache import cache + +OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" + +def fetch_weather(lat: float, lon: float, timestamp: datetime) -> dict: + """ + Returns a dict with: + - temperature_c: float + - precipitation_mm: float + - wind_speed_kmh: float + - weather_code: int (WMO code) + - visibility_m: float + + Caching: + Key: weather:{lat}:{lon}:{timestamp_hour_utc_iso} + TTL: 3600s + """ + # Normalize timestamp to UTC and truncate to the start of the hour + if timestamp.tzinfo is None: + ts_utc = timestamp.replace(tzinfo=timezone.utc) + else: + ts_utc = timestamp.astimezone(timezone.utc) + ts_hour = ts_utc.replace(minute=0, second=0, microsecond=0) + + cache_key = f"weather:{lat:.5f}:{lon:.5f}:{ts_hour.isoformat()}" + cached = cache.get(cache_key) + if cached is not None: + return cached + + # Query a 1-hour window [ts_hour, ts_hour+1h) so the array has exactly one item + ts_end = ts_hour + timedelta(hours=1) + target_time_str = ts_hour.strftime("%Y-%m-%dT%H:00") + + params = { + "latitude": lat, + "longitude": lon, + "hourly": ",".join([ + "temperature_2m", + "precipitation", + "wind_speed_10m", + "weather_code", + "visibility" + ]), + # Use explicit start/end to avoid a full-day fetch; keep timezone consistent + "start": ts_hour.isoformat().replace("+00:00", "Z"), + "end": ts_end.isoformat().replace("+00:00", "Z"), + "timezone": "UTC", + "windspeed_unit": "kmh", # ensures wind speed is km/h + "precipitation_unit": "mm" # (default is mm, set explicitly for clarity) + } + + try: + resp = requests.get(OPEN_METEO_URL, params=params, timeout=8) + resp.raise_for_status() + data = resp.json() + except requests.RequestException: + # Network/API error — return Nones (caller can decide fallback) + result = { + "temperature_c": None, + "precipitation_mm": None, + "wind_speed_kmh": None, + "weather_code": None, + "visibility_m": None, + } + cache.set(cache_key, result, timeout=3600) + return result + + hourly = data.get("hourly") or {} + times = hourly.get("time") or [] + + # Find the index for the exact hour + try: + idx = times.index(target_time_str) + except ValueError: + # Hour not found (e.g., API model coverage gap) — return Nones + result = { + "temperature_c": None, + "precipitation_mm": None, + "wind_speed_kmh": None, + "weather_code": None, + "visibility_m": None, + } + cache.set(cache_key, result, timeout=3600) + return result + + def _get(series_name, default=None): + series = hourly.get(series_name) + if not series or idx >= len(series): + return default + return series[idx] + + result = { + "temperature_c": _get("temperature_2m"), + "precipitation_mm": _get("precipitation"), + "wind_speed_kmh": _get("wind_speed_10m"), + "weather_code": _get("weather_code"), + "visibility_m": _get("visibility"), + } + + cache.set(cache_key, result, timeout=3600) + return result diff --git a/eta_prediction/gtfs-rt-pipeline/.env.example b/eta_prediction/gtfs-rt-pipeline/.env.example new file mode 100644 index 0000000..101bfa1 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/.env.example @@ -0,0 +1,14 @@ +DJANGO_SECRET_KEY=change-me +DJANGO_DEBUG=True +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 + +DATABASE_URL=postgresql://gtfs:gtfs@localhost:5432/gtfs + +REDIS_URL=redis://localhost:6379/0 + +GTFS_SCHEDULE_ZIP_URL=https://cdn.mbta.com/MBTA_GTFS.zip + +FEED_NAME=mbta +GTFSRT_VEHICLE_POSITIONS_URL=https://cdn.mbta.com/realtime/VehiclePositions.pb +GTFSRT_TRIP_UPDATES_URL=https://cdn.mbta.com/realtime/TripUpdates.pb +POLL_SECONDS=15 \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/README.md b/eta_prediction/gtfs-rt-pipeline/README.md new file mode 100644 index 0000000..ec217a5 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/README.md @@ -0,0 +1,123 @@ +# 🚍 GTFS-RT Ingestion Pipeline + +A Django + Celery + Redis workflow for ingesting **GTFS-Realtime feeds** (Vehicle Positions and Trip Updaes protobufs `.pb`) into **PostgreSQL**. +--- + +## ✨ Features +- Periodic fetch of GTFS-RT feeds (VP + TU) +- Deduplication via SHA-256 (`RawMessage` model) +- Idempotent parse & upsert into normalized tables +- Namespaced feed labels (e.g. `mbta:VP`, `mbta:TU`) +- Admin UI for browsing ingested rows +- Tested with MBTA live feeds (15s polling) + +--- + +## 🛠️ Stack +- **Django** → ORM, admin, migrations +- **Celery** → task queue, periodic tasks +- **Redis** → broker + result backend +- **PostgreSQL** → durable storage +- **uv** → Python project/env manager (`pyproject.toml`) +- **gtfs-realtime-bindings** → protobuf parsing +- **requests** → HTTP client + +--- + +## 📂 Project Structure +``` +gtfs-rt-pipeline/ +├─ pyproject.toml # deps +├─ .env.example # DB, Redis, feeds +├─ manage.py +├─ ingestproj/ # Django project +│ ├─ settings.py # DB + Celery config +│ ├─ celery.py # Celery app +└─ rt_pipeline/ # Django app + ├─ models.py # RawMessage, VehiclePosition, TripUpdate + ├─ tasks.py # fetch → parse → upsert + ├─ admin.py # models registered for web UI + └─ migrations/ +``` + +--- + +## ⚙️ Setup + +1. **Install deps** + ```bash + uv sync + ``` + +2. **Configure environment** (`.env`) + ```env + # --- Django --- + DJANGO_SECRET_KEY=change-me + DJANGO_DEBUG=True + DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 + + # --- Postgres --- + DATABASE_URL=postgresql://gtfs:gtfs@localhost:5432/gtfs + + # --- Redis (Celery broker/backend) --- + REDIS_URL=redis://localhost:6379/0 + + # --- Feed config --- + FEED_NAME=mbta + GTFSRT_VEHICLE_POSITIONS_URL=https://cdn.mbta.com/realtime/VehiclePositions.pb + GTFSRT_TRIP_UPDATES_URL=https://cdn.mbta.com/realtime/TripUpdates.pb + POLL_SECONDS=15 + + # --- HTTP --- + HTTP_CONNECT_TIMEOUT=3 + HTTP_READ_TIMEOUT=5 + ``` + +3. **Run services** + ```bash + # Start Redis + Postgres + redis-server & + postgres -D /usr/local/var/postgres & + + # Django + python manage.py migrate + python manage.py createsuperuser + python manage.py runserver + + # Celery + celery -A ingestproj worker -Q fetch,upsert -l INFO + celery -A ingestproj beat -l INFO + ``` + +--- + +## 🔄 How It Works + +1. **Fetch task** (`fetch_vehicle_positions` / `fetch_trip_updates`) + - Downloads `.pb` feed → computes hash → dedup in `RawMessage`. + - If new → enqueues parse task. + +2. **Parse task** (`parse_and_upsert_*`) + - Converts protobuf into rows. + - Bulk upserts into `VehiclePosition` or `TripUpdate`. + - Idempotent thanks to unique constraints. + +--- + +## 🔍 Inspecting Data + +**Django Admin** +```bash +python manage.py runserver +``` +Go to `localhost:8000/admin` → log in with your superuser → browse RawMessages, Vehicle Positions, and Trip Updates. + +--- + +## 📊 Current Status +✅ End-to-end pipeline works: +- Live MBTA data ingested every ~15s +- Deduplication enforced +- VP & TU both flowing into Postgres +- Admin UI enabled for quick inspection + diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/README.md b/eta_prediction/gtfs-rt-pipeline/analytics/README.md new file mode 100644 index 0000000..3c89bf5 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/README.md @@ -0,0 +1,56 @@ +# Analytics SQL Scripts + +This directory contains diagnostic and exploratory SQL queries for the GTFS-RT + Schedule database. +The included `run_exports.sh` script automatically executes all queries and exports their results to CSV files in the `exports/` folder. + +## Usage + +From `.../gtfs-rt-pipeline/`, + +```bash +./analytics/run_exports.sh +``` +- Loads `DATABASE_URL` from `.env` if not set. +- Cleans and executes each `.sql` file in `sql/`. +- Writes results as CSVs under `exports/`. + +--- + +## Query Descriptions + +### Data Coverage & Volume +- **vp_per_feed.sql** — VehiclePosition counts per feed/day. +- **vp_per_day.sql** — Daily VehiclePosition counts. +- **vp_time_range.sql** — First and last timestamps with total coverage span. +- **trips_per_route.sql** — Scheduled trip count per route. + +### Temporal Consistency & Gaps +- **vp_gap_hist.sql** — Histogram of polling intervals between VehiclePositions. +- **vp_gap_ranges.sql** — Gaps >60s between consecutive VehiclePositions. +- **vp_gaps_per_vehicle_day.sql** — Max reporting gap per vehicle/day (>60s). +- **vp_gaps_per_day_summary.sql** — Avg/max gaps and vehicle counts per day. +- **vehicle_poll_time_per_route.sql** — Avg/min/max polling interval per route. +- **vehicle_poll_time_per_trip.sql** — Avg/min/max polling interval per trip. + +### Trip & Route Structure +- **stops_per_route_distribution.sql** — Avg/min/max stops per trip by route. +- **stops_per_trip_and_route.sql** — Stop counts per trip and route. +- **sch_nextday_check.sql** — Detects stop times extending past midnight. + +### Trip ID & Vehicle Consistency +- **vp_tripid_nulls.sql** — Percentage of VehiclePositions missing `trip_id`. +- **vp_tripid_consistency.sql** — Distinct trips per vehicle/day (consistency check). +- **vp_trip_switches.sql** — Trip change events per vehicle/day. + +### GPS Completeness & Quality +- **vp_missing_gps_counts.sql** — Global count of rows missing lat/lon. +- **vp_missing_gps_by_vehicle.sql** — Missing GPS counts per vehicle. +- **vp_dupes_and_out_of_order.sql** — Detects duplicate or out-of-order timestamps. + +### Housekeeping +- **vp_and_tu_per_route.sql** — Counts of VehiclePositions vs TripUpdates per route. +- **vp_and_tu_per_trip.sql** — Counts of VehiclePositions vs TripUpdates per trip. + +--- + +**Output:** All query results are exported as CSV files in the `exports/` directory. diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/run_exports.sh b/eta_prediction/gtfs-rt-pipeline/analytics/run_exports.sh new file mode 100755 index 0000000..5427ace --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/run_exports.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Load .env if needed +if [ -z "${DATABASE_URL:-}" ]; then + ENV_FILE="$(dirname "$0")/../.env" + [ -f "$ENV_FILE" ] && { set -a; source "$ENV_FILE"; set +a; } || { + echo "DATABASE_URL not set and .env not found" >&2; exit 1; } +fi + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SQL_DIR="$ROOT/sql" +OUT_DIR="$ROOT/exports" +mkdir -p "$OUT_DIR" + +clean_sql() { + # strip block comments, line comments, and trailing semicolon; squash newlines + perl -0777 -pe 's{/\*.*?\*/}{}gs; s/(?m)--.*$//g;' "$1" \ + | tr '\n' ' ' \ + | sed -E 's/;[[:space:]]*$//' +} + +copy() { + local name="$1"; local file="$2"; local out="$OUT_DIR/${name}.csv" + echo "-> ${name}" + local query; query="$(clean_sql "$file")" + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "\copy (${query}) TO '${out}' CSV HEADER" +} + +# copy "vp_time_range" "$SQL_DIR/vp_time_range.sql" +# copy "vp_tripid_nulls" "$SQL_DIR/vp_tripid_nulls.sql" +# copy "vp_tripid_consistency" "$SQL_DIR/vp_tripid_consistency.sql" +# copy "vp_trip_switches" "$SQL_DIR/vp_trip_switches.sql" +# copy "vp_gaps_per_vehicle_day" "$SQL_DIR/vp_gaps_per_vehicle_day.sql" +# copy "vp_gaps_per_day_summary" "$SQL_DIR/vp_gaps_per_day_summary.sql" +# copy "vp_gap_hist" "$SQL_DIR/vp_gap_hist.sql" +# copy "vp_gap_ranges" "$SQL_DIR/vp_gap_ranges.sql" +# copy "sch_nextday_check" "$SQL_DIR/sch_nextday_check.sql" + +# Loop over all .sql files in the SQL_DIR +for file in "$SQL_DIR"/*.sql; do + name="$(basename "$file" .sql)" + copy "$name" "$file" +done + +echo "✅ CSVs written to $OUT_DIR" \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/sch_nextday_check.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/sch_nextday_check.sql new file mode 100644 index 0000000..400f159 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/sch_nextday_check.sql @@ -0,0 +1,8 @@ +SELECT + COUNT(*) FILTER (WHERE arrival_time >= '24:00:00') AS arrival_nextday, + COUNT(*) FILTER (WHERE departure_time >= '24:00:00') AS departure_nextday, + COUNT(*) AS total_rows, + ROUND(100.0 * COUNT(*) FILTER (WHERE arrival_time >= '24:00:00') / COUNT(*), 2) AS pct_arrival_nextday, + ROUND(100.0 * COUNT(*) FILTER (WHERE departure_time >= '24:00:00') / COUNT(*), 2) AS pct_departure_nextday +FROM sch_pipeline_stoptime; +-- This query checks for stop times with arrival or departure times indicating service on the next day (i.e., times >= 24:00:00). \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/stops_per_route_distribution.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/stops_per_route_distribution.sql new file mode 100644 index 0000000..f8505f8 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/stops_per_route_distribution.sql @@ -0,0 +1,17 @@ +-- stops_per_route_distribution.sql +WITH trip_counts AS ( + SELECT st.trip_id, COUNT(*) AS cnt + FROM sch_pipeline_stoptime st + GROUP BY st.trip_id +) +SELECT + r.route_id AS route_code, + ROUND(AVG(tc.cnt), 2) AS avg_stops_per_trip, + MIN(tc.cnt) AS min_stops, + MAX(tc.cnt) AS max_stops, + COUNT(*) AS trips_count +FROM trip_counts tc +JOIN sch_pipeline_trip t ON tc.trip_id = t.trip_id +JOIN sch_pipeline_route r ON t.route_id = r.route_id +GROUP BY r.route_id +ORDER BY r.route_id; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/stops_per_trip_and_route.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/stops_per_trip_and_route.sql new file mode 100644 index 0000000..dbf6784 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/stops_per_trip_and_route.sql @@ -0,0 +1,10 @@ +-- stops_per_trip_and_route.sql +SELECT + r.route_id AS route_code, + t.trip_id AS trip_code, + COUNT(*) AS n_stops +FROM sch_pipeline_stoptime st +JOIN sch_pipeline_trip t ON st.trip_id = t.trip_id +JOIN sch_pipeline_route r ON t.route_id = r.route_id +GROUP BY r.route_id, t.trip_id +ORDER BY r.route_id, t.trip_id; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/trips_per_route.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/trips_per_route.sql new file mode 100644 index 0000000..3d52eb9 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/trips_per_route.sql @@ -0,0 +1,19 @@ +-- -- Trips per route (by provider) +-- SELECT +-- feed_name, +-- route_id, +-- COUNT(*) AS trips_count +-- FROM sch_pipeline_trip +-- GROUP BY feed_name, route_id +-- ORDER BY feed_name, route_id; + +SELECT + t.route_id, + r.route_short_name, + r.route_long_name, + COUNT(*) AS trips_count +FROM sch_pipeline_trip t +LEFT JOIN sch_pipeline_route r + ON r.route_id = t.route_id +GROUP BY t.route_id, r.route_short_name, r.route_long_name +ORDER BY t.route_id; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vehicle_poll_time_per_route.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vehicle_poll_time_per_route.sql new file mode 100644 index 0000000..82eecd5 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vehicle_poll_time_per_route.sql @@ -0,0 +1,24 @@ +-- Per VEHICLE & ROUTE +WITH deltas AS ( + SELECT + feed_name, + vehicle_id, + route_id, + EXTRACT(EPOCH FROM ts - LAG(ts) OVER ( + PARTITION BY feed_name, vehicle_id, route_id + ORDER BY ts + )) AS delta_s + FROM rt_pipeline_vehicleposition +) +SELECT + feed_name, + vehicle_id, + route_id, + ROUND(AVG(delta_s)::numeric, 2) AS avg_poll_s, + MIN(delta_s) AS min_poll_s, + MAX(delta_s) AS max_poll_s, + COUNT(*) AS samples +FROM deltas +WHERE delta_s IS NOT NULL AND delta_s > 0 +GROUP BY feed_name, vehicle_id, route_id +ORDER BY feed_name, vehicle_id, route_id; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vehicle_poll_time_per_trip.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vehicle_poll_time_per_trip.sql new file mode 100644 index 0000000..201c2a3 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vehicle_poll_time_per_trip.sql @@ -0,0 +1,24 @@ +-- Per VEHICLE & TRIP +WITH deltas AS ( + SELECT + feed_name, + vehicle_id, + trip_id, + EXTRACT(EPOCH FROM ts - LAG(ts) OVER ( + PARTITION BY feed_name, vehicle_id, trip_id + ORDER BY ts + )) AS delta_s + FROM rt_pipeline_vehicleposition +) +SELECT + feed_name, + vehicle_id, + trip_id, + ROUND(AVG(delta_s)::numeric, 2) AS avg_poll_s, + MIN(delta_s) AS min_poll_s, + MAX(delta_s) AS max_poll_s, + COUNT(*) AS samples +FROM deltas +WHERE delta_s IS NOT NULL AND delta_s > 0 +GROUP BY feed_name, vehicle_id, trip_id +ORDER BY feed_name, vehicle_id, trip_id; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_and_tu_per_route.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_and_tu_per_route.sql new file mode 100644 index 0000000..6716b6f --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_and_tu_per_route.sql @@ -0,0 +1,20 @@ +-- Per ROUTE: counts of VehiclePositions vs TripUpdates (by provider) +WITH vp AS ( + SELECT feed_name, route_id, COUNT(*) AS vp_count + FROM rt_pipeline_vehicleposition + GROUP BY feed_name, route_id +), +tu AS ( + SELECT feed_name, route_id, COUNT(*) AS tu_count + FROM rt_pipeline_tripupdate + GROUP BY feed_name, route_id +) +SELECT + COALESCE(vp.feed_name, tu.feed_name) AS feed_name, + COALESCE(vp.route_id, tu.route_id) AS route_id, + vp.vp_count, + tu.tu_count +FROM vp +FULL OUTER JOIN tu + ON vp.feed_name = tu.feed_name AND vp.route_id = tu.route_id +ORDER BY feed_name, route_id; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_and_tu_per_trip.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_and_tu_per_trip.sql new file mode 100644 index 0000000..908d476 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_and_tu_per_trip.sql @@ -0,0 +1,20 @@ +-- Per TRIP: counts of VehiclePositions vs TripUpdates (by provider) +WITH vp AS ( + SELECT feed_name, trip_id, COUNT(*) AS vp_count + FROM rt_pipeline_vehicleposition + GROUP BY feed_name, trip_id +), +tu AS ( + SELECT feed_name, trip_id, COUNT(*) AS tu_count + FROM rt_pipeline_tripupdate + GROUP BY feed_name, trip_id +) +SELECT + COALESCE(vp.feed_name, tu.feed_name) AS feed_name, + COALESCE(vp.trip_id, tu.trip_id) AS trip_id, + vp.vp_count, + tu.tu_count +FROM vp +FULL OUTER JOIN tu + ON vp.feed_name = tu.feed_name AND vp.trip_id = tu.trip_id +ORDER BY feed_name, trip_id; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_dupes_and_out_of_order.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_dupes_and_out_of_order.sql new file mode 100644 index 0000000..e994f15 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_dupes_and_out_of_order.sql @@ -0,0 +1,35 @@ +-- vp_dupes_and_out_of_order.sql +-- Replace :vehicle_id with your target id (or remove the WHERE to scan all). +WITH ordered AS ( + SELECT + vehicle_id, + ts, + LAG(ts) OVER (PARTITION BY vehicle_id ORDER BY ts) AS prev_ts + FROM rt_pipeline_vehicleposition +-- WHERE vehicle_id = :vehicle_id +) +SELECT + 'DUPLICATE_TS' AS issue, + vehicle_id, + ts AS current_ts, + prev_ts +FROM ( + SELECT + vehicle_id, + ts, + COUNT(*) OVER (PARTITION BY vehicle_id, ts) AS ts_ct, + prev_ts + FROM ordered +) x +WHERE ts_ct > 1 + +UNION ALL + +SELECT + 'OUT_OF_ORDER' AS issue, + vehicle_id, + ts AS current_ts, + prev_ts +FROM ordered +WHERE prev_ts IS NOT NULL AND ts < prev_ts +ORDER BY issue, vehicle_id, current_ts; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gap_hist.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gap_hist.sql new file mode 100644 index 0000000..2dbc5cd --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gap_hist.sql @@ -0,0 +1,10 @@ +WITH deltas AS ( + SELECT EXTRACT(EPOCH FROM ts - LAG(ts) OVER (PARTITION BY vehicle_id ORDER BY ts)) AS delta_s + FROM rt_pipeline_vehicleposition +) +SELECT ROUND(delta_s) AS delta_s_rounded, COUNT(*) AS n +FROM deltas +WHERE delta_s IS NOT NULL +GROUP BY 1 +ORDER BY 1; +-- This query generates a histogram of time gaps (in seconds) between consecutive vehicle position reports. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gap_ranges.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gap_ranges.sql new file mode 100644 index 0000000..f04c587 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gap_ranges.sql @@ -0,0 +1,13 @@ +WITH ordered AS ( + SELECT vehicle_id, ts, LAG(ts) OVER (PARTITION BY vehicle_id ORDER BY ts) AS prev_ts + FROM rt_pipeline_vehicleposition +) +SELECT vehicle_id, + prev_ts AS gap_start, + ts AS gap_end, + ROUND(EXTRACT(EPOCH FROM ts - prev_ts))::int AS gap_s +FROM ordered +WHERE prev_ts IS NOT NULL + AND ts - prev_ts > INTERVAL '60 seconds' +ORDER BY gap_s DESC; +-- This query identifies gaps greater than 60 seconds between consecutive vehicle position reports, showing the start and end times of each gap along with its duration in seconds. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gaps_per_day_summary.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gaps_per_day_summary.sql new file mode 100644 index 0000000..ca06c57 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gaps_per_day_summary.sql @@ -0,0 +1,20 @@ +WITH deltas AS ( + SELECT DATE(ts) AS svc_day, vehicle_id, + EXTRACT(EPOCH FROM ts - LAG(ts) OVER (PARTITION BY vehicle_id ORDER BY ts)) AS delta_s + FROM rt_pipeline_vehicleposition +), +per_day AS ( + SELECT svc_day, AVG(delta_s) AS avg_gap_s, MAX(delta_s) AS max_gap_s + FROM deltas + WHERE delta_s IS NOT NULL + GROUP BY svc_day +) +SELECT + pd.svc_day, + (SELECT COUNT(DISTINCT vehicle_id) FROM rt_pipeline_vehicleposition t WHERE DATE(t.ts)=pd.svc_day) AS vehicles, + (SELECT COUNT(*) FROM rt_pipeline_vehicleposition t WHERE DATE(t.ts)=pd.svc_day) AS rows, + ROUND(pd.avg_gap_s)::int AS avg_gap_s, + ROUND(pd.max_gap_s)::int AS max_gap_s +FROM per_day pd +ORDER BY pd.svc_day DESC; +-- This query summarizes average and maximum gaps between vehicle position reports per service day, along with counts of distinct vehicles and total rows. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gaps_per_vehicle_day.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gaps_per_vehicle_day.sql new file mode 100644 index 0000000..1e9a131 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_gaps_per_vehicle_day.sql @@ -0,0 +1,12 @@ +WITH deltas AS ( + SELECT vehicle_id, DATE(ts) AS svc_day, + EXTRACT(EPOCH FROM ts - LAG(ts) OVER (PARTITION BY vehicle_id ORDER BY ts)) AS delta_s + FROM rt_pipeline_vehicleposition +) +SELECT vehicle_id, svc_day, MAX(delta_s) AS max_gap_s +FROM deltas +WHERE delta_s IS NOT NULL +GROUP BY 1,2 +HAVING MAX(delta_s) > 60 +ORDER BY max_gap_s DESC; +-- This query identifies vehicles that have gaps greater than 60 seconds between consecutive vehicle position reports on a given service day. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_missing_gps_by_vehicle.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_missing_gps_by_vehicle.sql new file mode 100644 index 0000000..3b02be9 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_missing_gps_by_vehicle.sql @@ -0,0 +1,8 @@ +-- vp_missing_gps_by_vehicle.sql +SELECT + vehicle_id, + COUNT(*) AS missing_rows +FROM rt_pipeline_vehicleposition +WHERE lat IS NULL OR lon IS NULL +GROUP BY vehicle_id +ORDER BY missing_rows DESC; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_missing_gps_counts.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_missing_gps_counts.sql new file mode 100644 index 0000000..dbb9dcf --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_missing_gps_counts.sql @@ -0,0 +1,8 @@ +-- vp_missing_gps_counts.sql +SELECT + COUNT(*) AS rows_with_missing_gps, + COUNT(*) FILTER (WHERE lat IS NULL) AS rows_missing_lat, + COUNT(*) FILTER (WHERE lon IS NULL) AS rows_missing_lon, + COUNT(DISTINCT vehicle_id) AS vehicles_with_missing_gps +FROM rt_pipeline_vehicleposition +WHERE lat IS NULL OR lon IS NULL; diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_per_day.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_per_day.sql new file mode 100644 index 0000000..dc97e27 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_per_day.sql @@ -0,0 +1,7 @@ +SELECT + DATE(ts) AS day, + COUNT(*) AS vp_count +FROM rt_pipeline_vehicleposition +GROUP BY 1 +ORDER BY 1; +-- Daily count of vehicle positions \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_per_feed.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_per_feed.sql new file mode 100644 index 0000000..92bf3b6 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_per_feed.sql @@ -0,0 +1,7 @@ +SELECT + feed_name, + DATE(ts) AS day, + COUNT(*) AS vp_count +FROM rt_pipeline_vehicleposition +GROUP BY 1,2 +ORDER BY 1,2; \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_time_range.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_time_range.sql new file mode 100644 index 0000000..e91f761 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_time_range.sql @@ -0,0 +1,4 @@ +SELECT MIN(ts) AS first_record, MAX(ts) AS last_record, (MAX(ts) - MIN(ts)) AS timespan +FROM rt_pipeline_vehicleposition; + +-- This query returns the first and last timestamps in the vehicle position table, along with the total timespan covered by the records. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_trip_switches.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_trip_switches.sql new file mode 100644 index 0000000..67465fa --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_trip_switches.sql @@ -0,0 +1,13 @@ +WITH ordered AS ( + SELECT vehicle_id, DATE(ts) AS svc_day, ts, trip_id, + LAG(trip_id) OVER (PARTITION BY vehicle_id, DATE(ts) ORDER BY ts) AS prev_trip + FROM rt_pipeline_vehicleposition +) +SELECT svc_day, vehicle_id, + SUM(CASE WHEN prev_trip IS NULL THEN 0 + WHEN trip_id IS DISTINCT FROM prev_trip THEN 1 + ELSE 0 END) AS real_trip_switches +FROM ordered +GROUP BY 1,2 +ORDER BY svc_day DESC, vehicle_id; +-- This query counts the number of times a vehicle switches trips on a given service day. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_tripid_consistency.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_tripid_consistency.sql new file mode 100644 index 0000000..ff20549 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_tripid_consistency.sql @@ -0,0 +1,9 @@ +WITH vp AS ( + SELECT vehicle_id, DATE(ts) AS svc_day, trip_id + FROM rt_pipeline_vehicleposition +) +SELECT vehicle_id, svc_day, COUNT(*) AS n_rows, COUNT(DISTINCT trip_id) AS distinct_trips +FROM vp +GROUP BY 1,2 +ORDER BY svc_day DESC, vehicle_id; +-- This query checks for consistency of trip_id values associated with each vehicle_id on a given service day. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_tripid_nulls.sql b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_tripid_nulls.sql new file mode 100644 index 0000000..a2e031b --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/analytics/sql/vp_tripid_nulls.sql @@ -0,0 +1,6 @@ +SELECT + COUNT(*) AS total_rows, + COUNT(*) FILTER (WHERE trip_id IS NULL) AS null_rows, + ROUND(100.0 * COUNT(*) FILTER (WHERE trip_id IS NULL) / COUNT(*), 2) AS null_pct +FROM rt_pipeline_vehicleposition; +-- This query checks for NULL values in the trip_id column of the vehicle position table. \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/diagnostics.py b/eta_prediction/gtfs-rt-pipeline/diagnostics.py new file mode 100644 index 0000000..791a897 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/diagnostics.py @@ -0,0 +1,136 @@ +""" +Debug helper to diagnose data availability issues. +Run with: python manage.py shell < debug_data.py +""" + +from django.db.models import Min, Max, Count, Q +from datetime import timedelta +from django.utils import timezone + + +import os +# init Django before importing models +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ingestproj.settings") +import django +django.setup() + + + +from sch_pipeline.models import StopTime, Stop, Route, Trip +from rt_pipeline.models import VehiclePosition, TripUpdate + +print("="*70) +print("GTFS-RT PIPELINE DATA DIAGNOSTICS") +print("="*70) + +# 1. Check Trip/Route data +print("\n1. SCHEDULE DATA (GTFS)") +print("-" * 70) +trip_count = Trip.objects.count() +route_count = Route.objects.count() +stop_count = Stop.objects.count() +stoptime_count = StopTime.objects.count() + +print(f" Trips: {trip_count:,}") +print(f" Routes: {route_count:,}") +print(f" Stops: {stop_count:,}") +print(f" StopTimes: {stoptime_count:,}") + +if route_count > 0: + top_routes = ( + Trip.objects + .values("route_id") + .annotate(trip_count=Count("id")) + .order_by("-trip_count")[:5] + ) + print(f"\n Top 5 routes by trip count:") + for r in top_routes: + print(f" {r['route_id']}: {r['trip_count']:,} trips") + +# 2. Check realtime data +print("\n2. REALTIME DATA (GTFS-RT)") +print("-" * 70) +vp_count = VehiclePosition.objects.count() +tu_count = TripUpdate.objects.count() + +print(f" VehiclePositions: {vp_count:,}") +print(f" TripUpdates: {tu_count:,}") + +if tu_count > 0: + tu_stats = TripUpdate.objects.aggregate( + min_ts=Min("ts"), + max_ts=Max("ts"), + unique_trips=Count("trip_id", distinct=True), + unique_stops=Count("stop_id", distinct=True), + ) + print(f"\n TripUpdate time range:") + print(f" Earliest: {tu_stats['min_ts']}") + print(f" Latest: {tu_stats['max_ts']}") + if tu_stats['min_ts'] and tu_stats['max_ts']: + span = tu_stats['max_ts'] - tu_stats['min_ts'] + print(f" Span: {span.days} days") + print(f" Unique trip_ids: {tu_stats['unique_trips']:,}") + print(f" Unique stop_ids: {tu_stats['unique_stops']:,}") + + # Check for start_date field + with_start_date = TripUpdate.objects.exclude(start_date__isnull=True).count() + print(f" TripUpdates with start_date: {with_start_date:,} ({100*with_start_date/tu_count:.1f}%)") + + # Check for arrival_time + with_arrival = TripUpdate.objects.exclude(arrival_time__isnull=True).count() + print(f" TripUpdates with arrival_time: {with_arrival:,} ({100*with_arrival/tu_count:.1f}%)") + +# 3. Check join potential +print("\n3. JOIN DIAGNOSTICS") +print("-" * 70) + +if trip_count > 0 and tu_count > 0: + # Check trip_id overlap + sample_trip_ids = set(Trip.objects.values_list("trip_id", flat=True)[:]) + tu_trip_ids = set(TripUpdate.objects.values_list("trip_id", flat=True).distinct()[:]) + overlap = sample_trip_ids & tu_trip_ids + + print(f" Sample trip_id overlap:") + print(f" Trip table (sample): {len(sample_trip_ids)}") + print(f" TripUpdate table (sample): {len(tu_trip_ids)}") + print(f" Overlap: {len(overlap)}") + + if overlap: + print(f" Example matching trip_ids: {list(overlap)[:3]}") + else: + print(f" ⚠️ NO OVERLAP - trip_ids don't match between tables!") + print(f" Trip examples: {list(sample_trip_ids)[:3]}") + print(f" TripUpdate examples: {list(tu_trip_ids)[:3]}") + +if stoptime_count > 0 and tu_count > 0: + # Check stop_id overlap + st_stop_ids = set(StopTime.objects.values_list("stop_id", flat=True).distinct()[:1000]) + tu_stop_ids = set(TripUpdate.objects.exclude(stop_id__isnull=True).values_list("stop_id", flat=True).distinct()[:1000]) + overlap = st_stop_ids & tu_stop_ids + + print(f"\n Stop_id overlap:") + print(f" StopTime table (sample): {len(st_stop_ids)}") + print(f" TripUpdate table (sample): {len(tu_stop_ids)}") + print(f" Overlap: {len(overlap)}") + + if not overlap and st_stop_ids and tu_stop_ids: + print(f" ⚠️ NO OVERLAP - stop_ids don't match!") + print(f" StopTime examples: {list(st_stop_ids)[:3]}") + print(f" TripUpdate examples: {list(tu_stop_ids)[:3]}") + +# 4. Sample query test +print("\n4. SAMPLE JOIN TEST") +print("-" * 70) + +if trip_count > 0 and tu_count > 0: + # Try to find one matching record + from django.db.models import OuterRef, Subquery, Exists + + # Get a trip_id that exists in both tables + tu_trip_ids = set(TripUpdate.objects.values_list("trip_id", flat=True).distinct()[:100]) + matching_trips = Trip.objects.filter(trip_id__in=tu_trip_ids)[:1] + + if matching_trips: + test_trip = matching_trips[0] + print(f" Test trip_id: {test_trip.trip_id}") + print(f" Route: {test_trip.route_id}") \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/eta_vp_sample.parquet b/eta_prediction/gtfs-rt-pipeline/eta_vp_sample.parquet new file mode 100644 index 0000000..ebd8d88 Binary files /dev/null and b/eta_prediction/gtfs-rt-pipeline/eta_vp_sample.parquet differ diff --git a/eta_prediction/gtfs-rt-pipeline/ingestproj/__init__.py b/eta_prediction/gtfs-rt-pipeline/ingestproj/__init__.py new file mode 100644 index 0000000..86bb0e5 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/ingestproj/__init__.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import, unicode_literals + +# This will make sure the app is always imported when Django starts +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/ingestproj/asgi.py b/eta_prediction/gtfs-rt-pipeline/ingestproj/asgi.py new file mode 100644 index 0000000..ecc270b --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/ingestproj/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for ingestproj project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ingestproj.settings') + +application = get_asgi_application() diff --git a/eta_prediction/gtfs-rt-pipeline/ingestproj/celery.py b/eta_prediction/gtfs-rt-pipeline/ingestproj/celery.py new file mode 100644 index 0000000..db5ceab --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/ingestproj/celery.py @@ -0,0 +1,27 @@ +import os +from celery import Celery +from celery.schedules import crontab +from django.conf import settings + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ingestproj.settings") +app = Celery("ingestproj") + +# All Celery config via Django settings/env +app.conf.broker_url = settings.REDIS_URL +app.conf.result_backend = settings.REDIS_URL +app.conf.task_acks_late = True +app.conf.worker_prefetch_multiplier = 4 +app.conf.task_routes = { + "rt_pipeline.tasks.fetch_vehicle_positions": {"queue": "fetch"}, + "rt_pipeline.tasks.parse_and_upsert_vehicle_positions": {"queue": "upsert"}, + "rt_pipeline.tasks.fetch_trip_updates": {"queue": "fetch"}, + "rt_pipeline.tasks.parse_and_upsert_trip_updates": {"queue": "upsert"}, + 'fetch-gtfs-schedule': { + 'task': 'gtfs_static.tasks.fetch_and_import_gtfs_schedule', + 'schedule': crontab(hour=3, minute=0), + 'options': {'queue': 'static'} + } +} +app.autodiscover_tasks() + + diff --git a/eta_prediction/gtfs-rt-pipeline/ingestproj/settings.py b/eta_prediction/gtfs-rt-pipeline/ingestproj/settings.py new file mode 100644 index 0000000..877b3c5 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/ingestproj/settings.py @@ -0,0 +1,79 @@ +import environ, os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +env = environ.Env() +environ.Env.read_env(BASE_DIR / ".env") + +SECRET_KEY = env("DJANGO_SECRET_KEY", default="dev-key") +DEBUG = env.bool("DJANGO_DEBUG", default=True) +ALLOWED_HOSTS = [h.strip() for h in env("DJANGO_ALLOWED_HOSTS", default="*").split(",")] + +INSTALLED_APPS = [ + "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", + "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.gis", + "rt_pipeline", "sch_pipeline" +] + +# Admin/templates config (required for admin) +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], # you can add template dirs later if you need + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +# Silence the auto field warning (recommended) +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", +] + +ROOT_URLCONF = "ingestproj.urls" +WSGI_APPLICATION = "ingestproj.wsgi.application" + + +DATABASES = {"default": env.db("DATABASE_URL")} +DATABASES["default"]["ENGINE"] = "django.contrib.gis.db.backends.postgis" + + +STATIC_URL = "static/" + +# Celery via env (set in celery.py as well) +REDIS_URL = env("REDIS_URL") +FEED_NAME = env("FEED_NAME") +GTFSRT_VEHICLE_POSITIONS_URL = env("GTFSRT_VEHICLE_POSITIONS_URL") +GTFSRT_TRIP_UPDATES_URL = env("GTFSRT_TRIP_UPDATES_URL") +POLL_SECONDS = env.int("POLL_SECONDS", default=15) +HTTP_CONNECT_TIMEOUT = env.float("HTTP_CONNECT_TIMEOUT", default=3.0) +HTTP_READ_TIMEOUT = env.float("HTTP_READ_TIMEOUT", default=5.0) + +from celery.schedules import schedule + +# CELERY_BEAT_SCHEDULE = { +# # existing vehicle positions schedule +# "poll-vehicle-positions": { +# "task": "rt_pipeline.tasks.fetch_vehicle_positions", +# "schedule": schedule(run_every=POLL_SECONDS), +# }, +# # NEW — trip updates +# "poll-trip-updates": { +# "task": "rt_pipeline.tasks.fetch_trip_updates", +# "schedule": schedule(run_every=POLL_SECONDS), +# }, +# } diff --git a/eta_prediction/gtfs-rt-pipeline/ingestproj/urls.py b/eta_prediction/gtfs-rt-pipeline/ingestproj/urls.py new file mode 100644 index 0000000..d394a44 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/ingestproj/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for ingestproj project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/eta_prediction/gtfs-rt-pipeline/ingestproj/wsgi.py b/eta_prediction/gtfs-rt-pipeline/ingestproj/wsgi.py new file mode 100644 index 0000000..b64d2d2 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/ingestproj/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for ingestproj project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ingestproj.settings') + +application = get_wsgi_application() diff --git a/eta_prediction/gtfs-rt-pipeline/manage.py b/eta_prediction/gtfs-rt-pipeline/manage.py new file mode 100755 index 0000000..3d2b818 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ingestproj.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/eta_prediction/gtfs-rt-pipeline/pyproject.toml b/eta_prediction/gtfs-rt-pipeline/pyproject.toml new file mode 100644 index 0000000..d706937 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "gtfs-rt-pipeline" +version = "0.1.0" +description = "Django + Celery pipeline for GTFS-Realtime ingestion" +requires-python = ">=3.11" +dependencies = [ + "Django>=5.0", + "celery>=5.4", + "redis>=5.0", + "psycopg[binary,pool]>=3.2", + "django-environ>=0.11", + "python-dotenv>=1.0", + "requests>=2.32", + "gtfs-realtime-bindings>=1.0.0", + "numpy>=2.3.4", + "pandas>=2.3.3", + "fastparquet>=2024.11.0", + "scikit-learn>=1.7.2", +] + +[project.optional-dependencies] +dev = ["ipython", "black", "pytest", "pytest-django", "mypy"] + +[tool.black] +line-length = 100 +target-version = ["py311"] + +[dependency-groups] +dev = [ + "ipykernel>=7.1.0", +] diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/__init__.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/admin.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/admin.py new file mode 100644 index 0000000..9475d7d --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/admin.py @@ -0,0 +1,52 @@ +from django.contrib import admin +from .models import RawMessage, VehiclePosition, TripUpdate + +@admin.register(RawMessage) +class RawMessageAdmin(admin.ModelAdmin): + list_display = ("feed_name", "message_type", "fetched_at", "header_timestamp", "incrementality", "content_hash") + search_fields = ("feed_name", "content_hash") + list_filter = ("feed_name", "message_type", "incrementality") + date_hierarchy = "fetched_at" + ordering = ("-fetched_at",) + + def __str__(self): + return f"{self.feed_name}:{self.message_type} @ {self.fetched_at:%Y-%m-%d %H:%M:%S}" + +@admin.register(VehiclePosition) +class VehiclePositionAdmin(admin.ModelAdmin): + list_display = ("feed_name", "vehicle_id", "ts", "lat", "lon", "route_id", "trip_id", "speed", "current_stop_sequence", "raw_message") + search_fields = ("vehicle_id", "route_id", "trip_id", "feed_name") + list_filter = ("feed_name", "route_id") + date_hierarchy = "ts" + ordering = ("-ts",) + + def __str__(self): + return f"{self.feed_name}:{self.vehicle_id} @ {self.ts:%Y-%m-%d %H:%M:%S}" + +@admin.register(TripUpdate) +class TripUpdateAdmin(admin.ModelAdmin): + list_display = ( + "feed_name", + "trip_id", + "start_time", + "start_date", + "schedule_relationship", + "vehicle_id", + "route_id", + "ts", + "stop_id", + "stop_sequence", + "arrival_time", + "departure_time", + "arrival_delay", + "departure_delay", + "stu_schedule_relationship", + "raw_message" + ) + search_fields = ("trip_id", "route_id", "vehicle_id", "stop_id", "feed_name") + list_filter = ("feed_name", "route_id") + date_hierarchy = "ts" + ordering = ("-ts",) + + def __str__(self): + return f"{self.feed_name}:{self.trip_id} @ {self.ts:%Y-%m-%d %H:%M:%S}" \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/apps.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/apps.py new file mode 100644 index 0000000..c795fcc --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RtPipelineConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'rt_pipeline' diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/management/commands/build_eta_sample.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/management/commands/build_eta_sample.py new file mode 100644 index 0000000..903e7ed --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/management/commands/build_eta_sample.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from datetime import timedelta, UTC + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.utils import timezone + +# --- Make sibling "feature_engineering" importable (no packaging needed) --- +BASE_DIR = Path(getattr(settings, "BASE_DIR", Path(__file__).resolve().parents[3])) +ETA_PREDICTION_ROOT = BASE_DIR.parent +FEATURE_ENG_ROOT = ETA_PREDICTION_ROOT / "feature_engineering" + +if str(ETA_PREDICTION_ROOT) not in sys.path: + sys.path.insert(0, str(ETA_PREDICTION_ROOT)) + +try: + from feature_engineering.dataset_builder import build_vp_training_dataset, save_dataset + from sch_pipeline.utils import top_routes_by_scheduled_trips +except ImportError as e: + print(f"ERROR: Failed to import required modules: {e}") + print(f"ETA_PREDICTION_ROOT: {ETA_PREDICTION_ROOT}") + print(f"FEATURE_ENG_ROOT: {FEATURE_ENG_ROOT}") + print(f"sys.path: {sys.path[:3]}") + raise + + +class Command(BaseCommand): + help = "Build ETA training dataset from VehiclePosition data for the top-N busiest routes." + + def add_arguments(self, parser): + parser.add_argument( + "--top-routes", + type=int, + default=3, + help="Top N routes by scheduled trips (global)" + ) + parser.add_argument( + "--days", + type=int, + default=14, + help="Lookback window in days" + ) + parser.add_argument( + "--min-observations", + type=int, + default=10, + help="Min observations per stop" + ) + parser.add_argument( + "--distance-threshold", + type=float, + default=50.0, + help="Distance threshold (meters) to consider vehicle 'arrived' at stop" + ) + parser.add_argument( + "--max-stops-ahead", + type=int, + default=5, + help="Maximum number of upcoming stops to include per VP" + ) + parser.add_argument( + "--vp-sample-interval", + type=int, + default=30, + help="Sample VPs every N seconds per vehicle (0=no sampling, use all VPs)" + ) + parser.add_argument( + "--out", + type=str, + default="eta_vp_sample.parquet", + help="Output parquet path" + ) + parser.add_argument( + "--no-weather", + action="store_true", + help="Disable weather features" + ) + parser.add_argument( + "--route-ids", + type=str, + help="Comma-separated route IDs (overrides --top-routes)" + ) + parser.add_argument( + "--start-date", + type=str, + help="Start date (YYYY-MM-DD format, overrides --days)" + ) + parser.add_argument( + "--end-date", + type=str, + help="End date (YYYY-MM-DD format, overrides --days)" + ) + + def handle(self, *args, **opts): + n = opts["top_routes"] + days = opts["days"] + min_obs = opts["min_observations"] + distance_threshold = opts["distance_threshold"] + max_stops_ahead = opts["max_stops_ahead"] + # vp_sample_interval = opts["vp_sample_interval"] + out = "../datasets/" + opts["out"] + attach_weather = not opts["no_weather"] + manual_routes = opts.get("route_ids") + start_date_str = opts.get("start_date") + end_date_str = opts.get("end_date") + + # Determine date range + if start_date_str and end_date_str: + try: + start = timezone.datetime.strptime(start_date_str, "%Y-%m-%d").replace(tzinfo=UTC) + end = timezone.datetime.strptime(end_date_str, "%Y-%m-%d").replace(tzinfo=UTC) + except ValueError as e: + self.stdout.write( + self.style.ERROR(f"Invalid date format: {e}. Use YYYY-MM-DD") + ) + return + else: + # Default: use fixed date range or calculate from --days + # For testing, using fixed dates: + start = timezone.datetime(2025, 10, 8, 0, 0, tzinfo=UTC) + end = timezone.datetime(2025, 10, 9, 0, 0, tzinfo=UTC) + # Or calculate from days: + # end = timezone.now() + # start = end - timedelta(days=days) + + # Determine which routes to use + if manual_routes: + route_ids = [r.strip() for r in manual_routes.split(",")] + self.stdout.write( + self.style.NOTICE(f"Using manually specified routes: {', '.join(route_ids)}") + ) + else: + self.stdout.write( + self.style.NOTICE(f"Selecting top {n} routes by scheduled trips...") + ) + try: + route_ids = top_routes_by_scheduled_trips(n=n) + except Exception as e: + self.stdout.write( + self.style.ERROR(f"Failed to get top routes: {e}") + ) + return + + if not route_ids: + self.stdout.write( + self.style.WARNING( + "No routes found. Is the schedule loaded? " + "Check: python manage.py shell -c 'from sch_pipeline.models import Trip; print(Trip.objects.count())'" + ) + ) + return + + self.stdout.write( + self.style.SUCCESS(f"Found routes: {', '.join(route_ids)}") + ) + + # Display configuration + self.stdout.write("\n" + "="*60) + self.stdout.write(self.style.NOTICE("Configuration:")) + self.stdout.write(f" Routes: {', '.join(route_ids)}") + self.stdout.write(f" Date range: {start.date()} to {end.date()}") + self.stdout.write(f" Distance threshold: {distance_threshold}m") + self.stdout.write(f" Max stops ahead: {max_stops_ahead}") + # # self.stdout.write(f" VP sample interval: {vp_sample_interval}s ({'all VPs' if vp_sample_interval == 0 else 'sampled'})") + self.stdout.write(f" Min observations/stop: {min_obs}") + self.stdout.write(f" Weather features: {'enabled' if attach_weather else 'disabled'}") + self.stdout.write(f" Output: {out}") + self.stdout.write("="*60 + "\n") + + # Check for VehiclePosition data + from rt_pipeline.models import VehiclePosition + vp_count = VehiclePosition.objects.filter( + ts__gte=start, + ts__lt=end + ).count() + + if vp_count == 0: + self.stdout.write( + self.style.WARNING( + f"No VehiclePosition data found in date range {start.date()} to {end.date()}\n" + "Check data availability:\n" + " python manage.py shell -c 'from rt_pipeline.models import VehiclePosition; " + "from django.db.models import Min, Max; " + "print(VehiclePosition.objects.aggregate(min=Min(\"ts\"), max=Max(\"ts\")))'" + ) + ) + return + else: + self.stdout.write( + self.style.SUCCESS(f"Found {vp_count:,} VehiclePosition records in date range") + ) + + # Build dataset + try: + self.stdout.write(self.style.NOTICE("\nBuilding dataset...")) + df = build_vp_training_dataset( + route_ids=route_ids, + start_date=start, + end_date=end, + distance_threshold=distance_threshold, + max_stops_ahead=max_stops_ahead, + # min_observations_per_stop=min_obs, + # vp_sample_interval_seconds=vp_sample_interval, + attach_weather=attach_weather, + ) + except Exception as e: + self.stdout.write( + self.style.ERROR(f"Failed to build dataset: {e}") + ) + import traceback + self.stdout.write(traceback.format_exc()) + return + + if df.empty: + self.stdout.write( + self.style.WARNING( + "Resulting dataset is empty. Possible issues:\n" + " 1. No VehiclePosition data in the date range\n" + " 2. VPs not matching any trips with stop sequences\n" + " 3. Vehicles never came close enough to stops (try increasing --distance-threshold)\n" + " 4. All data filtered out by --min-observations threshold\n" + " 5. No future VPs available to detect arrivals (incomplete trips)\n" + "\nDebug queries:\n" + " - Check VP count: python manage.py shell -c 'from rt_pipeline.models import VehiclePosition; print(VehiclePosition.objects.count())'\n" + " - Check date range: python manage.py shell -c 'from rt_pipeline.models import VehiclePosition; from django.db.models import Min, Max; print(VehiclePosition.objects.aggregate(min=Min(\"ts\"), max=Max(\"ts\")))'\n" + " - Check StopTime data: python manage.py shell -c 'from sch_pipeline.models import StopTime, Stop; print(f\"StopTimes: {StopTime.objects.count()}, Stops with coords: {Stop.objects.exclude(stop_lat__isnull=True).count()}\")'\n" + "\nTry adjusting parameters:\n" + " - Increase --distance-threshold (current: {})m\n" + " - Reduce --min-observations (current: {})\n" + " - Increase --max-stops-ahead (current: {})\n" + " - Set --vp-sample-interval to 0 to use all VPs".format( + distance_threshold, min_obs, max_stops_ahead + ) + ) + ) + return + + # Display summary statistics + self.stdout.write("\n" + "="*60) + self.stdout.write(self.style.SUCCESS("Dataset Summary:")) + self.stdout.write(f" Total rows: {len(df):,}") + self.stdout.write(f" Unique trips: {df['trip_id'].nunique():,}") + self.stdout.write(f" Unique routes: {df['route_id'].nunique()}") + self.stdout.write(f" Unique vehicles: {df['vehicle_id'].nunique():,}") + self.stdout.write(f" Unique stops: {df['stop_id'].nunique():,}") + + if "time_to_arrival_seconds" in df.columns: + tta_stats = df["time_to_arrival_seconds"].describe() + self.stdout.write(f"\n Time-to-arrival statistics:") + self.stdout.write(f" Mean: {tta_stats['mean']:.1f}s ({tta_stats['mean']/60:.1f} min)") + self.stdout.write(f" Median: {tta_stats['50%']:.1f}s ({tta_stats['50%']/60:.1f} min)") + self.stdout.write(f" Std: {tta_stats['std']:.1f}s") + self.stdout.write(f" Min: {tta_stats['min']:.1f}s") + self.stdout.write(f" Max: {tta_stats['max']:.1f}s ({tta_stats['max']/60:.1f} min)") + + if "distance_to_stop" in df.columns: + dist_stats = df["distance_to_stop"].describe() + self.stdout.write(f"\n Distance-to-stop statistics:") + self.stdout.write(f" Mean: {dist_stats['mean']:.1f}m") + self.stdout.write(f" Median: {dist_stats['50%']:.1f}m") + self.stdout.write(f" Min: {dist_stats['min']:.1f}m") + self.stdout.write(f" Max: {dist_stats['max']:.1f}m") + + if "current_speed_kmh" in df.columns: + speed_stats = df[df["current_speed_kmh"] > 0]["current_speed_kmh"].describe() + if not speed_stats.empty: + self.stdout.write(f"\n Speed statistics (km/h):") + self.stdout.write(f" Mean: {speed_stats['mean']:.1f}") + self.stdout.write(f" Median: {speed_stats['50%']:.1f}") + + missing = df.isnull().sum() + if missing.any(): + self.stdout.write(f"\n Missing values:") + for col, count in missing[missing > 0].items(): + pct = 100 * count / len(df) + self.stdout.write(f" {col}: {count:,} ({pct:.1f}%)") + + self.stdout.write("="*60 + "\n") + + # Save dataset + try: + save_dataset(df, out) + self.stdout.write( + self.style.SUCCESS(f"✓ Successfully saved to {out}") + ) + + # Provide guidance on next steps + self.stdout.write("\n" + self.style.NOTICE("Next steps:")) + self.stdout.write(" 1. Inspect the dataset: ") + self.stdout.write(f" import pandas as pd; df = pd.read_parquet('{out}'); df.head()") + self.stdout.write(" 2. Check feature distributions and correlations") + self.stdout.write(" 3. Train a model predicting 'time_to_arrival_seconds' from:") + self.stdout.write(" - distance_to_stop") + self.stdout.write(" - current_speed_kmh") + self.stdout.write(" - temporal features (hour, is_peak_hour, etc.)") + self.stdout.write(" - operational features (headway_seconds)") + self.stdout.write(" - weather features (if enabled)") + + except Exception as e: + self.stdout.write( + self.style.ERROR(f"Failed to save dataset: {e}") + ) + import traceback + self.stdout.write(traceback.format_exc()) + return \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/models.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/models.py new file mode 100644 index 0000000..38efd8b --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/models.py @@ -0,0 +1,94 @@ +from django.db import models +import uuid + +class RawMessage(models.Model): + # Message type choices + MESSAGE_TYPE_VEHICLE_POSITIONS = 'VP' + MESSAGE_TYPE_TRIP_UPDATES = 'TU' + MESSAGE_TYPE_CHOICES = [ + (MESSAGE_TYPE_VEHICLE_POSITIONS, 'Vehicle Positions'), + (MESSAGE_TYPE_TRIP_UPDATES, 'Trip Updates'), + ] + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + feed_name = models.TextField() + message_type = models.CharField(max_length=3, choices=MESSAGE_TYPE_CHOICES) + fetched_at = models.DateTimeField(auto_now_add=True) + header_timestamp = models.DateTimeField(null=True, blank=True) + incrementality = models.TextField(null=True, blank=True) + content = models.BinaryField() + content_hash = models.CharField(max_length=64) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["feed_name", "message_type", "content_hash"], + name="uq_feed_type_hash" + ) + ] + indexes = [ + models.Index(fields=["feed_name", "message_type", "-fetched_at"]), + ] + +class VehiclePosition(models.Model): + feed_name = models.TextField() + vehicle_id = models.TextField() + ts = models.DateTimeField() + lat = models.FloatField(null=True, blank=True) + lon = models.FloatField(null=True, blank=True) + bearing = models.FloatField(null=True, blank=True) + speed = models.FloatField(null=True, blank=True) + route_id = models.TextField(null=True, blank=True) + trip_id = models.TextField(null=True, blank=True) + current_stop_sequence = models.IntegerField(null=True, blank=True) + raw_message = models.ForeignKey(RawMessage, null=True, on_delete=models.SET_NULL) + ingested_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["feed_name", "vehicle_id", "ts"], name="uq_vp_natkey" + ) + ] + indexes = [ + models.Index(fields=["vehicle_id", "-ts"]), + models.Index(fields=["route_id", "-ts"]), + ] + +class TripUpdate(models.Model): + feed_name = models.TextField() + + # trip_update header-level + ts = models.DateTimeField(null=True, blank=True) # entity.trip_update.timestamp (UTC) + trip_id = models.TextField(null=True, blank=True) + route_id = models.TextField(null=True, blank=True) + start_time = models.TextField(null=True, blank=True) + start_date = models.TextField(null=True, blank=True) + schedule_relationship = models.TextField(null=True, blank=True) # header-level SR + + vehicle_id = models.TextField(null=True, blank=True) + + stop_sequence = models.IntegerField(null=True, blank=True) + stop_id = models.TextField(null=True, blank=True) + arrival_delay = models.IntegerField(null=True, blank=True) + arrival_time = models.DateTimeField(null=True, blank=True) # UTC + departure_delay = models.IntegerField(null=True, blank=True) + departure_time = models.DateTimeField(null=True, blank=True) # UTC + stu_schedule_relationship = models.TextField(null=True, blank=True) + + raw_message = models.ForeignKey('RawMessage', null=True, on_delete=models.SET_NULL) + ingested_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["feed_name", "trip_id", "ts", "stop_sequence"], + name="uq_tu_natkey", + ) + ] + indexes = [ + models.Index(fields=["trip_id", "-ts"]), + models.Index(fields=["route_id", "-ts"]), + models.Index(fields=["vehicle_id", "-ts"]), + models.Index(fields=["stop_id", "-ts"]), + ] \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/tasks.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/tasks.py new file mode 100644 index 0000000..31d36b6 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/tasks.py @@ -0,0 +1,219 @@ +from celery import shared_task +from django.conf import settings +import requests +from google.transit import gtfs_realtime_pb2 +from datetime import datetime, timezone +from django.db import transaction, IntegrityError +from .models import RawMessage, VehiclePosition, TripUpdate +import hashlib + +def _sha256_hex(b: bytes) -> str: return hashlib.sha256(b).hexdigest() +def _to_ts(sec): return datetime.fromtimestamp(sec, tz=timezone.utc) if sec else None +def _now(): return datetime.now(timezone.utc) + +# Vehicle Positions tasks +@shared_task(bind=True, autoretry_for=(requests.RequestException,), retry_backoff=True, retry_kwargs={"max_retries": 5}) +def fetch_vehicle_positions(self): + url = settings.GTFSRT_VEHICLE_POSITIONS_URL + r = requests.get(url, timeout=(settings.HTTP_CONNECT_TIMEOUT, settings.HTTP_READ_TIMEOUT)) + r.raise_for_status() + if not r.content: + return {"skipped": True} + + h = _sha256_hex(r.content) + + # Parse header early + feed = gtfs_realtime_pb2.FeedMessage() + feed.ParseFromString(r.content) + header_ts = _to_ts(feed.header.timestamp) if feed.HasField("header") else None + inc = None + if feed.HasField("header"): + inc_val = feed.header.incrementality # this is an int + try: + # Map int -> enum name (e.g., 0 -> "FULL_DATASET", 1 -> "DIFFERENTIAL") + inc = gtfs_realtime_pb2.FeedHeader.Incrementality.Name(inc_val) + except Exception: + # Fallback: store the raw int as string if mapping not available + inc = str(inc_val) + + try: + with transaction.atomic(): + obj = RawMessage.objects.create( + feed_name=settings.FEED_NAME, + message_type=RawMessage.MESSAGE_TYPE_VEHICLE_POSITIONS, + header_timestamp=header_ts, + incrementality=inc, + content=r.content, + content_hash=h, + ) + except IntegrityError: + # duplicate blob + existing = RawMessage.objects.filter( + feed_name=settings.FEED_NAME, + message_type=RawMessage.MESSAGE_TYPE_VEHICLE_POSITIONS, + content_hash=h + ).first() + return {"created": False, "raw_message_id": str(existing.id) if existing else None} + + parse_and_upsert_vehicle_positions.delay(str(obj.id)) + return {"created": True, "raw_message_id": str(obj.id)} + +@shared_task(bind=True) +def parse_and_upsert_vehicle_positions(self, raw_message_id: str): + raw = RawMessage.objects.filter(id=raw_message_id).only("content").first() + if not raw: return {"error": "raw_not_found"} + + feed = gtfs_realtime_pb2.FeedMessage() + feed.ParseFromString(bytes(raw.content)) + + rows = [] + for ent in feed.entity: + if not ent.HasField("vehicle"): + continue + v = ent.vehicle + vh_id = (v.vehicle.id or v.vehicle.label or ent.id or "unknown").strip() + ts = _to_ts(v.timestamp) or _now() + lat = v.position.latitude if v.HasField("position") else None + lon = v.position.longitude if v.HasField("position") else None + bearing = v.position.bearing if v.HasField("position") else None + speed = v.position.speed if v.HasField("position") else None + route_id = v.trip.route_id if v.HasField("trip") else None + trip_id = v.trip.trip_id if v.HasField("trip") else None + css = v.current_stop_sequence if v.HasField("current_stop_sequence") else None + + rows.append(VehiclePosition( + feed_name=settings.FEED_NAME, vehicle_id=vh_id, ts=ts, lat=lat, lon=lon, + bearing=bearing, speed=speed, route_id=route_id, trip_id=trip_id, + current_stop_sequence=css, raw_message_id=raw_message_id + )) + + if not rows: + return {"inserted": 0} + + # Bulk insert with conflict handling (Django 5+) + # We'll try bulk_create(ignore_conflicts=True) then optional updates + inserted = 0 + try: + VehiclePosition.objects.bulk_create(rows, ignore_conflicts=True, batch_size=2000) + inserted = len(rows) # approximate; conflicts ignored + except Exception as e: + return {"error": str(e)} + + return {"inserted": inserted} + +# Trip Updates tasks +@shared_task(bind=True, autoretry_for=(requests.RequestException,), retry_backoff=True, retry_kwargs={"max_retries": 5}) +def fetch_trip_updates(self): + """Download TU .pb and store a RawMessage. Enqueue parse task only for NEW content.""" + url = getattr(settings, "GTFSRT_TRIP_UPDATES_URL", None) + if not url: + return {"error": "GTFSRT_TRIP_UPDATES_URL not configured"} + + resp = requests.get(url, timeout=20) + resp.raise_for_status() + raw_bytes = resp.content + content_hash = _sha256_hex(raw_bytes) + + feed = gtfs_realtime_pb2.FeedMessage() + feed.ParseFromString(raw_bytes) + + header_ts = _to_ts(getattr(feed.header, "timestamp", None)) + incr = str(getattr(feed.header, "incrementality", "")) if hasattr(feed.header, "incrementality") else None + + try: + with transaction.atomic(): + raw = RawMessage.objects.create( + feed_name=settings.FEED_NAME, + message_type=RawMessage.MESSAGE_TYPE_TRIP_UPDATES, + content_hash=content_hash, + header_timestamp=header_ts, + incrementality=incr, + content=raw_bytes + ) + except IntegrityError: + # duplicate blob + existing = RawMessage.objects.filter( + feed_name=settings.FEED_NAME, + message_type=RawMessage.MESSAGE_TYPE_TRIP_UPDATES, + content_hash=content_hash + ).first() + return {"created": False, "raw_message_id": str(existing.id) if existing else None} + + # only parse if this payload is new + parse_and_upsert_trip_updates.delay(str(raw.id)) + return {"created": True, "raw_message_id": str(raw.id), "hash": content_hash} + +@shared_task(bind=True) +def parse_and_upsert_trip_updates(self, raw_id: str): + """Parse a stored RawMessage (TU) into TripUpdate rows (one per StopTimeUpdate).""" + raw = RawMessage.objects.get(id=raw_id) + feed = gtfs_realtime_pb2.FeedMessage() + feed.ParseFromString(bytes(raw.content)) + + rows = [] + for ent in feed.entity: + if not ent.HasField("trip_update"): + continue + tu = ent.trip_update + tu_ts = _to_ts(getattr(tu, "timestamp", None)) + + trip = getattr(tu, "trip", None) + veh = getattr(tu, "vehicle", None) + + trip_id = getattr(trip, "trip_id", None) or None + route_id = getattr(trip, "route_id", None) or None + start_time = getattr(trip, "start_time", None) or None + start_date = getattr(trip, "start_date", None) or None + header_sr = str(getattr(trip, "schedule_relationship", "")) if trip else None + vehicle_id = getattr(veh, "id", None) or None + + for stu in getattr(tu, "stop_time_update", []): + stop_sequence = getattr(stu, "stop_sequence", None) + stop_id = getattr(stu, "stop_id", None) or None + + arr = getattr(stu, "arrival", None) + dep = getattr(stu, "departure", None) + + arrival_delay = getattr(arr, "delay", None) if arr else None + arrival_time = _to_ts(getattr(arr, "time", None)) if arr and getattr(arr, "time", None) else None + departure_delay = getattr(dep, "delay", None) if dep else None + departure_time = _to_ts(getattr(dep, "time", None)) if dep and getattr(dep, "time", None) else None + stu_sr = str(getattr(stu, "schedule_relationship", "")) if stu else None + + rows.append(TripUpdate( + feed_name=settings.FEED_NAME, + ts=tu_ts, + trip_id=trip_id, + route_id=route_id, + start_time=start_time, + start_date=start_date, + schedule_relationship=header_sr, + vehicle_id=vehicle_id, + stop_sequence=stop_sequence, + stop_id=stop_id, + arrival_delay=arrival_delay, + arrival_time=arrival_time, + departure_delay=departure_delay, + departure_time=departure_time, + stu_schedule_relationship=stu_sr, + raw_message=raw, + )) + + if rows: + TripUpdate.objects.bulk_create(rows, ignore_conflicts=True) + return {"parsed_rows": len(rows)} + +# ---- Celery Beat schedule ---- +from celery.schedules import schedule +from ingestproj.celery import app as celery_app + +celery_app.conf.beat_schedule = { + "poll-vehicle-positions": { + "task": "rt_pipeline.tasks.fetch_vehicle_positions", + "schedule": schedule(run_every=settings.POLL_SECONDS), + }, + "poll-trip-updates": { + "task": "rt_pipeline.tasks.fetch_trip_updates", + "schedule": schedule(run_every=settings.POLL_SECONDS), + }, +} \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/tests.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/eta_prediction/gtfs-rt-pipeline/rt_pipeline/views.py b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/rt_pipeline/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/admin.py b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/admin.py new file mode 100644 index 0000000..3a314be --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/admin.py @@ -0,0 +1,306 @@ +from django.contrib import admin +from django.contrib.gis.admin import GISModelAdmin +from .models import ( + GTFSProvider, Feed, Agency, Stop, Route, Calendar, CalendarDate, + Shape, GeoShape, Trip, StopTime, FareAttribute, FareRule, + FeedInfo, RouteStop, TripDuration, TripTime +) + + +@admin.register(GTFSProvider) +class GTFSProviderAdmin(admin.ModelAdmin): + list_display = ('code', 'name', 'timezone', 'is_active') + list_filter = ('is_active',) + search_fields = ('code', 'name') + fieldsets = ( + ('Basic Information', { + 'fields': ('code', 'name', 'description', 'website', 'timezone', 'is_active') + }), + ('Feed URLs', { + 'fields': ( + 'schedule_url', + 'trip_updates_url', + 'vehicle_positions_url', + 'service_alerts_url' + ) + }), + ) + + +@admin.register(Feed) +class FeedAdmin(admin.ModelAdmin): + list_display = ('feed_id', 'gtfs_provider', 'is_current', 'retrieved_at') + list_filter = ('is_current', 'gtfs_provider') + search_fields = ('feed_id',) + readonly_fields = ('retrieved_at',) + date_hierarchy = 'retrieved_at' + + +@admin.register(Agency) +class AgencyAdmin(admin.ModelAdmin): + list_display = ('agency_name', 'agency_id', 'feed', 'agency_timezone') + list_filter = ('feed', 'agency_timezone') + search_fields = ('agency_id', 'agency_name') + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'agency_id', 'agency_name') + }), + ('Contact Information', { + 'fields': ('agency_url', 'agency_phone', 'agency_email', 'agency_fare_url') + }), + ('Regional Settings', { + 'fields': ('agency_timezone', 'agency_lang') + }), + ) + + +@admin.register(Stop) +class StopAdmin(GISModelAdmin): + list_display = ('stop_id', 'stop_name', 'feed', 'stop_lat', 'stop_lon', 'location_type') + list_filter = ('feed', 'location_type', 'wheelchair_boarding') + search_fields = ('stop_id', 'stop_name', 'stop_code') + readonly_fields = ('stop_point',) + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'stop_id', 'stop_code', 'stop_name') + }), + ('Location', { + 'fields': ('stop_lat', 'stop_lon', 'stop_point', 'zone_id') + }), + ('Type & Hierarchy', { + 'fields': ('location_type', 'parent_station') + }), + ('Accessibility', { + 'fields': ('wheelchair_boarding', 'platform_code') + }), + ('Amenities', { + 'fields': ('shelter', 'bench', 'lit', 'bay', 'device_charging_station'), + 'classes': ('collapse',) + }), + ('Additional Info', { + 'fields': ('stop_desc', 'stop_url', 'stop_heading', 'stop_timezone'), + 'classes': ('collapse',) + }), + ) + gis_widget_kwargs = { + 'attrs': { + 'default_zoom': 12, + } + } + + +@admin.register(Route) +class RouteAdmin(admin.ModelAdmin): + list_display = ('route_short_name', 'route_long_name', 'route_type', 'feed', '_agency') + list_filter = ('feed', 'route_type', '_agency') + search_fields = ('route_id', 'route_short_name', 'route_long_name') + readonly_fields = ('_agency',) + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'route_id', 'agency_id', '_agency') + }), + ('Names', { + 'fields': ('route_short_name', 'route_long_name', 'route_desc') + }), + ('Display', { + 'fields': ('route_type', 'route_color', 'route_text_color', 'route_sort_order') + }), + ('Additional', { + 'fields': ('route_url',), + 'classes': ('collapse',) + }), + ) + + +@admin.register(Calendar) +class CalendarAdmin(admin.ModelAdmin): + list_display = ('service_id', 'feed', 'start_date', 'end_date', 'weekday_summary') + list_filter = ('feed', 'start_date', 'end_date') + search_fields = ('service_id',) + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'service_id') + }), + ('Service Days', { + 'fields': ( + ('monday', 'tuesday', 'wednesday', 'thursday'), + ('friday', 'saturday', 'sunday') + ) + }), + ('Date Range', { + 'fields': ('start_date', 'end_date') + }), + ) + + def weekday_summary(self, obj): + days = [] + if obj.monday: days.append('Mon') + if obj.tuesday: days.append('Tue') + if obj.wednesday: days.append('Wed') + if obj.thursday: days.append('Thu') + if obj.friday: days.append('Fri') + if obj.saturday: days.append('Sat') + if obj.sunday: days.append('Sun') + return ', '.join(days) if days else 'No days' + weekday_summary.short_description = 'Service Days' + + +@admin.register(CalendarDate) +class CalendarDateAdmin(admin.ModelAdmin): + list_display = ('service_id', 'date', 'exception_type', 'holiday_name', 'feed') + list_filter = ('feed', 'exception_type', 'date') + search_fields = ('service_id', 'holiday_name') + date_hierarchy = 'date' + readonly_fields = ('_service',) + + +@admin.register(Shape) +class ShapeAdmin(admin.ModelAdmin): + list_display = ('shape_id', 'shape_pt_sequence', 'feed', 'shape_pt_lat', 'shape_pt_lon') + list_filter = ('feed',) + search_fields = ('shape_id',) + ordering = ('shape_id', 'shape_pt_sequence') + + +@admin.register(GeoShape) +class GeoShapeAdmin(GISModelAdmin): + list_display = ('shape_id', 'feed', 'shape_name', 'has_altitude') + list_filter = ('feed', 'has_altitude') + search_fields = ('shape_id', 'shape_name') + readonly_fields = ('geometry',) + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'shape_id') + }), + ('Metadata', { + 'fields': ('shape_name', 'shape_desc', 'shape_from', 'shape_to', 'has_altitude') + }), + ('Geometry', { + 'fields': ('geometry',) + }), + ) + + +@admin.register(Trip) +class TripAdmin(admin.ModelAdmin): + list_display = ('trip_id', 'route_id', 'service_id', 'trip_headsign', 'direction_id', 'feed') + list_filter = ('feed', 'direction_id', 'wheelchair_accessible', 'bikes_allowed') + search_fields = ('trip_id', 'trip_headsign', 'route_id') + readonly_fields = ('_route', '_service', 'geoshape') + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'trip_id', 'route_id', '_route') + }), + ('Service', { + 'fields': ('service_id', '_service', 'direction_id') + }), + ('Display', { + 'fields': ('trip_headsign', 'trip_short_name') + }), + ('Shape & Block', { + 'fields': ('shape_id', 'geoshape', 'block_id'), + 'classes': ('collapse',) + }), + ('Accessibility', { + 'fields': ('wheelchair_accessible', 'bikes_allowed') + }), + ) + + +@admin.register(StopTime) +class StopTimeAdmin(admin.ModelAdmin): + list_display = ('trip_id', 'stop_sequence', 'stop_id', 'arrival_time', 'departure_time', 'feed') + list_filter = ('feed', 'pickup_type', 'drop_off_type', 'timepoint') + search_fields = ('trip_id', 'stop_id') + readonly_fields = ('_trip', '_stop') + ordering = ('trip_id', 'stop_sequence') + fieldsets = ( + ('References', { + 'fields': ('feed', 'trip_id', '_trip', 'stop_id', '_stop') + }), + ('Sequence', { + 'fields': ('stop_sequence',) + }), + ('Times', { + 'fields': ('arrival_time', 'departure_time', 'timepoint') + }), + ('Pickup/Drop-off', { + 'fields': ('pickup_type', 'drop_off_type', 'stop_headsign') + }), + ('Distance', { + 'fields': ('shape_dist_traveled',), + 'classes': ('collapse',) + }), + ) + + +@admin.register(FareAttribute) +class FareAttributeAdmin(admin.ModelAdmin): + list_display = ('fare_id', 'price', 'currency_type', 'payment_method', 'feed') + list_filter = ('feed', 'currency_type', 'payment_method', 'transfers') + search_fields = ('fare_id',) + readonly_fields = ('_agency',) + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'fare_id', 'agency_id', '_agency') + }), + ('Price', { + 'fields': ('price', 'currency_type', 'payment_method') + }), + ('Transfers', { + 'fields': ('transfers', 'transfer_duration') + }), + ) + + +@admin.register(FareRule) +class FareRuleAdmin(admin.ModelAdmin): + list_display = ('fare_id', 'route_id', 'origin_id', 'destination_id', 'feed') + list_filter = ('feed',) + search_fields = ('fare_id', 'route_id') + readonly_fields = ('_fare', '_route') + + +@admin.register(FeedInfo) +class FeedInfoAdmin(admin.ModelAdmin): + list_display = ('feed_publisher_name', 'feed_version', 'feed', 'feed_start_date', 'feed_end_date') + list_filter = ('feed',) + search_fields = ('feed_publisher_name', 'feed_version') + fieldsets = ( + ('Identification', { + 'fields': ('feed', 'feed_publisher_name', 'feed_publisher_url') + }), + ('Version & Dates', { + 'fields': ('feed_version', 'feed_start_date', 'feed_end_date') + }), + ('Language & Contact', { + 'fields': ('feed_lang', 'feed_contact_email', 'feed_contact_url') + }), + ) + + +@admin.register(RouteStop) +class RouteStopAdmin(admin.ModelAdmin): + list_display = ('route_id', 'stop_id', 'stop_sequence', 'direction_id', 'timepoint', 'feed') + list_filter = ('feed', 'direction_id', 'timepoint') + search_fields = ('route_id', 'stop_id') + readonly_fields = ('_route', '_shape', '_stop') + ordering = ('route_id', 'shape_id', 'stop_sequence') + + +@admin.register(TripDuration) +class TripDurationAdmin(admin.ModelAdmin): + list_display = ('route_id', 'service_id', 'start_time', 'end_time', 'stretch', 'stretch_duration', 'feed') + list_filter = ('feed',) + search_fields = ('route_id', 'service_id') + readonly_fields = ('_route', '_shape', '_service') + ordering = ('route_id', 'start_time', 'stretch') + + +@admin.register(TripTime) +class TripTimeAdmin(admin.ModelAdmin): + list_display = ('trip_id', 'stop_id', 'stop_sequence', 'departure_time', 'feed') + list_filter = ('feed',) + search_fields = ('trip_id', 'stop_id') + readonly_fields = ('_trip', '_stop') + ordering = ('trip_id', 'stop_sequence') \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/fixtures/initial_providers.json b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/fixtures/initial_providers.json new file mode 100644 index 0000000..ba0264a --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/fixtures/initial_providers.json @@ -0,0 +1,17 @@ +[ + { + "model": "sch_pipeline.gtfsprovider", + "pk": 1, + "fields": { + "code": "mbta", + "name": "Massachusetts Bay Transportation Authority", + "description": "Boston area transit authority", + "website": "https://www.mbta.com", + "timezone": "America/New_York", + "is_active": true, + "schedule_url": "https://cdn.mbta.com/MBTA_GTFS.zip", + "vehicle_positions_url": "https://cdn.mbta.com/realtime/VehiclePositions.pb", + "trip_updates_url": "https://cdn.mbta.com/realtime/TripUpdates.pb" + } + } +] \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/management/__init__.py b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/management/commands/__init__.py b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/management/commands/import_gtfs.py b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/management/commands/import_gtfs.py new file mode 100644 index 0000000..2f6b9f9 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/management/commands/import_gtfs.py @@ -0,0 +1,283 @@ +import os +import io +import csv +import zipfile +from datetime import datetime +from decimal import Decimal + +import requests +from django.core.management.base import BaseCommand +from django.db import transaction +from django.contrib.gis.geos import Point, LineString + +from sch_pipeline.models import ( + Feed, Agency, Stop, Route, Calendar, CalendarDate, + Shape, GeoShape, Trip, StopTime, GTFSProvider +) + + +class Command(BaseCommand): + help = 'Download and import GTFS static schedule data' + + def add_arguments(self, parser): + parser.add_argument( + '--url', + type=str, + help='GTFS zip URL (overrides .env)', + ) + parser.add_argument( + '--provider-id', + type=int, + default=1, + help='GTFSProvider ID to associate with this feed', + ) + + def handle(self, *args, **options): + url = options['url'] or os.getenv('GTFS_SCHEDULE_ZIP_URL') + provider_id = options['provider_id'] + + if not url: + self.stdout.write(self.style.ERROR('No GTFS URL provided')) + return + + self.stdout.write(f'Downloading {url}...') + + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + except Exception as e: + self.stdout.write(self.style.ERROR(f'Download failed: {e}')) + return + + # Create feed ID + feed_id = f"{os.getenv('FEED_NAME', 'gtfs')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + + self.stdout.write(f'Importing as feed: {feed_id}') + + try: + with zipfile.ZipFile(io.BytesIO(response.content)) as zf: + importer = GTFSImporter(feed_id, provider_id, zf, self.stdout) + importer.import_all() + except Exception as e: + self.stdout.write(self.style.ERROR(f'Import failed: {e}')) + raise + + self.stdout.write(self.style.SUCCESS(f'Successfully imported {feed_id}')) + + +class GTFSImporter: + def __init__(self, feed_id, provider_id, zipfile, stdout): + self.feed_id = feed_id + self.provider_id = provider_id + self.zipfile = zipfile + self.stdout = stdout + self.feed = None + + def import_all(self): + """Import in dependency order""" + with transaction.atomic(): + self.import_feed() + self.import_agencies() + self.import_stops() + self.import_routes() + self.import_calendar() + self.import_calendar_dates() + self.import_shapes() + self.import_trips() + self.import_stop_times() + + def import_feed(self): + self.stdout.write('Creating Feed...') + provider = GTFSProvider.objects.get(provider_id=self.provider_id) + self.feed = Feed.objects.create( + feed_id=self.feed_id, + gtfs_provider=provider, + is_current=True + ) + + def import_agencies(self): + self.stdout.write('Importing agencies...') + with self.zipfile.open('agency.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + agencies = [] + for row in reader: + agencies.append(Agency( + feed=self.feed, + agency_id=row.get('agency_id', ''), + agency_name=row['agency_name'], + agency_url=row['agency_url'], + agency_timezone=row['agency_timezone'], + agency_lang=row.get('agency_lang', ''), + agency_phone=row.get('agency_phone', ''), + agency_fare_url=row.get('agency_fare_url', ''), + agency_email=row.get('agency_email', ''), + )) + Agency.objects.bulk_create(agencies, batch_size=1000, ignore_conflicts=True) + self.stdout.write(f' Imported {len(agencies)} agencies') + + def import_stops(self): + self.stdout.write('Importing stops...') + with self.zipfile.open('stops.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + stops = [] + for row in reader: + lat = Decimal(row['stop_lat']) if row.get('stop_lat') else None + lon = Decimal(row['stop_lon']) if row.get('stop_lon') else None + + stops.append(Stop( + feed=self.feed, + stop_id=row['stop_id'], + stop_code=row.get('stop_code', ''), + stop_name=row['stop_name'], + stop_lat=lat, + stop_lon=lon, + stop_point=Point(float(lon), float(lat)) if lat and lon else None, + location_type=int(row.get('location_type', 0)), + parent_station=row.get('parent_station', ''), + wheelchair_boarding=int(row.get('wheelchair_boarding', 0)), + )) + Stop.objects.bulk_create(stops, batch_size=1000, ignore_conflicts=True) + self.stdout.write(f' Imported {len(stops)} stops') + + def import_routes(self): + self.stdout.write('Importing routes...') + with self.zipfile.open('routes.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + routes = [] + for row in reader: + routes.append(Route( + feed=self.feed, + route_id=row['route_id'], + agency_id=row.get('agency_id', ''), + route_short_name=row.get('route_short_name', ''), + route_long_name=row.get('route_long_name', ''), + route_type=int(row.get('route_type', 3)), + route_color=row.get('route_color', ''), + )) + Route.objects.bulk_create(routes, batch_size=1000, ignore_conflicts=True) + self.stdout.write(f' Imported {len(routes)} routes') + + def import_calendar(self): + self.stdout.write('Importing calendar...') + try: + with self.zipfile.open('calendar.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + calendars = [] + for row in reader: + calendars.append(Calendar( + feed=self.feed, + service_id=row['service_id'], + monday=row['monday'] == '1', + tuesday=row['tuesday'] == '1', + wednesday=row['wednesday'] == '1', + thursday=row['thursday'] == '1', + friday=row['friday'] == '1', + saturday=row['saturday'] == '1', + sunday=row['sunday'] == '1', + start_date=datetime.strptime(row['start_date'], '%Y%m%d').date(), + end_date=datetime.strptime(row['end_date'], '%Y%m%d').date(), + )) + Calendar.objects.bulk_create(calendars, batch_size=1000, ignore_conflicts=True) + self.stdout.write(f' Imported {len(calendars)} calendar entries') + except KeyError: + self.stdout.write(' No calendar.txt found (optional)') + + def import_calendar_dates(self): + self.stdout.write('Importing calendar dates...') + try: + with self.zipfile.open('calendar_dates.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + dates = [] + for row in reader: + dates.append(CalendarDate( + feed=self.feed, + service_id=row['service_id'], + date=datetime.strptime(row['date'], '%Y%m%d').date(), + exception_type=int(row['exception_type']), + )) + CalendarDate.objects.bulk_create(dates, batch_size=1000, ignore_conflicts=True) + self.stdout.write(f' Imported {len(dates)} calendar date exceptions') + except KeyError: + self.stdout.write(' No calendar_dates.txt found (optional)') + + def import_shapes(self): + self.stdout.write('Importing shapes...') + try: + with self.zipfile.open('shapes.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + shapes = [] + for row in reader: + shapes.append(Shape( + feed=self.feed, + shape_id=row['shape_id'], + shape_pt_lat=Decimal(row['shape_pt_lat']), + shape_pt_lon=Decimal(row['shape_pt_lon']), + shape_pt_sequence=int(row['shape_pt_sequence']), + )) + Shape.objects.bulk_create(shapes, batch_size=1000, ignore_conflicts=True) + self.stdout.write(f' Imported {len(shapes)} shape points') + except KeyError: + self.stdout.write(' No shapes.txt found (optional)') + + def import_trips(self): + self.stdout.write('Importing trips...') + with self.zipfile.open('trips.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + trips = [] + for row in reader: + trips.append(Trip( + feed=self.feed, + route_id=row['route_id'], + service_id=row['service_id'], + trip_id=row['trip_id'], + trip_headsign=row.get('trip_headsign', ''), + direction_id=int(row.get('direction_id', 0)), + shape_id=row.get('shape_id', ''), + wheelchair_accessible=int(row.get('wheelchair_accessible', 0)), + bikes_allowed=int(row.get('bikes_allowed', 0)), + )) + Trip.objects.bulk_create(trips, batch_size=1000, ignore_conflicts=True) + self.stdout.write(f' Imported {len(trips)} trips') + + def import_stop_times(self): + self.stdout.write('Importing stop times (this may take a while)...') + with self.zipfile.open('stop_times.txt') as f: + reader = csv.DictReader(io.TextIOWrapper(f, 'utf-8-sig')) + stop_times = [] + count = 0 + for row in reader: + stop_times.append(StopTime( + feed=self.feed, + trip_id=row['trip_id'], + stop_id=row['stop_id'], + stop_sequence=int(row['stop_sequence']), + arrival_time=self._parse_time(row.get('arrival_time')), + departure_time=self._parse_time(row.get('departure_time')), + pickup_type=int(row.get('pickup_type', 0)), + drop_off_type=int(row.get('drop_off_type', 0)), + )) + + if len(stop_times) >= 5000: + StopTime.objects.bulk_create(stop_times, batch_size=5000, ignore_conflicts=True) + count += len(stop_times) + self.stdout.write(f' {count} stop times...', ending='\r') + stop_times = [] + + if stop_times: + StopTime.objects.bulk_create(stop_times, batch_size=5000, ignore_conflicts=True) + count += len(stop_times) + + self.stdout.write(f' Imported {count} stop times') + + def _parse_time(self, time_str): + """Parse GTFS time format (HH:MM:SS, may be >24 hours)""" + if not time_str: + return None + try: + h, m, s = time_str.split(':') + # GTFS allows times > 24 hours (e.g., 25:30:00 for 1:30 AM next day) + # Django TimeField can't handle this, so we cap at 23:59:59 + hours = min(int(h), 23) + return f"{hours:02d}:{m}:{s}" + except: + return None \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/models.py b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/models.py new file mode 100644 index 0000000..35965a7 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/models.py @@ -0,0 +1,1085 @@ +import re +from django.db.models import UniqueConstraint +from django.core.exceptions import ValidationError +from django.contrib.gis.db import models +from django.contrib.gis.geos import Point + + +def validate_no_spaces_or_special_symbols(value): + if re.search(r"[^a-zA-Z0-9_]", value): + raise ValidationError( + "Este campo no puede contener espacios ni símbolos especiales, solamente letras, números y guiones bajos." + ) + + +class GTFSProvider(models.Model): + """A provider provides transportation services GTFS data. + + It might or might not be the same as the agency in the GTFS feed. A GTFS provider can serve multiple agencies. + """ + + provider_id = models.BigAutoField(primary_key=True) + code = models.CharField( + max_length=31, + help_text="Código (típicamente el acrónimo) de la empresa. No debe tener espacios ni símbolos especiales.", + validators=[validate_no_spaces_or_special_symbols], + ) + name = models.CharField(max_length=255, help_text="Nombre de la empresa.") + description = models.TextField( + blank=True, null=True, help_text="Descripción de la institución o empresa." + ) + website = models.URLField( + blank=True, null=True, help_text="Sitio web de la empresa." + ) + schedule_url = models.URLField( + blank=True, + null=True, + help_text="URL del suministro (Feed) de GTFS Schedule (.zip).", + ) + trip_updates_url = models.URLField( + blank=True, + null=True, + help_text="URL del suministro (FeedMessage) de la entidad GTFS Realtime TripUpdates (.pb).", + ) + vehicle_positions_url = models.URLField( + blank=True, + null=True, + help_text="URL del suministro (FeedMessage) de la entidad GTFS Realtime VehiclePositions (.pb).", + ) + service_alerts_url = models.URLField( + blank=True, + null=True, + help_text="URL del suministro (FeedMessage) de la entidad GTFS Realtime ServiceAlerts (.pb).", + ) + timezone = models.CharField( + max_length=63, + help_text="Zona horaria del proveedor de datos (asume misma zona horaria para todas las agencias). Ejemplo: America/Costa_Rica.", + ) + is_active = models.BooleanField( + default=False, + help_text="¿Está activo el proveedor de datos? Si no, no se importarán los datos de este proveedor.", + ) + + def __str__(self): + return f"{self.name} ({self.code})" + + +# ------------- +# GTFS Schedule +# ------------- + + +class Feed(models.Model): + feed_id = models.CharField(max_length=100, primary_key=True, unique=True) + gtfs_provider = models.ForeignKey( + GTFSProvider, on_delete=models.SET_NULL, blank=True, null=True + ) + http_etag = models.CharField(max_length=1023, blank=True, null=True) + http_last_modified = models.DateTimeField(blank=True, null=True) + is_current = models.BooleanField(blank=True, null=True) + retrieved_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.feed_id + + +class Agency(models.Model): + """One or more transit agencies that provide the data in this feed. + Maps to agency.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, to_field="feed_id", on_delete=models.CASCADE) + agency_id = models.CharField( + max_length=255, + blank=True, + help_text="Identificador único de la agencia de transportes.", + ) + agency_name = models.CharField( + max_length=255, help_text="Nombre completo de la agencia de transportes." + ) + agency_url = models.URLField(help_text="URL de la agencia de transportes.") + agency_timezone = models.CharField( + max_length=255, help_text="Zona horaria de la agencia de transportes." + ) + agency_lang = models.CharField( + max_length=2, blank=True, help_text="Código ISO 639-1 de idioma primario." + ) + agency_phone = models.CharField( + max_length=127, blank=True, null=True, help_text="Número de teléfono." + ) + agency_fare_url = models.URLField( + blank=True, null=True, help_text="URL para la compra de tiquetes en línea." + ) + agency_email = models.EmailField( + max_length=254, + blank=True, + null=True, + help_text="Correo electrónico de servicio al cliente.", + ) + + class Meta: + constraints = [ + UniqueConstraint(fields=["feed", "agency_id"], name="unique_agency_in_feed") + ] + verbose_name = "agency" + verbose_name_plural = "agencies" + + def __str__(self): + return self.agency_name + + +class Stop(models.Model): + """Individual locations where vehicles pick up or drop off riders. + Maps to stops.txt in the GTFS feed. + """ + + STOP_HEADING_CHOICES = ( + ("N", "norte"), + ("NE", "noreste"), + ("E", "este"), + ("SE", "sureste"), + ("S", "sur"), + ("SW", "suroeste"), + ("W", "oeste"), + ("NW", "noroeste"), + ) + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + stop_id = models.CharField( + max_length=255, help_text="Identificador único de la parada." + ) + stop_code = models.CharField( + max_length=255, blank=True, null=True, help_text="Código de la parada." + ) + stop_name = models.CharField(max_length=255, help_text="Nombre de la parada.") + stop_heading = models.CharField( + max_length=2, blank=True, null=True, choices=STOP_HEADING_CHOICES + ) + stop_desc = models.TextField( + blank=True, null=True, help_text="Descripción de la parada." + ) + stop_lat = models.DecimalField( + max_digits=9, + decimal_places=6, + blank=True, + null=True, + help_text="Latitud de la parada.", + ) + stop_lon = models.DecimalField( + max_digits=9, + decimal_places=6, + blank=True, + null=True, + help_text="Longitud de la parada.", + ) + stop_point = models.PointField( + blank=True, null=True, help_text="Punto georreferenciado de la parada." + ) + zone_id = models.CharField( + max_length=255, + blank=True, + null=True, + help_text="Identificador de la zona tarifaria.", + ) + stop_url = models.URLField(blank=True, null=True, help_text="URL de la parada.") + location_type = models.PositiveIntegerField( + blank=True, + null=True, + help_text="Tipo de parada.", + choices=( + (0, "Parada o plataforma"), + (1, "Estación"), + (2, "Entrada o salida"), + (3, "Nodo genérico"), + (4, "Área de abordaje"), + ), + ) + parent_station = models.CharField( + max_length=255, blank=True, help_text="Estación principal." + ) + stop_timezone = models.CharField( + max_length=255, blank=True, help_text="Zona horaria de la parada." + ) + wheelchair_boarding = models.PositiveIntegerField( + blank=True, + null=True, + help_text="Acceso para sillas de ruedas.", + choices=((0, "No especificado"), (1, "Accesible"), (2, "No accesible")), + ) + platform_code = models.CharField( + max_length=255, blank=True, help_text="Código de la plataforma." + ) + shelter = models.BooleanField(blank=True, null=True, help_text="Con techo.") + bench = models.BooleanField( + blank=True, null=True, help_text="Con banco para sentarse." + ) + lit = models.BooleanField(blank=True, null=True, help_text="Con iluminación.") + bay = models.BooleanField(blank=True, null=True, help_text="Con bahía para el bus.") + device_charging_station = models.BooleanField( + blank=True, + null=True, + help_text="Con estación de carga de dispositivos móviles.", + ) + + class Meta: + constraints = [ + UniqueConstraint(fields=["feed", "stop_id"], name="unique_stop_in_feed") + ] + + # Build stop_point or stop_lat and stop_lon + def save(self, *args, **kwargs): + if self.stop_point: + self.stop_lat = self.stop_point.y + self.stop_lon = self.stop_point.x + else: + self.stop_point = Point(self.stop_lon, self.stop_lat) + super(Stop, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.stop_id}: {self.stop_name}" + + +class Route(models.Model): + """A group of trips that are displayed to riders as a single service. + Maps to routes.txt in the GTFS feed. + + _agency is a field to store the Agency object related to the route. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + route_id = models.CharField( + max_length=255, help_text="Identificador único de la ruta." + ) + agency_id = models.CharField(max_length=200) + _agency = models.ForeignKey(Agency, on_delete=models.CASCADE, blank=True, null=True) + route_short_name = models.CharField( + max_length=63, blank=True, null=True, help_text="Nombre corto de la ruta." + ) + route_long_name = models.CharField( + max_length=255, blank=True, null=True, help_text="Nombre largo de la ruta." + ) + route_desc = models.TextField( + blank=True, null=True, help_text="Descripción de la ruta." + ) + route_type = models.PositiveIntegerField( + choices=( + (0, "Tranvía o tren ligero"), + (1, "Subterráneo o metro"), + (2, "Ferrocarril"), + (3, "Bus"), + (4, "Ferry"), + (5, "Teleférico"), + (6, "Góndola"), + (7, "Funicular"), + ), + default=3, + help_text="Modo de transporte público.", + ) + route_url = models.URLField( + blank=True, null=True, help_text="URL de la ruta en el sitio web de la agencia." + ) + route_color = models.CharField( + max_length=6, + blank=True, + null=True, + help_text="Color que representa la ruta en formato hexadecimal.", + ) + route_text_color = models.CharField( + max_length=6, + blank=True, + null=True, + help_text="Color del texto que representa la ruta en formato hexadecimal.", + ) + route_sort_order = models.PositiveIntegerField(blank=True, null=True) + + class Meta: + constraints = [ + UniqueConstraint(fields=["feed", "route_id"], name="unique_route_in_feed") + ] + + def save(self, *args, **kwargs): + self._agency = Agency.objects.get(feed=self.feed, agency_id=self.agency_id) + super(Route, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.route_short_name}: {self.route_long_name}" + + +class Calendar(models.Model): + """Dates for service IDs using a weekly schedule. + Maps to calendar.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + service_id = models.CharField( + max_length=255, help_text="Identificador único del servicio." + ) + monday = models.BooleanField(help_text="Lunes") + tuesday = models.BooleanField(help_text="Martes") + wednesday = models.BooleanField(help_text="Miércoles") + thursday = models.BooleanField(help_text="Jueves") + friday = models.BooleanField(help_text="Viernes") + saturday = models.BooleanField(help_text="Sábado") + sunday = models.BooleanField(help_text="Domingo") + start_date = models.DateField(help_text="Fecha de inicio del servicio.") + end_date = models.DateField(help_text="Fecha de finalización del servicio.") + + class Meta: + constraints = [ + UniqueConstraint( + fields=["feed", "service_id"], name="unique_service_in_feed" + ) + ] + + def __str__(self): + return self.service_id + + +class CalendarDate(models.Model): + """Exceptions for the service IDs defined in the calendar.txt file. + Maps to calendar_dates.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + service_id = models.CharField( + max_length=255, help_text="Identificador único del servicio." + ) + _service = models.ForeignKey( + Calendar, on_delete=models.CASCADE, blank=True, null=True + ) + date = models.DateField(help_text="Fecha de excepción.") + exception_type = models.PositiveIntegerField( + choices=((1, "Agregar"), (2, "Eliminar")), help_text="Tipo de excepción." + ) + holiday_name = models.CharField( + max_length=255, + default="Feriado", + help_text="Nombre de la festividad o feriado.", + ) + + class Meta: + constraints = [ + UniqueConstraint( + fields=["feed", "service_id", "date"], name="unique_date_in_feed" + ) + ] + + def save(self, *args, **kwargs): + self._service = Calendar.objects.get(feed=self.feed, service_id=self.service_id) + super(CalendarDate, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.holiday_name} ({self.service_id})" + + +class Shape(models.Model): + """Rules for drawing lines on a map to represent a transit organization's routes. + Maps to shapes.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + shape_id = models.CharField( + max_length=255, help_text="Identificador único de la trayectoria." + ) + shape_pt_lat = models.DecimalField( + max_digits=9, + decimal_places=6, + help_text="Latitud de un punto en la trayectoria.", + ) + shape_pt_lon = models.DecimalField( + max_digits=9, + decimal_places=6, + help_text="Longitud de un punto en la trayectoria.", + ) + shape_pt_sequence = models.PositiveIntegerField( + help_text="Secuencia del punto en la trayectoria." + ) + shape_dist_traveled = models.DecimalField( + max_digits=6, + decimal_places=4, + blank=True, + null=True, + help_text="Distancia recorrida en la trayectoria.", + ) + + class Meta: + constraints = [ + UniqueConstraint( + fields=["feed", "shape_id", "shape_pt_sequence"], + name="unique_shape_in_feed", + ) + ] + + def __str__(self): + return f"{self.shape_id}: {self.shape_pt_sequence}" + + +class GeoShape(models.Model): + """Rules for drawing lines on a map to represent a transit organization's routes. + Maps to shapes.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + shape_id = models.CharField( + max_length=255, help_text="Identificador único de la trayectoria." + ) + geometry = models.LineStringField( + help_text="Trayectoria de la ruta.", + # dim=3, # To store 3D coordinates (x, y, z) + ) + shape_name = models.CharField(max_length=255, blank=True, null=True) + shape_desc = models.TextField(blank=True, null=True) + shape_from = models.CharField(max_length=255, blank=True, null=True) + shape_to = models.CharField(max_length=255, blank=True, null=True) + has_altitude = models.BooleanField( + help_text="Indica si la trayectoria tiene datos de altitud", default=False + ) + + class Meta: + constraints = [ + UniqueConstraint( + fields=["feed", "shape_id"], name="unique_geoshape_in_feed" + ) + ] + + def __str__(self): + return self.shape_id + + +class Trip(models.Model): + """Trips for each route. A trip is a sequence of two or more stops that occurs at specific time. + Maps to trips.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + route_id = models.CharField(max_length=200) + _route = models.ForeignKey(Route, on_delete=models.CASCADE, blank=True, null=True) + service_id = models.CharField(max_length=200) + _service = models.ForeignKey( + Calendar, on_delete=models.CASCADE, blank=True, null=True + ) + trip_id = models.CharField( + max_length=255, help_text="Identificador único del viaje." + ) + trip_headsign = models.CharField( + max_length=255, blank=True, null=True, help_text="Destino del viaje." + ) + trip_short_name = models.CharField( + max_length=255, blank=True, null=True, help_text="Nombre corto del viaje." + ) + direction_id = models.PositiveIntegerField( + choices=((0, "En un sentido"), (1, "En el otro")), + help_text="Dirección del viaje.", + ) + block_id = models.CharField( + max_length=255, blank=True, null=True, help_text="Identificador del bloque." + ) + shape_id = models.CharField(max_length=255, blank=True, null=True) + geoshape = models.ForeignKey( + GeoShape, on_delete=models.SET_NULL, blank=True, null=True + ) + wheelchair_accessible = models.PositiveIntegerField( + choices=((0, "No especificado"), (1, "Accesible"), (2, "No accesible")), + help_text="¿Tiene acceso para sillas de ruedas?", + ) + bikes_allowed = models.PositiveIntegerField( + choices=((0, "No especificado"), (1, "Permitido"), (2, "No permitido")), + help_text="¿ Es permitido llevar bicicletas?", + ) + + class Meta: + constraints = [ + UniqueConstraint(fields=["feed", "trip_id"], name="unique_trip_in_feed") + ] + + def save(self, *args, **kwargs): + self._route = Route.objects.get(feed=self.feed, route_id=self.route_id) + self._service = Calendar.objects.get(feed=self.feed, service_id=self.service_id) + super(Trip, self).save(*args, **kwargs) + + def __str__(self): + return self.trip_id + + +class StopTime(models.Model): + """Times that a vehicle arrives at and departs from individual stops for each trip. + Maps to stop_times.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + trip_id = models.CharField(max_length=200) + _trip = models.ForeignKey(Trip, on_delete=models.CASCADE, blank=True, null=True) + arrival_time = models.TimeField( + help_text="Hora de llegada a la parada.", blank=True, null=True + ) + departure_time = models.TimeField( + help_text="Hora de salida de la parada.", blank=True, null=True + ) + stop_id = models.CharField(max_length=200) + _stop = models.ForeignKey(Stop, on_delete=models.CASCADE, blank=True, null=True) + stop_sequence = models.PositiveIntegerField( + help_text="Secuencia de la parada en el viaje." + ) + stop_headsign = models.CharField( + max_length=255, blank=True, null=True, help_text="Destino de la parada." + ) + pickup_type = models.PositiveIntegerField( + help_text="Tipo de recogida de pasajeros.", + ) + drop_off_type = models.PositiveIntegerField( + help_text="Tipo de bajada de pasajeros.", + ) + shape_dist_traveled = models.DecimalField( + max_digits=6, + decimal_places=4, + blank=True, + null=True, + help_text="Distancia recorrida en la trayectoria.", + ) + timepoint = models.BooleanField( + blank=True, + null=True, + default=False, + help_text="¿Es un punto de tiempo programado y exacto?", + ) + + class Meta: + constraints = [ + UniqueConstraint( + fields=["feed", "trip_id", "stop_sequence"], + name="unique_stoptime_in_feed", + ) + ] + + def save(self, *args, **kwargs): + self._trip = Trip.objects.get(feed=self.feed, trip_id=self.trip_id) + self._stop = Stop.objects.get(feed=self.feed, stop_id=self.stop_id) + super(StopTime, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.trip_id}: {self.stop_id} ({self.stop_sequence})" + + +class FareAttribute(models.Model): + """Rules for how to calculate the fare for a certain kind of trip. + Maps to fare_attributes.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + fare_id = models.CharField( + max_length=255, help_text="Identificador único de la tarifa." + ) + price = models.DecimalField( + max_digits=6, decimal_places=2, help_text="Precio de la tarifa." + ) + currency_type = models.CharField( + max_length=3, help_text="Código ISO 4217 de la moneda." + ) + payment_method = models.PositiveIntegerField( + choices=((0, "Pago a bordo"), (1, "Pago anticipado")), + help_text="Método de pago.", + ) + transfers = models.PositiveIntegerField( + choices=( + (0, "No permitido"), + (1, "Permitido una vez"), + (2, "Permitido dos veces"), + ), + blank=True, + null=True, + help_text="Número de transferencias permitidas.", + ) + agency_id = models.CharField(max_length=255, blank=True, null=True) + _agency = models.ForeignKey(Agency, on_delete=models.CASCADE, blank=True, null=True) + transfer_duration = models.PositiveIntegerField( + blank=True, null=True, help_text="Duración de la transferencia." + ) + + class Meta: + constraints = [ + UniqueConstraint(fields=["feed", "fare_id"], name="unique_fare_in_feed") + ] + + def save(self, *args, **kwargs): + if self.agency_id: + self._agency = Agency.objects.get(feed=self.feed, agency_id=self.agency_id) + super(FareAttribute, self).save(*args, **kwargs) + + def __str__(self): + return self.fare_id + + +class FareRule(models.Model): + """Rules for which fare to apply in a given situation. + Maps to fare_rules.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + fare_id = models.CharField(max_length=200) + _fare = models.ForeignKey( + FareAttribute, on_delete=models.CASCADE, blank=True, null=True + ) + route_id = models.CharField(max_length=200) + _route = models.ForeignKey(Route, on_delete=models.CASCADE, blank=True, null=True) + origin_id = models.CharField(max_length=255, blank=True, null=True) + destination_id = models.CharField(max_length=255, blank=True, null=True) + contains_id = models.CharField(max_length=255, blank=True, null=True) + + class Meta: + constraints = [ + UniqueConstraint( + fields=[ + "feed", + "fare_id", + "route_id", + "origin_id", + "destination_id", + "contains_id", + ], + name="unique_fare_rule_in_feed", + ) + ] + + def save(self, *args, **kwargs): + self._fare = FareAttribute.objects.get(feed=self.feed, fare_id=self.fare_id) + self._route = Route.objects.get(feed=self.feed, route_id=self.route_id) + super(FareRule, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.fare_id}: {self.route_id}" + + +class FeedInfo(models.Model): + """Additional information about the feed itself, including publisher, version, and expiration information. + Maps to feed_info.txt in the GTFS feed. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + feed_publisher_name = models.CharField( + max_length=255, help_text="Nombre del editor del feed." + ) + feed_publisher_url = models.URLField( + help_text="URL del editor del feed.", blank=True, null=True + ) + feed_lang = models.CharField( + max_length=2, help_text="Código ISO 639-1 de idioma primario." + ) + feed_start_date = models.DateField( + help_text="Fecha de inicio de la información del feed.", blank=True, null=True + ) + feed_end_date = models.DateField( + help_text="Fecha de finalización de la información del feed.", + blank=True, + null=True, + ) + feed_version = models.CharField( + max_length=255, blank=True, null=True, help_text="Versión del feed." + ) + feed_contact_email = models.EmailField( + max_length=254, + blank=True, + null=True, + help_text="Correo electrónico de contacto.", + ) + feed_contact_url = models.URLField( + blank=True, + null=True, + help_text="URL de contacto.", + ) + + def __str__(self): + return f"{self.feed_publisher_name}: {self.feed_version}" + + +# ---------------- +# Auxiliary models +# ---------------- + + +class RouteStop(models.Model): + """Describes the sequence of stops for a route and a shape.""" + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + route_id = models.CharField(max_length=200) + _route = models.ForeignKey(Route, on_delete=models.CASCADE, blank=True, null=True) + shape_id = models.CharField(max_length=200) + _shape = models.ForeignKey( + GeoShape, on_delete=models.CASCADE, blank=True, null=True + ) + direction_id = models.PositiveIntegerField() + stop_id = models.CharField(max_length=200) + _stop = models.ForeignKey(Stop, on_delete=models.CASCADE, blank=True, null=True) + stop_sequence = models.PositiveIntegerField() + timepoint = models.BooleanField() + + class Meta: + constraints = [ + UniqueConstraint( + fields=["feed", "route_id", "shape_id", "stop_sequence"], + name="unique_routestop_in_feed", + ) + ] + + def save(self, *args, **kwargs): + self._route = Route.objects.get(feed=self.feed, route_id=self.route_id) + self._shape = GeoShape.objects.get(feed=self.feed, shape_id=self.shape_id) + self._stop = Stop.objects.get(feed=self.feed, stop_id=self.stop_id) + super(RouteStop, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.route_id}: {self.stop_id} ({self.shape_id} {self.stop_sequence})" + + +class TripDuration(models.Model): + """Describes the duration of a trip for a route and a service.""" + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + route_id = models.CharField(max_length=200) + _route = models.ForeignKey(Route, on_delete=models.CASCADE, blank=True, null=True) + shape_id = models.CharField(max_length=200) + _shape = models.ForeignKey(Shape, on_delete=models.CASCADE, blank=True, null=True) + service_id = models.CharField(max_length=200) + _service = models.ForeignKey( + Calendar, on_delete=models.CASCADE, blank=True, null=True + ) + start_time = models.TimeField() + end_time = models.TimeField() + stretch = models.PositiveIntegerField() + stretch_duration = models.DurationField() + + class Meta: + constraints = [ + UniqueConstraint( + fields=[ + "feed", + "route_id", + "shape_id", + "service_id", + "start_time", + "stretch", + ], + name="unique_tripduration_in_feed", + ) + ] + + def save(self, *args, **kwargs): + self._route = Route.objects.get(feed=self.feed, route_id=self.route_id) + self._shape = Shape.objects.get(feed=self.feed, shape_id=self.shape_id) + self._service = Calendar.objects.get(feed=self.feed, service_id=self.service_id) + super(TripDuration, self).save(*args, **kwargs) + + def __str__(self): + return ( + f"{self.route_id}: {self.service_id} ({self.start_time} - {self.end_time})" + ) + + +class TripTime(models.Model): + """ + TODO: llenar cuando se hace el proceso de importación de GTFS. Este modelo se llena con los datos de stop_times.txt, con las filas donde timepoint = True. + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + trip_id = models.CharField(max_length=200) + _trip = models.ForeignKey(Trip, on_delete=models.CASCADE, blank=True, null=True) + stop_id = models.CharField(max_length=200) + _stop = models.ForeignKey(Stop, on_delete=models.CASCADE, blank=True, null=True) + stop_sequence = models.PositiveIntegerField() + departure_time = models.TimeField() + + class Meta: + constraints = [ + UniqueConstraint( + fields=["feed", "trip_id", "stop_id"], + name="unique_triptime_in_feed", + ) + ] + + def save(self, *args, **kwargs): + self._trip = Trip.objects.get(feed=self.feed, trip_id=self.trip_id) + self._stop = Stop.objects.get(feed=self.feed, stop_id=self.stop_id) + super(TripTime, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.trip_id}: {self.stop_id} ({self.departure_time})" + + +# ------------- +# GTFS Realtime +# ------------- + + +class FeedMessage(models.Model): + """ + Header of a GTFS Realtime FeedMessage. + + This is metadata to link records of other models to a retrieved FeedMessage containing several entities, typically (necessarily, in this implementation) of a single kind. + """ + + ENTITY_TYPE_CHOICES = ( + ("trip_update", "TripUpdate"), + ("vehicle", "VehiclePosition"), + ("alert", "Alert"), + ) + + feed_message_id = models.CharField(max_length=63, primary_key=True) + provider = models.ForeignKey( + GTFSProvider, on_delete=models.SET_NULL, blank=True, null=True + ) + entity_type = models.CharField(max_length=63, choices=ENTITY_TYPE_CHOICES) + timestamp = models.DateTimeField(auto_now=True) + incrementality = models.CharField(max_length=15) + gtfs_realtime_version = models.CharField(max_length=15) + + class Meta: + ordering = ["-timestamp"] + + def __str__(self): + return f"{self.entity_type} ({self.timestamp})" + + +class TripUpdate(models.Model): + """ + GTFS Realtime TripUpdate entity v2.0 (normalized). + + Trip updates represent fluctuations in the timetable. + """ + + id = models.BigAutoField(primary_key=True) + entity_id = models.CharField(max_length=127) + + # Foreign key to FeedMessage model + feed_message = models.ForeignKey("FeedMessage", on_delete=models.CASCADE) + + # TripDescriptor (message) + trip_trip_id = models.CharField(max_length=255, blank=True, null=True) + trip_route_id = models.CharField(max_length=255, blank=True, null=True) + trip_direction_id = models.IntegerField(blank=True, null=True) + trip_start_time = models.DurationField(blank=True, null=True) + trip_start_date = models.DateField(blank=True, null=True) + trip_schedule_relationship = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # VehicleDescriptor (message) + vehicle_id = models.CharField(max_length=255, blank=True, null=True) + vehicle_label = models.CharField(max_length=255, blank=True, null=True) + vehicle_license_plate = models.CharField(max_length=255, blank=True, null=True) + vehicle_wheelchair_accessible = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # Timestamp (uint64) + timestamp = models.DateTimeField(blank=True, null=True) + + # Delay (int32) + delay = models.IntegerField(blank=True, null=True) + + def __str__(self): + return f"{self.entity_id} ({self.feed_message})" + + +class StopTimeUpdate(models.Model): + """ + GTFS Realtime TripUpdate message v2.0 (normalized). + + Realtime update for arrival and/or departure events for a given stop on a trip, linked to a TripUpdate entity in a FeedMessage. + """ + + id = models.BigAutoField(primary_key=True) + + # Foreign key to FeedMessage and TripUpdate models + feed_message = models.ForeignKey(FeedMessage, on_delete=models.CASCADE) + trip_update = models.ForeignKey(TripUpdate, on_delete=models.CASCADE) + + # Stop ID (string) + stop_sequence = models.IntegerField(blank=True, null=True) + stop_id = models.CharField(max_length=127, blank=True, null=True) + + # StopTimeEvent (message): arrival + arrival_delay = models.IntegerField(blank=True, null=True) + arrival_time = models.DateTimeField(blank=True, null=True) + arrival_uncertainty = models.IntegerField(blank=True, null=True) + + # StopTimeEvent (message): departure + departure_delay = models.IntegerField(blank=True, null=True) + departure_time = models.DateTimeField(blank=True, null=True) + departure_uncertainty = models.IntegerField(blank=True, null=True) + + # OccupancyStatus (enum) + departure_occupancy_status = models.CharField(max_length=255, blank=True, null=True) + + # ScheduleRelationship (enum) + schedule_relationship = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return f"{self.stop_id} ({self.trip_update})" + + +class VehiclePosition(models.Model): + """ + GTFS Realtime VehiclePosition entity v2.0 (normalized). + + Vehicle position represents a few basic pieces of information about a particular vehicle on the network. + """ + + id = models.BigAutoField(primary_key=True) + entity_id = models.CharField(max_length=127) + + # Foreign key to FeedMessage model + feed_message = models.ForeignKey( + FeedMessage, on_delete=models.CASCADE, blank=True, null=True + ) + + # TripDescriptor (message) + vehicle_trip_trip_id = models.CharField(max_length=255) + vehicle_trip_route_id = models.CharField(max_length=255, blank=True, null=True) + vehicle_trip_direction_id = models.IntegerField(blank=True, null=True) + vehicle_trip_start_time = models.DurationField(blank=True, null=True) + vehicle_trip_start_date = models.DateField(blank=True, null=True) + vehicle_trip_schedule_relationship = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # VehicleDescriptor (message) + vehicle_vehicle_id = models.CharField(max_length=255, blank=True, null=True) + vehicle_vehicle_label = models.CharField(max_length=255, blank=True, null=True) + vehicle_vehicle_license_plate = models.CharField( + max_length=255, blank=True, null=True + ) + vehicle_vehicle_wheelchair_accessible = models.CharField( + max_length=31, blank=True, null=True + ) # (enum) + + # Position (message) + vehicle_position_latitude = models.FloatField(blank=True, null=True) + vehicle_position_longitude = models.FloatField(blank=True, null=True) + vehicle_position_point = models.PointField(srid=4326, blank=True, null=True) + vehicle_position_bearing = models.FloatField(blank=True, null=True) + vehicle_position_odometer = models.FloatField(blank=True, null=True) + vehicle_position_speed = models.FloatField(blank=True, null=True) # (meters/second) + + # Current stop sequence (uint32) + vehicle_current_stop_sequence = models.IntegerField(blank=True, null=True) + + # Stop ID (string) + vehicle_stop_id = models.CharField(max_length=255, blank=True, null=True) + + # VehicleStopStatus (enum) + vehicle_current_status = models.CharField(max_length=255, blank=True, null=True) + + # Timestamp (uint64) + vehicle_timestamp = models.DateTimeField(blank=True, null=True) + + # CongestionLevel (enum) + vehicle_congestion_level = models.CharField(max_length=255, blank=True, null=True) + + # OccupancyStatus (enum) + vehicle_occupancy_status = models.CharField(max_length=255, blank=True, null=True) + + # OccupancyPercentage (uint32) + vehicle_occupancy_percentage = models.FloatField(blank=True, null=True) + + # CarriageDetails (message): not implemented + + def save(self, *args, **kwargs): + self.vehicle_position_point = Point( + self.vehicle_position_longitude, self.vehicle_position_latitude + ) + super(VehiclePosition, self).save(*args, **kwargs) + + def __str__(self): + return f"{self.entity_id} ({self.feed_message})" + + +class Alert(models.Model): + """Alerts and warnings about the service. + Maps to alerts.txt in the GTFS feed. + + TODO: ajustar con Alerts de GTFS Realtime + """ + + id = models.BigAutoField(primary_key=True) + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + alert_id = models.CharField( + max_length=255, help_text="Identificador único de la alerta." + ) + route_id = models.CharField(max_length=255, help_text="Identificador de la ruta.") + trip_id = models.CharField(max_length=255, help_text="Identificador del viaje.") + service_date = models.DateField( + help_text="Fecha del servicio descrito por la alerta." + ) + service_start_time = models.TimeField( + help_text="Hora de inicio del servicio descrito por la alerta." + ) + service_end_time = models.TimeField( + help_text="Hora de finalización del servicio descrito por la alerta." + ) + alert_header = models.CharField( + max_length=255, help_text="Encabezado de la alerta." + ) + alert_description = models.TextField(help_text="Descripción de la alerta.") + alert_url = models.URLField(blank=True, null=True, help_text="URL de la alerta.") + cause = models.PositiveIntegerField( + choices=( + (1, "Otra causa"), + (2, "Accidente"), + (3, "Congestión"), + (4, "Evento"), + (5, "Mantenimiento"), + (6, "Planificado"), + (7, "Huelga"), + (8, "Manifestación"), + (9, "Demora"), + (10, "Cierre"), + ), + help_text="Causa de la alerta.", + ) + effect = models.PositiveIntegerField( + choices=( + (1, "Otro efecto"), + (2, "Desviación"), + (3, "Adelanto"), + (4, "Cancelación"), + (5, "Cierre"), + (6, "Desvío"), + (7, "Detención"), + (8, "Desconocido"), + ), + help_text="Efecto de la alerta.", + ) + severity = models.PositiveIntegerField( + choices=( + (1, "Desconocido"), + (2, "Información"), + (3, "Advertencia"), + (4, "Grave"), + (5, "Muy grave"), + ), + help_text="Severidad de la alerta.", + ) + published = models.DateTimeField( + help_text="Fecha y hora de publicación de la alerta." + ) + updated = models.DateTimeField( + help_text="Fecha y hora de actualización de la alerta." + ) + informed_entity = models.JSONField(help_text="Entidades informadas por la alerta.") + + def __str__(self): + return self.alert_id diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/tasks.py b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/tasks.py new file mode 100644 index 0000000..83603e5 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/tasks.py @@ -0,0 +1,32 @@ +from celery import shared_task +import requests +import zipfile +import io +from django.db import transaction +from .models import Feed, Agency, Stop, Route, Trip, StopTime, Calendar +from .importers import GTFSImporter # You'll need to write this + +@shared_task(queue='static') +def fetch_and_import_gtfs_schedule(): + """ + Download GTFS .zip, extract, parse CSVs, bulk insert. + Run this less frequently (e.g., daily or when feed updates). + """ + url = os.getenv('GTFS_SCHEDULE_ZIP_URL') + + # Download + response = requests.get(url, timeout=30) + response.raise_for_status() + + # Check if feed changed (via ETag or Last-Modified) + etag = response.headers.get('ETag') + last_modified = response.headers.get('Last-Modified') + + # Create Feed record + feed_id = f"{os.getenv('FEED_NAME')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + + with zipfile.ZipFile(io.BytesIO(response.content)) as z: + importer = GTFSImporter(feed_id, z) + importer.import_all() + + return f"Imported {feed_id}" \ No newline at end of file diff --git a/eta_prediction/gtfs-rt-pipeline/sch_pipeline/utils.py b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/utils.py new file mode 100644 index 0000000..b1939aa --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/sch_pipeline/utils.py @@ -0,0 +1,17 @@ +# gtfs-rt-pipeline/sch_pipeline/utils.py +from django.db.models import Count +from sch_pipeline.models import Trip + +def top_routes_by_scheduled_trips(n: int = 5) -> list[str]: + """ + Returns route_ids for the top-N busiest routes by number of scheduled trips. + Stable, fast, and independent of realtime ingestion volume. + """ + qs = ( + Trip.objects + # .filter(feed__provider_id=provider_id) + .values("route_id") + .annotate(trips_count=Count("id")) + .order_by("-trips_count")[:n] + ) + return [row["route_id"] for row in qs] diff --git a/eta_prediction/gtfs-rt-pipeline/uv.lock b/eta_prediction/gtfs-rt-pipeline/uv.lock new file mode 100644 index 0000000..acef554 --- /dev/null +++ b/eta_prediction/gtfs-rt-pipeline/uv.lock @@ -0,0 +1,1532 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "asgiref" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/bf/0f3ecda32f1cb3bf1dca480aca08a7a8a3bdc4bed2343a103f30731565c9/asgiref-3.9.2.tar.gz", hash = "sha256:a0249afacb66688ef258ffe503528360443e2b9a8d8c4581b6ebefa58c841ef1", size = 36894, upload-time = "2025-09-23T15:00:55.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "billiard" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/6a/1405343016bce8354b29d90aad6b0bf6485b5e60404516e4b9a3a9646cf0/billiard-4.2.2.tar.gz", hash = "sha256:e815017a062b714958463e07ba15981d802dc53d41c5b69d28c5a7c238f8ecf3", size = 155592, upload-time = "2025-09-20T14:44:40.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/80/ef8dff49aae0e4430f81842f7403e14e0ca59db7bbaf7af41245b67c6b25/billiard-4.2.2-py3-none-any.whl", hash = "sha256:4bc05dcf0d1cc6addef470723aac2a6232f3c7ed7475b0b580473a9145829457", size = 86896, upload-time = "2025-09-20T14:44:39.157Z" }, +] + +[[package]] +name = "black" +version = "25.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/f4/7531d4a336d2d4ac6cc101662184c8e7d068b548d35d874415ed9f4116ef/black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa", size = 1698727, upload-time = "2025-09-19T00:31:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/28/f9/66f26bfbbf84b949cc77a41a43e138d83b109502cd9c52dfc94070ca51f2/black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d", size = 1555679, upload-time = "2025-09-19T00:31:29.265Z" }, + { url = "https://files.pythonhosted.org/packages/bf/59/61475115906052f415f518a648a9ac679d7afbc8da1c16f8fdf68a8cebed/black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608", size = 1617453, upload-time = "2025-09-19T00:30:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5b/20fd5c884d14550c911e4fb1b0dae00d4abb60a4f3876b449c4d3a9141d5/black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f", size = 1333655, upload-time = "2025-09-19T00:30:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8e/319cfe6c82f7e2d5bfb4d3353c6cc85b523d677ff59edc61fdb9ee275234/black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0", size = 1742012, upload-time = "2025-09-19T00:33:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/94/cc/f562fe5d0a40cd2a4e6ae3f685e4c36e365b1f7e494af99c26ff7f28117f/black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4", size = 1581421, upload-time = "2025-09-19T00:35:25.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/67/6db6dff1ebc8965fd7661498aea0da5d7301074b85bba8606a28f47ede4d/black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e", size = 1655619, upload-time = "2025-09-19T00:30:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/10/10/3faef9aa2a730306cf469d76f7f155a8cc1f66e74781298df0ba31f8b4c8/black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a", size = 1342481, upload-time = "2025-09-19T00:31:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, +] + +[[package]] +name = "celery" +version = "5.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "cramjam" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/12/34bf6e840a79130dfd0da7badfb6f7810b8fcfd60e75b0539372667b41b6/cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4", size = 99100, upload-time = "2025-07-27T21:25:07.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/89/8001f6a9b6b6e9fa69bec5319789083475d6f26d52aaea209d3ebf939284/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04cfa39118570e70e920a9b75c733299784b6d269733dbc791d9aaed6edd2615", size = 3559272, upload-time = "2025-07-27T21:22:01.988Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/001d00070ca92e5fbe6aacc768e455568b0cde46b0eb944561a4ea132300/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66a18f68506290349a256375d7aa2f645b9f7993c10fc4cc211db214e4e61d2b", size = 1861743, upload-time = "2025-07-27T21:22:03.754Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/041a3af01bf3f6158f120070f798546d4383b962b63c35cd91dcbf193e17/cramjam-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50e7d65533857736cd56f6509cf2c4866f28ad84dd15b5bdbf2f8a81e77fa28a", size = 1699631, upload-time = "2025-07-27T21:22:05.192Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/5358b238808abebd0c949c42635c3751204ca7cf82b29b984abe9f5e33c8/cramjam-2.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f71989668458fc327ac15396db28d92df22f8024bb12963929798b2729d2df5", size = 2025603, upload-time = "2025-07-27T21:22:06.726Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/19dba7c03a27408d8d11b5a7a4a7908459cfd4e6f375b73264dc66517bf6/cramjam-2.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee77ac543f1e2b22af1e8be3ae589f729491b6090582340aacd77d1d757d9569", size = 1766283, upload-time = "2025-07-27T21:22:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/40e4b3408501d886d082db465c33971655fe82573c535428e52ab905f4d0/cramjam-2.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad52784120e7e4d8a0b5b0517d185b8bf7f74f5e17272857ddc8951a628d9be1", size = 1854407, upload-time = "2025-07-27T21:22:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/36/6e/c1b60ceb6d7ea6ff8b0bf197520aefe23f878bf2bfb0de65f2b0c2f82cd1/cramjam-2.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b86f8e6d9c1b3f9a75b2af870c93ceee0f1b827cd2507387540e053b35d7459", size = 2035793, upload-time = "2025-07-27T21:22:12.504Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ad/32a8d5f4b1e3717787945ec6d71bd1c6e6bccba4b7e903fc0d9d4e4b08c3/cramjam-2.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:320d61938950d95da2371b46c406ec433e7955fae9f396c8e1bf148ffc187d11", size = 2067499, upload-time = "2025-07-27T21:22:14.067Z" }, + { url = "https://files.pythonhosted.org/packages/ff/cd/3b5a662736ea62ff7fa4c4a10a85e050bfdaad375cc53dc80427e8afe41c/cramjam-2.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41eafc8c1653a35a5c7e75ad48138f9f60085cc05cd99d592e5298552d944e9f", size = 1981853, upload-time = "2025-07-27T21:22:15.908Z" }, + { url = "https://files.pythonhosted.org/packages/26/8e/1dbcfaaa7a702ee82ee683ec3a81656934dd7e04a7bc4ee854033686f98a/cramjam-2.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03a7316c6bf763dfa34279335b27702321da44c455a64de58112968c0818ec4a", size = 2034514, upload-time = "2025-07-27T21:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/f11709bfdce74af79a88b410dcb76dedc97612166e759136931bf63cfd7b/cramjam-2.11.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:244c2ed8bd7ccbb294a2abe7ca6498db7e89d7eb5e744691dc511a7dc82e65ca", size = 2155343, upload-time = "2025-07-27T21:22:18.854Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/3b98b61841a5376d9a9b8468ae58753a8e6cf22be9534a0fa5af4d8621cc/cramjam-2.11.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:405f8790bad36ce0b4bbdb964ad51507bfc7942c78447f25cb828b870a1d86a0", size = 2169367, upload-time = "2025-07-27T21:22:20.389Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/bd5db5c49dbebc8b002f1c4983101b28d2e7fc9419753db1c31ec22b03ef/cramjam-2.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b1b751a5411032b08fb3ac556160229ca01c6bbe4757bb3a9a40b951ebaac23", size = 2159334, upload-time = "2025-07-27T21:22:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/34/32/203c57acdb6eea727e7078b2219984e64ed4ad043c996ed56321301ba167/cramjam-2.11.0-cp311-cp311-win32.whl", hash = "sha256:5251585608778b9ac8effed544933df7ad85b4ba21ee9738b551f17798b215ac", size = 1605313, upload-time = "2025-07-27T21:22:24.126Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bd/102d6deb87a8524ac11cddcd31a7612b8f20bf9b473c3c645045e3b957c7/cramjam-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:dca88bc8b68ce6d35dafd8c4d5d59a238a56c43fa02b74c2ce5f9dfb0d1ccb46", size = 1710991, upload-time = "2025-07-27T21:22:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/7c84c913a5fae85b773a9dcf8874390f9d68ba0fcc6630efa7ff1541b950/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dba5c14b8b4f73ea1e65720f5a3fe4280c1d27761238378be8274135c60bbc6e", size = 3553368, upload-time = "2025-07-27T21:22:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/4f6d185d8a744776f53035e72831ff8eefc2354f46ab836f4bd3c4f6c138/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11eb40722b3fcf3e6890fba46c711bf60f8dc26360a24876c85e52d76c33b25b", size = 1860014, upload-time = "2025-07-27T21:22:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a8/626c76263085c6d5ded0e71823b411e9522bfc93ba6cc59855a5869296e7/cramjam-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeb26e2898994b6e8319f19a4d37c481512acdcc6d30e1b5ecc9d8ec57e835cb", size = 1693512, upload-time = "2025-07-27T21:22:30.999Z" }, + { url = "https://files.pythonhosted.org/packages/e9/52/0851a16a62447532e30ba95a80e638926fdea869a34b4b5b9d0a020083ba/cramjam-2.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f8d82081ed7d8fe52c982bd1f06e4c7631a73fe1fb6d4b3b3f2404f87dc40fe", size = 2025285, upload-time = "2025-07-27T21:22:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/122e444f59dbc216451d8e3d8282c9665dc79eaf822f5f1470066be1b695/cramjam-2.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:092a3ec26e0a679305018380e4f652eae1b6dfe3fc3b154ee76aa6b92221a17c", size = 1761327, upload-time = "2025-07-27T21:22:34.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bc/3a0189aef1af2b29632c039c19a7a1b752bc21a4053582a5464183a0ad3d/cramjam-2.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:529d6d667c65fd105d10bd83d1cd3f9869f8fd6c66efac9415c1812281196a92", size = 1854075, upload-time = "2025-07-27T21:22:36.157Z" }, + { url = "https://files.pythonhosted.org/packages/2e/80/8a6343b13778ce52d94bb8d5365a30c3aa951276b1857201fe79d7e2ad25/cramjam-2.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:555eb9c90c450e0f76e27d9ff064e64a8b8c6478ab1a5594c91b7bc5c82fd9f0", size = 2032710, upload-time = "2025-07-27T21:22:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/cd1778a207c29eda10791e3dfa018b588001928086e179fc71254793c625/cramjam-2.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5edf4c9e32493035b514cf2ba0c969d81ccb31de63bd05490cc8bfe3b431674e", size = 2068353, upload-time = "2025-07-27T21:22:39.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f0/5c2a5cd5711032f3b191ca50cb786c17689b4a9255f9f768866e6c9f04d9/cramjam-2.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2fe41f48c4d58d923803383b0737f048918b5a0d10390de9628bb6272b107", size = 1978104, upload-time = "2025-07-27T21:22:41.106Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8b/b363a5fb2c3347504fe9a64f8d0f1e276844f0e532aa7162c061cd1ffee4/cramjam-2.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca14cf1cabdb0b77d606db1bb9e9ca593b1dbd421fcaf251ec9a5431ec449f3", size = 2030779, upload-time = "2025-07-27T21:22:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/78/7b/d83dad46adb6c988a74361f81ad9c5c22642be53ad88616a19baedd06243/cramjam-2.11.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:309e95bf898829476bccf4fd2c358ec00e7ff73a12f95a3cdeeba4bb1d3683d5", size = 2155297, upload-time = "2025-07-27T21:22:44.6Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/60d9be4cb33d8740a4aa94c7513f2ef3c4eba4fd13536f086facbafade71/cramjam-2.11.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:86dca35d2f15ef22922411496c220f3c9e315d5512f316fe417461971cc1648d", size = 2169255, upload-time = "2025-07-27T21:22:46.534Z" }, + { url = "https://files.pythonhosted.org/packages/11/b0/4a595f01a243aec8ad272b160b161c44351190c35d98d7787919d962e9e5/cramjam-2.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:193c6488bd2f514cbc0bef5c18fad61a5f9c8d059dd56edf773b3b37f0e85496", size = 2155651, upload-time = "2025-07-27T21:22:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/47/7776659aaa677046b77f527106e53ddd47373416d8fcdb1e1a881ec5dc06/cramjam-2.11.0-cp312-cp312-win32.whl", hash = "sha256:514e2c008a8b4fa823122ca3ecab896eac41d9aa0f5fc881bd6264486c204e32", size = 1603568, upload-time = "2025-07-27T21:22:50.084Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/d53002729cfd94c5844ddfaf1233c86d29f2dbfc1b764a6562c41c044199/cramjam-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:53fed080476d5f6ad7505883ec5d1ec28ba36c2273db3b3e92d7224fe5e463db", size = 1709287, upload-time = "2025-07-27T21:22:51.534Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/406c5dc0f8e82385519d8c299c40fd6a56d97eca3fcd6f5da8dad48de75b/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c289729cc1c04e88bafa48b51082fb462b0a57dbc96494eab2be9b14dca62af", size = 3553330, upload-time = "2025-07-27T21:22:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/00/ad/4186884083d6e4125b285903e17841827ab0d6d0cffc86216d27ed91e91d/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:045201ee17147e36cf43d8ae2fa4b4836944ac672df5874579b81cf6d40f1a1f", size = 1859756, upload-time = "2025-07-27T21:22:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/54/01/91b485cf76a7efef638151e8a7d35784dae2c4ff221b1aec2c083e4b106d/cramjam-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:619cd195d74c9e1d2a3ad78d63451d35379c84bd851aec552811e30842e1c67a", size = 1693609, upload-time = "2025-07-27T21:22:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/d0c80d279b2976870fc7d10f15dcb90a3c10c06566c6964b37c152694974/cramjam-2.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6eb3ae5ab72edb2ed68bdc0f5710f0a6cad7fd778a610ec2c31ee15e32d3921e", size = 2024912, upload-time = "2025-07-27T21:22:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/d6/70/88f2a5cb904281ed5d3c111b8f7d5366639817a5470f059bcd26833fc870/cramjam-2.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7da3f4b19e3078f9635f132d31b0a8196accb2576e3213ddd7a77f93317c20", size = 1760715, upload-time = "2025-07-27T21:22:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/cf5b02081132537d28964fb385fcef9ed9f8a017dd7d8c59d317e53ba50d/cramjam-2.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57286b289cd557ac76c24479d8ecfb6c3d5b854cce54ccc7671f9a2f5e2a2708", size = 1853782, upload-time = "2025-07-27T21:23:01.07Z" }, + { url = "https://files.pythonhosted.org/packages/57/27/63525087ed40a53d1867021b9c4858b80cc86274ffe7225deed067d88d92/cramjam-2.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28952fbbf8b32c0cb7fa4be9bcccfca734bf0d0989f4b509dc7f2f70ba79ae06", size = 2032354, upload-time = "2025-07-27T21:23:03.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ef/dbba082c6ebfb6410da4dd39a64e654d7194fcfd4567f85991a83fa4ec32/cramjam-2.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ed2e4099812a438b545dfbca1928ec825e743cd253bc820372d6ef8c3adff4", size = 2068007, upload-time = "2025-07-27T21:23:04.526Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/d902b9358a46a086938feae83b2251720e030f06e46006f4c1fc0ac9da20/cramjam-2.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9aecd5c3845d415bd6c9957c93de8d93097e269137c2ecb0e5a5256374bdc8", size = 1977485, upload-time = "2025-07-27T21:23:06.058Z" }, + { url = "https://files.pythonhosted.org/packages/e8/03/982f54553244b0afcbdb2ad2065d460f0ab05a72a96896a969a1ca136a1e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:362fcf4d6f5e1242a4540812455f5a594949190f6fbc04f2ffbfd7ae0266d788", size = 2030447, upload-time = "2025-07-27T21:23:07.679Z" }, + { url = "https://files.pythonhosted.org/packages/74/5f/748e54cdb665ec098ec519e23caacc65fc5ae58718183b071e33fc1c45b4/cramjam-2.11.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:13240b3dea41b1174456cb9426843b085dc1a2bdcecd9ee2d8f65ac5703374b0", size = 2154949, upload-time = "2025-07-27T21:23:09.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/81/c4e6cb06ed69db0dc81f9a8b1dc74995ebd4351e7a1877143f7031ff2700/cramjam-2.11.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:c54eed83726269594b9086d827decc7d2015696e31b99bf9b69b12d9063584fe", size = 2168925, upload-time = "2025-07-27T21:23:10.976Z" }, + { url = "https://files.pythonhosted.org/packages/13/5b/966365523ce8290a08e163e3b489626c5adacdff2b3da9da1b0823dfb14e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f8195006fdd0fc0a85b19df3d64a3ef8a240e483ae1dfc7ac6a4316019eb5df2", size = 2154950, upload-time = "2025-07-27T21:23:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7d/7f8eb5c534b72b32c6eb79d74585bfee44a9a5647a14040bb65c31c2572d/cramjam-2.11.0-cp313-cp313-win32.whl", hash = "sha256:ccf30e3fe6d770a803dcdf3bb863fa44ba5dc2664d4610ba2746a3c73599f2e4", size = 1603199, upload-time = "2025-07-27T21:23:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/37/05/47b5e0bf7c41a3b1cdd3b7c2147f880c93226a6bef1f5d85183040cbdece/cramjam-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee36348a204f0a68b03400f4736224e9f61d1c6a1582d7f875c1ca56f0254268", size = 1708924, upload-time = "2025-07-27T21:23:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/de/07/a1051cdbbe6d723df16d756b97f09da7c1adb69e29695c58f0392bc12515/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100", size = 3554141, upload-time = "2025-07-27T21:23:17.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/58487d2e16ef3d04f51a7c7f0e69823e806744b4c21101e89da4873074bc/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3", size = 1860353, upload-time = "2025-07-27T21:23:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/67/b4/67f6254d166ffbcc9d5fa1b56876eaa920c32ebc8e9d3d525b27296b693b/cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968", size = 1693832, upload-time = "2025-07-27T21:23:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/55/a3/4e0b31c0d454ae70c04684ed7c13d3c67b4c31790c278c1e788cb804fa4a/cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0", size = 2027080, upload-time = "2025-07-27T21:23:23.303Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c7/5e8eed361d1d3b8be14f38a54852c5370cc0ceb2c2d543b8ba590c34f080/cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e", size = 1761543, upload-time = "2025-07-27T21:23:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/09/0c/06b7f8b0ce9fde89470505116a01fc0b6cb92d406c4fb1e46f168b5d3fa5/cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9", size = 1854636, upload-time = "2025-07-27T21:23:26.987Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c6/6ebc02c9d5acdf4e5f2b1ec6e1252bd5feee25762246798ae823b3347457/cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25", size = 2032715, upload-time = "2025-07-27T21:23:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/a122971c23f5ca4b53e4322c647ac7554626c95978f92d19419315dddd05/cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936", size = 2069039, upload-time = "2025-07-27T21:23:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f6121b90b86b9093c066889274d26a1de3f29969d45c2ed1ecbe2033cb78/cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4", size = 1979566, upload-time = "2025-07-27T21:23:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/f95bc57fd7f4166ce6da816cfa917fb7df4bb80e669eb459d85586498414/cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403", size = 2030905, upload-time = "2025-07-27T21:23:33.696Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/e429de4e8bc86ee65e090dae0f87f45abd271742c63fb2d03c522ffde28a/cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839", size = 2155592, upload-time = "2025-07-27T21:23:35.375Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6c/65a7a0207787ad39ad804af4da7f06a60149de19481d73d270b540657234/cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778", size = 2170839, upload-time = "2025-07-27T21:23:37.197Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/5c5db505ba692bc844246b066e23901d5905a32baf2f33719c620e65887f/cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3", size = 2157236, upload-time = "2025-07-27T21:23:38.854Z" }, + { url = "https://files.pythonhosted.org/packages/b0/22/88e6693e60afe98901e5bbe91b8dea193e3aa7f42e2770f9c3339f5c1065/cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6", size = 1604136, upload-time = "2025-07-27T21:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f8/01618801cd59ccedcc99f0f96d20be67d8cfc3497da9ccaaad6b481781dd/cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c", size = 1710272, upload-time = "2025-07-27T21:23:42.236Z" }, + { url = "https://files.pythonhosted.org/packages/40/81/6cdb3ed222d13ae86bda77aafe8d50566e81a1169d49ed195b6263610704/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e", size = 3559671, upload-time = "2025-07-27T21:23:44.504Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/52b7e54fe5ba1ef0270d9fdc43dabd7971f70ea2d7179be918c997820247/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86", size = 1867876, upload-time = "2025-07-27T21:23:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/28/30d5b8d10acd30db3193bc562a313bff722888eaa45cfe32aa09389f2b24/cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800", size = 1695562, upload-time = "2025-07-27T21:23:48.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/86/ec806f986e01b896a650655024ea52a13e25c3ac8a3a382f493089483cdc/cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51", size = 2025056, upload-time = "2025-07-27T21:23:50.404Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/c2c17586b90848d29d63181f7d14b8bd3a7d00975ad46e3edf2af8af7e1f/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb", size = 1764084, upload-time = "2025-07-27T21:23:52.265Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68bc334fadb434a61df10071dc8606702aa4f5b6cdb2df62474fc21d2845/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5", size = 1854859, upload-time = "2025-07-27T21:23:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4e/b48e67835b5811ec5e9cb2e2bcba9c3fd76dab3e732569fe801b542c6ca9/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3", size = 2035970, upload-time = "2025-07-27T21:23:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/d2ac33d572b4d90f7f0f2c8a1d60fb48f06b128fdc2c05f9b49891bb0279/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89", size = 2069320, upload-time = "2025-07-27T21:23:57.494Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4c/85cec77af4a74308ba5fca8e296c4e2f80ec465c537afc7ab1e0ca2f9a00/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e", size = 1982668, upload-time = "2025-07-27T21:23:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/55/45/938546d1629e008cc3138df7c424ef892719b1796ff408a2ab8550032e5e/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4", size = 2034028, upload-time = "2025-07-27T21:24:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/b5a53e20505555f1640e66dcf70394bcf51a1a3a072aa18ea35135a0f9ed/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c", size = 2155513, upload-time = "2025-07-27T21:24:02.92Z" }, + { url = "https://files.pythonhosted.org/packages/84/12/8d3f6ceefae81bbe45a347fdfa2219d9f3ac75ebc304f92cd5fcb4fbddc5/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65", size = 2170035, upload-time = "2025-07-27T21:24:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3be6f0a1398f976070672be64f61895f8839857618a2d8cc0d3ab529d3dc/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656", size = 2160229, upload-time = "2025-07-27T21:24:06.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/66cfc3635511b20014bbb3f2ecf0095efb3049e9e96a4a9e478e4f3d7b78/cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8", size = 1610267, upload-time = "2025-07-27T21:24:08.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c6/c71e82e041c95ffe6a92ac707785500aa2a515a4339c2c7dd67e3c449249/cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188", size = 1713108, upload-time = "2025-07-27T21:24:10.147Z" }, + { url = "https://files.pythonhosted.org/packages/81/da/b3301962ccd6fce9fefa1ecd8ea479edaeaa38fadb1f34d5391d2587216a/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:52d5db3369f95b27b9f3c14d067acb0b183333613363ed34268c9e04560f997f", size = 3573546, upload-time = "2025-07-27T21:24:52.944Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c2/410ddb8ad4b9dfb129284666293cb6559479645da560f7077dc19d6bee9e/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4820516366d455b549a44d0e2210ee7c4575882dda677564ce79092588321d54", size = 1873654, upload-time = "2025-07-27T21:24:54.958Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/f68a443c64f7ce7aff5bed369b0aa5b2fac668fa3dfd441837e316e97a1f/cramjam-2.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d9e5db525dc0a950a825202f84ee68d89a072479e07da98795a3469df942d301", size = 1702846, upload-time = "2025-07-27T21:24:57.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/02/0ff358ab773def1ee3383587906c453d289953171e9c92db84fdd01bf172/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62ab4971199b2270005359cdc379bc5736071dc7c9a228581c5122d9ffaac50c", size = 1773683, upload-time = "2025-07-27T21:24:59.28Z" }, + { url = "https://files.pythonhosted.org/packages/e9/31/3298e15f87c9cf2aabdbdd90b153d8644cf989cb42a45d68a1b71e1f7aaf/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24758375cc5414d3035ca967ebb800e8f24604ececcba3c67d6f0218201ebf2d", size = 1994136, upload-time = "2025-07-27T21:25:01.565Z" }, + { url = "https://files.pythonhosted.org/packages/c7/90/20d1747255f1ee69a412e319da51ea594c18cca195e7a4d4c713f045eff5/cramjam-2.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c2eea545fef1065c7dd4eda991666fd9c783fbc1d226592ccca8d8891c02f23", size = 1714982, upload-time = "2025-07-27T21:25:05.79Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" }, + { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "django" +version = "5.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/8c/2a21594337250a171d45dda926caa96309d5136becd1f48017247f9cdea0/django-5.2.6.tar.gz", hash = "sha256:da5e00372763193d73cecbf71084a3848458cecf4cee36b9a1e8d318d114a87b", size = 10858861, upload-time = "2025-09-03T13:04:03.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/af/6593f6d21404e842007b40fdeb81e73c20b6649b82d020bb0801b270174c/django-5.2.6-py3-none-any.whl", hash = "sha256:60549579b1174a304b77e24a93d8d9fafe6b6c03ac16311f3e25918ea5a20058", size = 8303111, upload-time = "2025-09-03T13:03:47.808Z" }, +] + +[[package]] +name = "django-environ" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804, upload-time = "2025-01-13T17:03:37.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957, upload-time = "2025-01-13T17:03:32.918Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastparquet" +version = "2024.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cramjam" }, + { name = "fsspec" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/66/862da14f5fde4eff2cedc0f51a8dc34ba145088e5041b45b2d57ac54f922/fastparquet-2024.11.0.tar.gz", hash = "sha256:e3b1fc73fd3e1b70b0de254bae7feb890436cb67e99458b88cb9bd3cc44db419", size = 467192, upload-time = "2024-11-15T19:30:10.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/51/e0d6e702523ac923ede6c05e240f4a02533ccf2cea9fec7a43491078e920/fastparquet-2024.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:374cdfa745aa7d5188430528d5841cf823eb9ad16df72ad6dadd898ccccce3be", size = 909934, upload-time = "2024-11-12T20:37:37.049Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c8/5c0fb644c19a8d80b2ae4d8aa7d90c2d85d0bd4a948c5c700bea5c2802ea/fastparquet-2024.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c8401bfd86cccaf0ab7c0ade58c91ae19317ff6092e1d4ad96c2178197d8124", size = 683844, upload-time = "2024-11-12T20:37:38.456Z" }, + { url = "https://files.pythonhosted.org/packages/33/4a/1e532fd1a0d4d8af7ffc7e3a8106c0bcd13ed914a93a61e299b3832dd3d2/fastparquet-2024.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9cca4c6b5969df5561c13786f9d116300db1ec22c7941e237cfca4ce602f59b", size = 1791698, upload-time = "2024-11-12T20:37:41.101Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e8/e1ede861bea68394a755d8be1aa2e2d60a3b9f6b551bfd56aeca74987e2e/fastparquet-2024.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a9387e77ac608d8978774caaf1e19de67eaa1386806e514dcb19f741b19cfe5", size = 1804289, upload-time = "2024-11-12T20:37:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/957090cccaede805583ca3f3e46e2762d0f9bf8860ecbce65197e47d84c1/fastparquet-2024.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6595d3771b3d587a31137e985f751b4d599d5c8e9af9c4858e373fdf5c3f8720", size = 1753638, upload-time = "2024-11-12T20:37:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/344787c685fd1531f07ae712a855a7c34d13deaa26c3fd4a9231bea7dbab/fastparquet-2024.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:053695c2f730b78a2d3925df7cd5c6444d6c1560076af907993361cc7accf3e2", size = 1814407, upload-time = "2024-11-12T20:37:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ec/ab9d5685f776a1965797eb68c4364c72edf57cd35beed2df49b34425d1df/fastparquet-2024.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a52eecc6270ae15f0d51347c3f762703dd667ca486f127dc0a21e7e59856ae5", size = 1874462, upload-time = "2024-11-12T20:37:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/90/4f/7a4ea9a7ddf0a3409873f0787f355806f9e0b73f42f2acecacdd9a8eff0a/fastparquet-2024.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:e29ff7a367fafa57c6896fb6abc84126e2466811aefd3e4ad4070b9e18820e54", size = 671023, upload-time = "2024-11-12T20:37:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/08/76/068ac7ec9b4fc783be21a75a6a90b8c0654da4d46934d969e524ce287787/fastparquet-2024.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dbad4b014782bd38b58b8e9f514fe958cfa7a6c4e187859232d29fd5c5ddd849", size = 915968, upload-time = "2024-11-12T20:37:52.861Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9e/6d3b4188ad64ed51173263c07109a5f18f9c84a44fa39ab524fca7420cda/fastparquet-2024.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:403d31109d398b6be7ce84fa3483fc277c6a23f0b321348c0a505eb098a041cb", size = 685399, upload-time = "2024-11-12T20:37:54.899Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6c/809220bc9fbe83d107df2d664c3fb62fb81867be8f5218ac66c2e6b6a358/fastparquet-2024.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbbb9057a26acf0abad7adf58781ee357258b7708ee44a289e3bee97e2f55d42", size = 1758557, upload-time = "2024-11-12T20:37:56.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/b3b3e6ca2e531484289024138cd4709c22512b3fe68066d7f9849da4a76c/fastparquet-2024.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63e0e416e25c15daa174aad8ba991c2e9e5b0dc347e5aed5562124261400f87b", size = 1781052, upload-time = "2024-11-12T20:37:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/21/fe/97ed45092d0311c013996dae633122b7a51c5d9fe8dcbc2c840dc491201e/fastparquet-2024.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2d7f02f57231e6c86d26e9ea71953737202f20e948790e5d4db6d6a1a150dc", size = 1715797, upload-time = "2024-11-12T20:38:00.694Z" }, + { url = "https://files.pythonhosted.org/packages/24/df/02fa6aee6c0d53d1563b5bc22097076c609c4c5baa47056b0b4bed456fcf/fastparquet-2024.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fbe4468146b633d8f09d7b196fea0547f213cb5ce5f76e9d1beb29eaa9593a93", size = 1795682, upload-time = "2024-11-12T20:38:02.38Z" }, + { url = "https://files.pythonhosted.org/packages/b0/25/f4f87557589e1923ee0e3bebbc84f08b7c56962bf90f51b116ddc54f2c9f/fastparquet-2024.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:29d5c718817bcd765fc519b17f759cad4945974421ecc1931d3bdc3e05e57fa9", size = 1857842, upload-time = "2024-11-12T20:38:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f9/98cd0c39115879be1044d59c9b76e8292776e99bb93565bf990078fd11c4/fastparquet-2024.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:74a0b3c40ab373442c0fda96b75a36e88745d8b138fcc3a6143e04682cbbb8ca", size = 673269, upload-time = "2024-12-11T21:22:48.073Z" }, + { url = "https://files.pythonhosted.org/packages/47/e3/e7db38704be5db787270d43dde895eaa1a825ab25dc245e71df70860ec12/fastparquet-2024.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:59e5c5b51083d5b82572cdb7aed0346e3181e3ac9d2e45759da2e804bdafa7ee", size = 912523, upload-time = "2024-11-12T20:38:06.003Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/e3387c99293dae441634e7724acaa425b27de19a00ee3d546775dace54a9/fastparquet-2024.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdadf7b6bad789125b823bfc5b0a719ba5c4a2ef965f973702d3ea89cff057f6", size = 683779, upload-time = "2024-11-12T20:38:07.442Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/d112d0573d086b578bf04302a502e9a7605ea8f1244a7b8577cd945eec78/fastparquet-2024.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46b2db02fc2a1507939d35441c8ab211d53afd75d82eec9767d1c3656402859b", size = 1751113, upload-time = "2024-11-12T20:38:09.36Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a7/040507cee3a7798954e8fdbca21d2dbc532774b02b882d902b8a4a6849ef/fastparquet-2024.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3afdef2895c9f459135a00a7ed3ceafebfbce918a9e7b5d550e4fae39c1b64d", size = 1780496, upload-time = "2024-11-12T20:38:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/bc/75/d0d9f7533d780ec167eede16ad88073ee71696150511126c31940e7f73aa/fastparquet-2024.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36b5c9bd2ffaaa26ff45d59a6cefe58503dd748e0c7fad80dd905749da0f2b9e", size = 1713608, upload-time = "2024-11-12T20:38:12.848Z" }, + { url = "https://files.pythonhosted.org/packages/30/fa/1d95bc86e45e80669c4f374b2ca26a9e5895a1011bb05d6341b4a7414693/fastparquet-2024.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b7df5d3b61a19d76e209fe8d3133759af1c139e04ebc6d43f3cc2d8045ef338", size = 1792779, upload-time = "2024-11-12T20:38:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/13/3d/c076beeb926c79593374c04662a9422a76650eef17cd1c8e10951340764a/fastparquet-2024.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b35823ac7a194134e5f82fa4a9659e42e8f9ad1f2d22a55fbb7b9e4053aabbb", size = 1851322, upload-time = "2024-11-12T20:38:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/1d0d47e64816002824d4a876644e8c65540fa23f91b701f0daa726931545/fastparquet-2024.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d20632964e65530374ff7cddd42cc06aa0a1388934903693d6d22592a5ba827b", size = 673266, upload-time = "2024-11-12T20:38:17.661Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e0/bab50af11c2d75c9c4a2a26a5254573c0bd97cea152254401510950486fa/fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19", size = 304847, upload-time = "2025-09-02T19:10:49.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, +] + +[[package]] +name = "gtfs-realtime-bindings" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/55/ed46db9267d2615851bfba3c22b6c0e7f88751efb5d5d1468291935c7f65/gtfs-realtime-bindings-1.0.0.tar.gz", hash = "sha256:2e8ced8904400cc93ab7e8520adb6934cfa601edacc6f593fc2cb4448662bb47", size = 6197, upload-time = "2023-02-23T17:53:20.8Z" } + +[[package]] +name = "gtfs-rt-pipeline" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "celery" }, + { name = "django" }, + { name = "django-environ" }, + { name = "fastparquet" }, + { name = "gtfs-realtime-bindings" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "psycopg", extra = ["binary", "pool"] }, + { name = "python-dotenv" }, + { name = "redis" }, + { name = "requests" }, + { name = "scikit-learn" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "ipython" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-django" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ipykernel" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'" }, + { name = "celery", specifier = ">=5.4" }, + { name = "django", specifier = ">=5.0" }, + { name = "django-environ", specifier = ">=0.11" }, + { name = "fastparquet", specifier = ">=2024.11.0" }, + { name = "gtfs-realtime-bindings", specifier = ">=1.0.0" }, + { name = "ipython", marker = "extra == 'dev'" }, + { name = "mypy", marker = "extra == 'dev'" }, + { name = "numpy", specifier = ">=2.3.4" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.2" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-django", marker = "extra == 'dev'" }, + { name = "python-dotenv", specifier = ">=1.0" }, + { name = "redis", specifier = ">=5.0" }, + { name = "requests", specifier = ">=2.32" }, + { name = "scikit-learn", specifier = ">=1.7.2" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [{ name = "ipykernel", specifier = ">=7.1.0" }] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, +] + +[[package]] +name = "ipython" +version = "9.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/71/a86262bf5a68bf211bcc71fe302af7e05f18a2852fdc610a854d20d085e6/ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113", size = 4389137, upload-time = "2025-08-29T12:15:21.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72", size = 612426, upload-time = "2025-08-29T12:15:18.866Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "kombu" +version = "5.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, + { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, + { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, + { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, + { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, + { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, + { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "6.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, +] + +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +] + +[[package]] +name = "psycopg" +version = "3.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f1/0258a123c045afaf3c3b60c22ccff077bceeb24b8dc2c593270899353bd0/psycopg-3.2.10.tar.gz", hash = "sha256:0bce99269d16ed18401683a8569b2c5abd94f72f8364856d56c0389bcd50972a", size = 160380, upload-time = "2025-09-08T09:13:37.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/90/422ffbbeeb9418c795dae2a768db860401446af0c6768bc061ce22325f58/psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3", size = 206586, upload-time = "2025-09-08T09:07:50.121Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.10" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/8c/f15bd09a0cc09f010c1462f1cb846d7d2706f0f6226ef8e953328243edcc/psycopg_binary-3.2.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db0eb06a19e4c64a08db0db80875ede44939af6a2afc281762c338fad5d6e547", size = 4002654, upload-time = "2025-09-08T09:08:49.779Z" }, + { url = "https://files.pythonhosted.org/packages/c9/df/9b7c9db70b624b96544560d062c27030a817e932f1fa803b58e25b26dcdd/psycopg_binary-3.2.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d922fdd49ed17c558b6b2f9ae2054c3d0cced2a34e079ce5a41c86904d0203f7", size = 4074650, upload-time = "2025-09-08T09:08:57.53Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/7aba5874e1dfd90bc3dcd26dd9200ae65e1e6e169230759dad60139f1b99/psycopg_binary-3.2.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d557a94cd6d2e775b3af6cc0bd0ff0d9d641820b5cc3060ccf1f5ca2bf971217", size = 4630536, upload-time = "2025-09-08T09:09:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b1/a430d08b4eb28dc534181eb68a9c2a9e90b77c0e2933e338790534e7dce0/psycopg_binary-3.2.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:29b6bb87959515bc8b6abef10d8d23a9a681f03e48e9f0c8adb4b9fb7fa73f11", size = 4728387, upload-time = "2025-09-08T09:09:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d4/26d0fa9e8e7c05f0338024d2822a3740fac6093999443ad54e164f154bcc/psycopg_binary-3.2.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b29285474e3339d0840e1b5079fdb0481914108f92ec62de0c87ae333c60b24", size = 4413805, upload-time = "2025-09-08T09:09:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/d05c037c02e2ac4cb1c5b895c6c82428b3eaa0c48d08767b771bc2ea155a/psycopg_binary-3.2.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:62590dd113d10cd9c08251cb80b32e2e8aaf01ece04a700322e776b1d216959f", size = 3886830, upload-time = "2025-09-08T09:09:18.102Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/db3dee4335cd80c56e173a5ffbda6d17a7a10eeed030378d9adf3ab19ea7/psycopg_binary-3.2.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:764a5b9b40ad371c55dfdf95374d89e44a82fd62272d4fceebea0adb8930e2fb", size = 3568543, upload-time = "2025-09-08T09:09:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/1b/45/4117274f24b8d49b8a9c1cb60488bb172ac9e57b8f804726115c332d16f8/psycopg_binary-3.2.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bd3676a04970cf825d2c771b0c147f91182c5a3653e0dbe958e12383668d0f79", size = 3610614, upload-time = "2025-09-08T09:09:27.534Z" }, + { url = "https://files.pythonhosted.org/packages/3c/22/f1b294dfc8af32a96a363aa99c0ebb530fc1c372a424c54a862dcf77ef47/psycopg_binary-3.2.10-cp311-cp311-win_amd64.whl", hash = "sha256:646048f46192c8d23786cc6ef19f35b7488d4110396391e407eca695fdfe9dcd", size = 2888340, upload-time = "2025-09-08T09:09:32.696Z" }, + { url = "https://files.pythonhosted.org/packages/a6/34/91c127fdedf8b270b1e3acc9f849d07ee8b80194379590c6f48dcc842924/psycopg_binary-3.2.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dee2f4d2adc9adacbfecf8254bd82f6ac95cff707e1b9b99aa721cd1ef16b47", size = 3983963, upload-time = "2025-09-08T09:09:38.454Z" }, + { url = "https://files.pythonhosted.org/packages/1e/03/1d10ce2bf70cf549a8019639dc0c49be03e41092901d4324371a968b8c01/psycopg_binary-3.2.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b45e65383da9c4a42a56f817973e521e893f4faae897fe9f1a971f9fe799742", size = 4069171, upload-time = "2025-09-08T09:09:44.395Z" }, + { url = "https://files.pythonhosted.org/packages/4c/5e/39cb924d6e119145aa5fc5532f48e79c67e13a76675e9366c327098db7b5/psycopg_binary-3.2.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:484d2b1659afe0f8f1cef5ea960bb640e96fa864faf917086f9f833f5c7a8034", size = 4610780, upload-time = "2025-09-08T09:09:53.073Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/5a1282ebc4e39f5890abdd4bb7edfe9d19e4667497a1793ad288a8b81826/psycopg_binary-3.2.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3bb4046973264ebc8cb7e20a83882d68577c1f26a6f8ad4fe52e4468cd9a8eee", size = 4700479, upload-time = "2025-09-08T09:09:58.183Z" }, + { url = "https://files.pythonhosted.org/packages/af/7a/e1c06e558ca3f37b7e6b002e555ebcfce0bf4dee6f3ae589a7444e16ce17/psycopg_binary-3.2.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14bcbcac0cab465d88b2581e43ec01af4b01c9833e663f1352e05cb41be19e44", size = 4391772, upload-time = "2025-09-08T09:10:04.406Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d6/56f449c86988c9a97dc6c5f31d3689cfe8aedb37f2a02bd3e3882465d385/psycopg_binary-3.2.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bb7f665587dfd79e69f48b34efe226149454d7aab138ed22d5431d703de2f6", size = 3858214, upload-time = "2025-09-08T09:10:09.693Z" }, + { url = "https://files.pythonhosted.org/packages/93/56/f9eed67c9a1701b1e315f3687ff85f2f22a0a7d0eae4505cff65ef2f2679/psycopg_binary-3.2.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2fe9eaa367f6171ab1a21a7dcb335eb2398be7f8bb7e04a20e2260aedc6f782", size = 3528051, upload-time = "2025-09-08T09:10:13.423Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/636709c72540cb859566537c0a03e46c3d2c4c4c2e13f78df46b6c4082b3/psycopg_binary-3.2.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:299834cce3eec0c48aae5a5207fc8f0c558fd65f2ceab1a36693329847da956b", size = 3580117, upload-time = "2025-09-08T09:10:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a8/a2c822fa06b0dbbb8ad4b0221da2534f77bac54332d2971dbf930f64be5a/psycopg_binary-3.2.10-cp312-cp312-win_amd64.whl", hash = "sha256:e037aac8dc894d147ef33056fc826ee5072977107a3fdf06122224353a057598", size = 2878872, upload-time = "2025-09-08T09:10:22.162Z" }, + { url = "https://files.pythonhosted.org/packages/3a/80/db840f7ebf948ab05b4793ad34d4da6ad251829d6c02714445ae8b5f1403/psycopg_binary-3.2.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55b14f2402be027fe1568bc6c4d75ac34628ff5442a70f74137dadf99f738e3b", size = 3982057, upload-time = "2025-09-08T09:10:28.725Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/39308328bb8388b1ec3501a16128c5ada405f217c6d91b3d921b9f3c5604/psycopg_binary-3.2.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:43d803fb4e108a67c78ba58f3e6855437ca25d56504cae7ebbfbd8fce9b59247", size = 4066830, upload-time = "2025-09-08T09:10:34.083Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5a/18e6f41b40c71197479468cb18703b2999c6e4ab06f9c05df3bf416a55d7/psycopg_binary-3.2.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:470594d303928ab72a1ffd179c9c7bde9d00f76711d6b0c28f8a46ddf56d9807", size = 4610747, upload-time = "2025-09-08T09:10:39.697Z" }, + { url = "https://files.pythonhosted.org/packages/be/ab/9198fed279aca238c245553ec16504179d21aad049958a2865d0aa797db4/psycopg_binary-3.2.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a1d4e4d309049e3cb61269652a3ca56cb598da30ecd7eb8cea561e0d18bc1a43", size = 4700301, upload-time = "2025-09-08T09:10:44.715Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0d/59024313b5e6c5da3e2a016103494c609d73a95157a86317e0f600c8acb3/psycopg_binary-3.2.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a92ff1c2cd79b3966d6a87e26ceb222ecd5581b5ae4b58961f126af806a861ed", size = 4392679, upload-time = "2025-09-08T09:10:49.106Z" }, + { url = "https://files.pythonhosted.org/packages/ff/47/21ef15d8a66e3a7a76a177f885173d27f0c5cbe39f5dd6eda9832d6b4e19/psycopg_binary-3.2.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac0365398947879c9827b319217096be727da16c94422e0eb3cf98c930643162", size = 3857881, upload-time = "2025-09-08T09:10:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/af/35/c5e5402ccd40016f15d708bbf343b8cf107a58f8ae34d14dc178fdea4fd4/psycopg_binary-3.2.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:42ee399c2613b470a87084ed79b06d9d277f19b0457c10e03a4aef7059097abc", size = 3531135, upload-time = "2025-09-08T09:11:03.346Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e2/9b82946859001fe5e546c8749991b8b3b283f40d51bdc897d7a8e13e0a5e/psycopg_binary-3.2.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2028073fc12cd70ba003309d1439c0c4afab4a7eee7653b8c91213064fffe12b", size = 3581813, upload-time = "2025-09-08T09:11:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/c5/91/c10cfccb75464adb4781486e0014ecd7c2ad6decf6cbe0afd8db65ac2bc9/psycopg_binary-3.2.10-cp313-cp313-win_amd64.whl", hash = "sha256:8390db6d2010ffcaf7f2b42339a2da620a7125d37029c1f9b72dfb04a8e7be6f", size = 2881466, upload-time = "2025-09-08T09:11:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/fd/89/b0702ba0d007cc787dd7a205212c8c8cae229d1e7214c8e27bdd3b13d33e/psycopg_binary-3.2.10-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b34c278a58aa79562afe7f45e0455b1f4cad5974fc3d5674cc5f1f9f57e97fc5", size = 3981253, upload-time = "2025-09-08T09:11:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/e51ac72ac34d1d8ea7fd861008ad8de60e56997f5bd3fbae7536570f6f58/psycopg_binary-3.2.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810f65b9ef1fe9dddb5c05937884ea9563aaf4e1a2c3d138205231ed5f439511", size = 4067542, upload-time = "2025-09-08T09:11:25.366Z" }, + { url = "https://files.pythonhosted.org/packages/d6/27/49625c79ae89959a070c1fb63ebb5c6eed426fa09e15086b6f5b626fcdc2/psycopg_binary-3.2.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8923487c3898c65e1450847e15d734bb2e6adbd2e79d2d1dd5ad829a1306bdc0", size = 4615338, upload-time = "2025-09-08T09:11:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0d/9fdb5482f50f56303770ea8a3b1c1f32105762da731c7e2a4f425e0b3887/psycopg_binary-3.2.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7950ff79df7a453ac8a7d7a74694055b6c15905b0a2b6e3c99eb59c51a3f9bf7", size = 4703401, upload-time = "2025-09-08T09:11:38.718Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f3/eb2f75ca2c090bf1d0c90d6da29ef340876fe4533bcfc072a9fd94dd52b4/psycopg_binary-3.2.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c2b95e83fda70ed2b0b4fadd8538572e4a4d987b721823981862d1ab56cc760", size = 4393458, upload-time = "2025-09-08T09:11:44.114Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/887abe0591b2f1c1af31164b9efb46c5763e4418f403503bc9fbddaa02ef/psycopg_binary-3.2.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20384985fbc650c09a547a13c6d7f91bb42020d38ceafd2b68b7fc4a48a1f160", size = 3863733, upload-time = "2025-09-08T09:11:49.237Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/9446e3a84187220a98657ef778518f9b44eba55b1f6c3e8300d229ec9930/psycopg_binary-3.2.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1f6982609b8ff8fcd67299b67cd5787da1876f3bb28fedd547262cfa8ddedf94", size = 3535121, upload-time = "2025-09-08T09:11:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/f0382c956bfaa951a0dbd4d5a354acf093ef7e5219996958143dfd2bf37d/psycopg_binary-3.2.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bf30dcf6aaaa8d4779a20d2158bdf81cc8e84ce8eee595d748a7671c70c7b890", size = 3584235, upload-time = "2025-09-08T09:12:01.118Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dd/464bd739bacb3b745a1c93bc15f20f0b1e27f0a64ec693367794b398673b/psycopg_binary-3.2.10-cp314-cp314-win_amd64.whl", hash = "sha256:d5c6a66a76022af41970bf19f51bc6bf87bd10165783dd1d40484bfd87d6b382", size = 2973554, upload-time = "2025-09-08T09:12:05.884Z" }, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/13/1e7850bb2c69a63267c3dbf37387d3f71a00fd0e2fa55c5db14d64ba1af4/psycopg_pool-3.2.6.tar.gz", hash = "sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5", size = 29770, upload-time = "2025-02-26T12:03:47.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252, upload-time = "2025-02-26T12:03:45.073Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "pytokens" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/5f/e959a442435e24f6fb5a01aec6c657079ceaca1b3baf18561c3728d681da/pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044", size = 12171, upload-time = "2025-02-19T14:51:22.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e5/63bed382f6a7a5ba70e7e132b8b7b8abbcf4888ffa6be4877698dcfbed7d/pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b", size = 12046, upload-time = "2025-02-19T14:51:18.694Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "redis" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/ef/37ed4b213d64b48422df92560af7300e10fe30b5d665dd79932baebee0c6/scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92", size = 36619956, upload-time = "2025-09-11T17:39:20.5Z" }, + { url = "https://files.pythonhosted.org/packages/85/ab/5c2eba89b9416961a982346a4d6a647d78c91ec96ab94ed522b3b6baf444/scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e", size = 28931117, upload-time = "2025-09-11T17:39:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/eed51ab64d227fe60229a2d57fb60ca5898cfa50ba27d4f573e9e5f0b430/scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173", size = 20921997, upload-time = "2025-09-11T17:39:34.892Z" }, + { url = "https://files.pythonhosted.org/packages/be/7c/33ea3e23bbadde96726edba6bf9111fb1969d14d9d477ffa202c67bec9da/scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d", size = 23523374, upload-time = "2025-09-11T17:39:40.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/7399dc96e1e3f9a05e258c98d716196a34f528eef2ec55aad651ed136d03/scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2", size = 33583702, upload-time = "2025-09-11T17:39:49.011Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/a5c75095089b96ea72c1bd37a4497c24b581ec73db4ef58ebee142ad2d14/scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9", size = 35883427, upload-time = "2025-09-11T17:39:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/ab/66/e25705ca3d2b87b97fe0a278a24b7f477b4023a926847935a1a71488a6a6/scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3", size = 36212940, upload-time = "2025-09-11T17:40:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fd/0bb911585e12f3abdd603d721d83fc1c7492835e1401a0e6d498d7822b4b/scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88", size = 38865092, upload-time = "2025-09-11T17:40:15.143Z" }, + { url = "https://files.pythonhosted.org/packages/d6/73/c449a7d56ba6e6f874183759f8483cde21f900a8be117d67ffbb670c2958/scipy-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa", size = 38687626, upload-time = "2025-09-11T17:40:24.041Z" }, + { url = "https://files.pythonhosted.org/packages/68/72/02f37316adf95307f5d9e579023c6899f89ff3a051fa079dbd6faafc48e5/scipy-1.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c", size = 25503506, upload-time = "2025-09-11T17:40:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d", size = 36646259, upload-time = "2025-09-11T17:40:39.329Z" }, + { url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371", size = 28888976, upload-time = "2025-09-11T17:40:46.82Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0", size = 20879905, upload-time = "2025-09-11T17:40:52.545Z" }, + { url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232", size = 23553066, upload-time = "2025-09-11T17:40:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1", size = 33336407, upload-time = "2025-09-11T17:41:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f", size = 35673281, upload-time = "2025-09-11T17:41:15.055Z" }, + { url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef", size = 36004222, upload-time = "2025-09-11T17:41:23.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1", size = 38664586, upload-time = "2025-09-11T17:41:31.021Z" }, + { url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e", size = 38550641, upload-time = "2025-09-11T17:41:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851", size = 25456070, upload-time = "2025-09-11T17:41:41.3Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" }, + { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" }, + { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9f/b62587029980378304ba5a8563d376c96f40b1e133daacee76efdcae32de/scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff", size = 39360061, upload-time = "2025-09-11T17:45:09.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/7a2f1609921352c7fbee0815811b5050582f67f19983096c4769867ca45f/scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d", size = 26126914, upload-time = "2025-09-11T17:45:14.73Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b4/5c18a766e8353015439f3780f5fc473f36f9762edc1a2e45da3ff5a31b21/scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9", size = 39457455, upload-time = "2025-09-11T17:44:58.899Z" }, + { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] diff --git a/eta_prediction/models/README.md b/eta_prediction/models/README.md new file mode 100644 index 0000000..263b601 --- /dev/null +++ b/eta_prediction/models/README.md @@ -0,0 +1,162 @@ +# ETA Prediction Models + +Comprehensive modeling stack that ingests feature-engineered GTFS-RT datasets, trains multiple ETA estimators, evaluates them with consistent metrics, and persists artifacts to a registry for online or batch inference. + +--- + +## Architecture & Workflow + +1. **Dataset management** – `common/data.py` loads `datasets/*.parquet`, performs cleaning, and produces temporal or route-based splits. All models rely on `ETADataset.clean_data`, `temporal_split`, and `route_split` to prevent leakage and keep outliers consistent. +2. **Training entry points** – Each model family exposes a `train_*` routine (e.g., `historical_mean/train.py`, `polyreg_time/train.py`, `xgb/train.py`) that handles filtering, splitting, fitting, and evaluation. +3. **Evaluation & metrics** – `common/metrics.py` computes the canonical metric suite (MAE/RMSE/R²/bias/coverage). `evaluation/leaderboard.py` and `evaluation/roll_validate.py` enable cross-model comparison and walk-forward validation over time. +4. **Registry** – `common/registry.py` persists `{model_key}.pkl` artifacts plus `{model_key}_meta.json` metadata while maintaining `trained/registry.json`. `common/keys.py` standardizes identifiers (dataset, feature groups, scope) for reproducibility. +5. **Inference** – Each model directory contains a `predict.py` that loads registry artifacts, validates required features, and formats outputs for downstream services. + +--- + +## Shared Utilities (`models/common/`) + +| Module | Responsibilities | +| --- | --- | +| `data.py` | Defines `ETADataset`, cleaning rules (drop missing targets, enforce distance thresholds), splits (`temporal_split`, `route_split`), and feature-selection helpers such as `prepare_features_target`. | +| `keys.py` | Generates descriptive model keys and experiment identifiers (dataset, feature groups, version, route scope) and parses keys to recover metadata when routing predictions. | +| `metrics.py` | Implements MAE/RMSE/MAPE/quantile/bias plus `compute_all_metrics`, segmentation tooling (`error_analysis`), and prediction intervals from residuals. | +| `registry.py` | Saves/loads pickled estimators + JSON metadata, lists/filter models, selects best models per metric/route, and handles deletion/cleanup. | +| `utils.py` | Logging setup, clipping/smoothing helpers, lag feature generators, formatted metric tables, and convenience functions like `train_test_summary`. | + +--- + +## Model Families + +### Historical Mean (`historical_mean/`) +- Groups ETAs by configurable dimensions (default `['route_id', 'stop_sequence', 'hour']`) and stores lookup tables with coverage metrics. +- Training pipeline filters datasets, performs temporal splits, and reports validation/test performance before saving to the registry. +- Prediction API reports whether the output was backed by historical data or fell back to the global mean, making it ideal for baseline comparisons and cold-start monitoring. + +### Polynomial Regression – Distance (`polyreg_distance/`) +- Fits Ridge-regularized polynomial features on `distance_to_stop`. Supports route-specific models (`route_specific=True`) with an optional global fallback. +- Metadata records polynomial degree, regularization strength, and coefficient samples for transparency. +- Prediction helper exposes coefficients and supports batch inference with automatic route routing. + +### Polynomial Regression – Time Enhanced (`polyreg_time/`) +- Extends distance regression with optional temporal (`hour`, `day_of_week`, `is_peak_hour`, etc.), spatial (segment progress + identifiers), and weather inputs. +- Uses `ColumnTransformer` + `PolynomialFeatures` for distance and `StandardScaler` for dense features, with configurable NaN strategies (`drop`, `impute`, or strict `error`). +- Provides coefficient-based feature importance summaries to highlight influential covariates per dataset/route. + +### Exponentially Weighted Moving Average (`ewma/`) +- Maintains streaming EWMA statistics per `(route_id, stop_sequence [, hour])` key with configurable `alpha` and minimum observation thresholds. +- Offers online learning through `predict_and_update`, enabling real-time adaptation when ground truth becomes available. +- Ideal for highly non-stationary congestion patterns where recency outweighs historical aggregates. + +### XGBoost Gradient Boosted Trees (`xgb/`) +- `XGBTimeModel` mirrors the feature-flag system from the time polynomial model (temporal/spatial toggles) but leverages `xgboost.XGBRegressor` for nonlinear interactions. +- Cleans datasets using the same missing-value audits, exposes feature importance from the trained booster, and supports hyper-parameter tuning (`max_depth`, `n_estimators`, `learning_rate`, subsampling knobs). +- Prediction API aligns with the polynomial time model so switching model keys requires no payload changes. + +--- + +## Training Orchestration (`train_all_models.py`) + +`python models/train_all_models.py --dataset sample_dataset [--by-route] [--models ...] [--no-save]` + +- Loads the dataset once, optionally filtered by route, and delegates to each `train_*` routine. Default models: historical mean, distance polyreg, time polyreg, EWMA, and XGBoost. +- **Global mode** – trains one model per type and prints MAE/RMSE/R² summaries. +- **Route-specific mode** – iterates each route present in the dataset, trains the selected model types, and prints per-route performance plus correlations between trip volume and error. +- Pass `--no-save` for dry runs; otherwise results land in the registry with enriched metadata (sample counts, coverage, and configurations). + +--- + +## Evaluation Toolkit (`models/evaluation/`) + +- **Leaderboard (`leaderboard.py`)** – Loads trained models from the registry, evaluates each on a consistent test split (temporal or route-based), and prints a ranked table with MAE/RMSE/R², coverage, and bias. Use `quick_compare([...], dataset)` for one-liners. +- **Rolling validation (`roll_validate.py`)** – Implements walk-forward validation across sliding temporal windows to measure stability over time. Accepts custom `train_fn`/`predict_fn` callables so any model type can be stress-tested. +- **Plotting helpers** – Optional Matplotlib visualizations to inspect MAE drift, coverage trends, or metric distributions across windows. + +--- + +## Model Registry & Keys + +- `ModelKey.generate(...)` assembles identifiers of the form `polyreg_time_sample_temporal-spatial_global_20250126_143022_degree=2`. Keys capture dataset, feature groups, scope (global vs. `route_{id}`), timestamp, and supplemental hyper-parameters to simplify filtering and reproducibility. +- `ModelRegistry.save_model` writes `{model_key}.pkl` (pickled estimator) and `{model_key}_meta.json` (config + metrics) while updating `trained/registry.json`. Metadata contains dataset info, route scope, sample counts, evaluation results, and custom training attributes. +- Consumers can: + ```python + from models.common.registry import get_registry + registry = get_registry() + df = registry.list_models(model_type='polyreg_time') + best_key = registry.get_best_model(metric='test_mae_seconds', route_id='global') + model = registry.load_model(best_key) + meta = registry.load_metadata(best_key) + ``` +- `check_registry.py` provides diagnostics (permissions, file counts, distribution of model types) and an optional save/load smoke test. + +--- + +## Prediction Interfaces + +Each model package ships with a `predict.py` tailored to its feature requirements: + +| Module | Required Inputs | Notes | +| --- | --- | --- | +| `historical_mean.predict.predict_eta` | `route_id`, `stop_sequence`, `hour` (+ optional weekday/peak flags) | Returns coverage flag and fallback status. | +| `polyreg_distance.predict.predict_eta` | `distance_to_stop` (+ `route_id` for route-specific models) | Exposes polynomial coefficients for transparency. | +| `polyreg_time.predict.predict_eta` | `distance_to_stop` plus any temporal/spatial/weather signals enabled during training | `features_used` documents the expected payload. | +| `ewma.predict.predict_eta` | `route_id`, `stop_sequence` (+ optional `hour`) | Supports `predict_and_update` for online learning. | +| `xgb.predict.predict_eta` | Same schema as the time polynomial model | Uses XGBoost’s native handling for missing optional fields. | + +Batch helpers (`batch_predict`) are available in every module when you need to score pandas DataFrames efficiently. + +--- + +## Example Workflow + +```python +from models.train_all_models import train_all_models +from models.common.registry import get_registry +from models.evaluation.leaderboard import quick_compare +from models.xgb.predict import predict_eta as predict_xgb + +# 1. Train baselines on pre-built parquet +train_all_models( + dataset_name="sample_dataset", + by_route=False, + model_types=["historical_mean", "polyreg_time", "xgboost"] +) + +# 2. Inspect registry and select best global model +registry = get_registry() +best_key = registry.get_best_model(metric="test_mae_seconds", route_id="global") + +# 3. Run inference +prediction = predict_xgb( + model_key=best_key, + distance_to_stop=1200, + hour=8, + is_peak_hour=True +) +print(prediction["eta_formatted"]) + +# 4. Compare candidates on a hold-out split +candidate_keys = registry.list_models().head(4)["model_key"].tolist() +leaderboard_df = quick_compare(candidate_keys, dataset_name="sample_dataset") +``` + +--- + +## Directory Structure + +``` +models/ +├── common/ # Dataset + registry + metric utilities +├── evaluation/ # Leaderboard + rolling validation + plotting +├── historical_mean/ # Baseline mean model (train/predict) +├── polyreg_distance/ # Distance-only polynomial regression +├── polyreg_time/ # Distance + temporal/spatial regression +├── ewma/ # Exponential smoothing model with online updates +├── xgb/ # Gradient-boosted tree regressor +├── train_all_models.py # CLI orchestrator +├── example_workflow.py # Import barrel + quick-start helpers +├── check_registry.py # Diagnostics for trained/ registry folder +└── trained/ # Auto-created artifacts (PKL + JSON + registry index) +``` + +Place datasets under `datasets/{name}.parquet`, run the feature-engineering builder beforehand, and execute the modeling scripts within the same Django/ORM environment as the ETA service so shared settings and caches are available. diff --git a/eta_prediction/models/__init__.py b/eta_prediction/models/__init__.py new file mode 100644 index 0000000..06633ae --- /dev/null +++ b/eta_prediction/models/__init__.py @@ -0,0 +1,196 @@ +""" +ETA Prediction Models Package + +Comprehensive modeling framework for transit ETA prediction. + +Example usage: + >>> from models import train_all_baselines, get_registry + >>> models = train_all_baselines("sample_dataset") + >>> registry = get_registry() + >>> best_key = registry.get_best_model() +""" + +__version__ = "1.0.0" +__author__ = "SIMOVI Team" + +# Core utilities +from .common.data import load_dataset, ETADataset, prepare_features_target +from .common.registry import get_registry, ModelRegistry +from .common.keys import ModelKey, PredictionKey +from .common.metrics import ( + compute_all_metrics, + mae_minutes, + rmse_minutes, + within_threshold +) +from .common.utils import ( + format_seconds, + haversine_distance, + clip_predictions, + setup_logging +) + +# Training functions +from .historical_mean.train import train_historical_mean, HistoricalMeanModel +from .polyreg_distance.train import train_polyreg_distance, PolyRegDistanceModel +from .polyreg_time.train import train_polyreg_time, PolyRegTimeModel +from .ewma.train import train_ewma, EWMAModel + +# Prediction functions +from .historical_mean.predict import predict_eta as predict_historical_mean +from .polyreg_distance.predict import predict_eta as predict_polyreg_distance +from .polyreg_time.predict import predict_eta as predict_polyreg_time +from .ewma.predict import predict_eta as predict_ewma + +# Evaluation +from .evaluation.leaderboard import ModelLeaderboard, quick_compare +from .evaluation.roll_validate import RollingValidator, quick_rolling_validate + +# Main training pipeline +# from .train_all_models import train_all_baselines, train_advanced_configurations +from .train_all_models import train_all_models + + +__all__ = [ + # Core utilities + 'load_dataset', + 'ETADataset', + 'prepare_features_target', + 'get_registry', + 'ModelRegistry', + 'ModelKey', + 'PredictionKey', + 'compute_all_metrics', + 'mae_minutes', + 'rmse_minutes', + 'within_threshold', + 'format_seconds', + 'haversine_distance', + 'clip_predictions', + 'setup_logging', + + # Models + 'HistoricalMeanModel', + 'PolyRegDistanceModel', + 'PolyRegTimeModel', + 'EWMAModel', + + # Training + 'train_historical_mean', + 'train_polyreg_distance', + 'train_polyreg_time', + 'train_ewma', + # 'train_all_baselines', + 'train_all_models' + 'train_advanced_configurations', + + # Prediction + 'predict_historical_mean', + 'predict_polyreg_distance', + 'predict_polyreg_time', + 'predict_ewma', + + # Evaluation + 'ModelLeaderboard', + 'quick_compare', + 'RollingValidator', + 'quick_rolling_validate', +] + + +# Package info +MODELS = { + 'historical_mean': { + 'description': 'Historical average baseline', + 'typical_mae': '2-4 minutes', + 'features': ['route_id', 'stop_sequence', 'temporal'] + }, + 'polyreg_distance': { + 'description': 'Polynomial regression on distance', + 'typical_mae': '1.5-3 minutes', + 'features': ['distance_to_stop'] + }, + 'polyreg_time': { + 'description': 'Polynomial regression with time features', + 'typical_mae': '1-2.5 minutes', + 'features': ['distance_to_stop', 'temporal', 'operational'] + }, + 'ewma': { + 'description': 'Exponentially weighted moving average', + 'typical_mae': '1.5-3 minutes', + 'features': ['route_id', 'stop_sequence', 'temporal'], + 'online_learning': True + } +} + + +def list_models(): + """List available model types.""" + print("\nAvailable Model Types:") + print("=" * 70) + for name, info in MODELS.items(): + print(f"\n{name}:") + print(f" Description: {info['description']}") + print(f" Typical MAE: {info['typical_mae']}") + print(f" Features: {', '.join(info['features'])}") + if info.get('online_learning'): + print(f" Online Learning: Yes") + print("\n" + "=" * 70) + + +def quick_start_guide(): + """Print quick start guide.""" + guide = """ + ETA Prediction Models - Quick Start + ==================================== + + 1. Train all baselines: + >>> from models import train_all_baselines + >>> results = train_all_baselines("sample_dataset") + + 2. Compare models: + >>> from models import quick_compare, get_registry + >>> registry = get_registry() + >>> model_keys = registry.list_models()['model_key'].tolist() + >>> comparison = quick_compare(model_keys) + + 3. Load and use best model: + >>> best_key = registry.get_best_model(metric='test_mae_seconds') + >>> model = registry.load_model(best_key) + >>> predictions = model.predict(your_data) + + 4. Make single prediction: + >>> from models import predict_polyreg_time + >>> result = predict_polyreg_time( + ... model_key=best_key, + ... distance_to_stop=1500.0, + ... hour=8, + ... is_peak_hour=True + ... ) + >>> print(f"ETA: {result['eta_formatted']}") + + For more details, see models/README.md + """ + print(guide) + + +# Auto-create directories +def _setup_directories(): + """Create necessary directories if they don't exist.""" + from pathlib import Path + base_dir = Path(__file__).parent + + dirs = [ + base_dir / 'trained', + base_dir.parent / 'datasets', + base_dir.parent / 'datasets' / 'metadata', + base_dir.parent / 'datasets' / 'production', + base_dir.parent / 'datasets' / 'experimental' + ] + + for dir_path in dirs: + dir_path.mkdir(parents=True, exist_ok=True) + + +# Run setup on import +_setup_directories() \ No newline at end of file diff --git a/eta_prediction/models/check_registry.py b/eta_prediction/models/check_registry.py new file mode 100644 index 0000000..01afcd7 --- /dev/null +++ b/eta_prediction/models/check_registry.py @@ -0,0 +1,198 @@ +""" +Diagnostic script to check model registry and troubleshoot saving issues. +""" + +import sys +from pathlib import Path +import json +import os + +sys.path.append(str(Path(__file__).parent)) + +from common.registry import get_registry + + +def check_registry_status(): + """Check registry status and list all models.""" + print("="*80) + print("MODEL REGISTRY DIAGNOSTICS".center(80)) + print("="*80 + "\n") + + # Get registry + registry = get_registry() + + # Check directory exists + print(f"Registry Location: {registry.base_dir}") + print(f"Directory exists: {os.path.exists(registry.base_dir)}") + print(f"Is writable: {os.access(registry.base_dir, os.W_OK)}") + + # List all files + if os.path.exists(registry.base_dir): + all_files = list(Path(registry.base_dir).rglob('*')) + print(f"\nTotal files in registry: {len(all_files)}") + + # Count by type + pkl_files = [f for f in all_files if f.suffix == '.pkl'] + json_files = [f for f in all_files if f.suffix == '.json'] + + print(f" - .pkl files: {len(pkl_files)}") + print(f" - .json files: {len(json_files)}") + + # Show directory structure + print("\nDirectory structure:") + for root, dirs, files in os.walk(registry.base_dir): + level = root.replace(str(registry.base_dir), '').count(os.sep) + indent = ' ' * 2 * level + print(f'{indent}{os.path.basename(root)}/') + subindent = ' ' * 2 * (level + 1) + for file in files[:10]: # Show first 10 files per dir + size_kb = os.path.getsize(os.path.join(root, file)) / 1024 + print(f'{subindent}{file} ({size_kb:.1f} KB)') + if len(files) > 10: + print(f'{subindent}... and {len(files) - 10} more files') + + # Get model list from registry + print("\n" + "="*80) + print("MODELS IN REGISTRY") + print("="*80 + "\n") + + models_df = registry.list_models() + + if models_df.empty: + print("❌ No models found in registry!") + print("\nPossible issues:") + print("1. Models not being saved (check save_model=True)") + print("2. Registry directory path incorrect") + print("3. Permission issues writing files") + print("4. Errors during model saving (check training logs)") + else: + print(f"✅ Found {len(models_df)} models\n") + print(models_df.to_string()) + + # Show details of most recent model + print("\n" + "="*80) + print("MOST RECENT MODEL DETAILS") + print("="*80 + "\n") + + latest = models_df.iloc[0] + model_key = latest['model_key'] + + print(f"Model Key: {model_key}") + print(f"Type: {latest['model_type']}") + print(f"Saved: {latest['saved_at']}") + print(f"Dataset: {latest['dataset']}") + + # Try to load metadata + try: + metadata_path = Path(registry.base_dir) / f"{model_key}_metadata.json" + if metadata_path.exists(): + with open(metadata_path) as f: + metadata = json.load(f) + print("\nMetadata:") + print(json.dumps(metadata, indent=2)) + except Exception as e: + print(f"\n⚠️ Could not load metadata: {e}") + + # Check model file + model_path = Path(registry.base_dir) / f"{model_key}.pkl" + if model_path.exists(): + size_mb = model_path.stat().st_size / (1024 * 1024) + print(f"\nModel file: {model_path.name} ({size_mb:.2f} MB)") + else: + print(f"\n⚠️ Model file not found: {model_path}") + + +def test_save_load(): + """Test saving and loading a simple model.""" + print("\n" + "="*80) + print("SAVE/LOAD TEST") + print("="*80 + "\n") + + registry = get_registry() + + # Create a dummy model + test_model = {"type": "test", "value": 42} + test_metadata = { + "test": True, + "model_type": "diagnostic_test", + "dataset": "test_dataset", + "metrics": {"mae": 1.0} + } + test_key = "test_model_diagnostic_123" + + # Try to save + print("Attempting to save test model...") + try: + registry.save_model(test_key, test_model, test_metadata) + print("✅ Save successful!") + + # Try to load + print("\nAttempting to load test model...") + loaded_model, loaded_metadata = registry.load_model(test_key) + + if loaded_model == test_model: + print("✅ Load successful! Model matches.") + else: + print("⚠️ Load successful but model doesn't match.") + print(f" Expected: {test_model}") + print(f" Got: {loaded_model}") + + # Clean up + print("\nCleaning up test files...") + model_path = Path(registry.base_dir) / f"{test_key}.pkl" + metadata_path = Path(registry.base_dir) / f"{test_key}_metadata.json" + + if model_path.exists(): + model_path.unlink() + if metadata_path.exists(): + metadata_path.unlink() + + print("✅ Test complete!") + + except Exception as e: + print(f"❌ Test failed: {e}") + import traceback + traceback.print_exc() + + +def check_model_type_distribution(): + """Show distribution of model types in registry.""" + registry = get_registry() + models_df = registry.list_models() + + if models_df.empty: + return + + print("\n" + "="*80) + print("MODEL TYPE DISTRIBUTION") + print("="*80 + "\n") + + type_counts = models_df['model_type'].value_counts() + + for model_type, count in type_counts.items(): + print(f"{model_type:30s} {count:3d} models") + + print(f"\n{'Total':30s} {len(models_df):3d} models") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Check model registry status") + parser.add_argument('--test', action='store_true', help='Run save/load test') + + args = parser.parse_args() + + # Always show status + check_registry_status() + + # Show distribution if models exist + check_model_type_distribution() + + # Optional test + if args.test: + test_save_load() + + print("\n" + "="*80) + print("DIAGNOSTICS COMPLETE".center(80)) + print("="*80) \ No newline at end of file diff --git a/eta_prediction/models/common/data.py b/eta_prediction/models/common/data.py new file mode 100644 index 0000000..a6a3788 --- /dev/null +++ b/eta_prediction/models/common/data.py @@ -0,0 +1,230 @@ +""" +Data loading and preprocessing utilities for ETA prediction models. +Handles dataset splitting, feature engineering, and train/test preparation. +""" + +import pandas as pd +import numpy as np +from pathlib import Path +from typing import Tuple, List, Optional, Dict +from datetime import datetime, timedelta + + +class ETADataset: + """ + Manages ETA prediction datasets with consistent preprocessing and splitting. + + Expected columns from VP dataset builder: + - Identifiers: trip_id, route_id, vehicle_id, stop_id, stop_sequence + - Position: vp_ts, vp_lat, vp_lon, vp_bearing + - Stop: stop_lat, stop_lon, distance_to_stop + - Target: actual_arrival, time_to_arrival_seconds + - Temporal: hour, day_of_week, is_weekend, is_holiday, is_peak_hour + - Operational: headway_seconds, current_speed_kmh + - Weather (optional): temperature_c, precipitation_mm, wind_speed_kmh + """ + + FEATURE_GROUPS = { + 'identifiers': ['trip_id', 'route_id', 'vehicle_id', 'stop_id', 'stop_sequence'], + 'position': ['vp_lat', 'vp_lon', 'vp_bearing', 'distance_to_stop'], + 'temporal': ['hour', 'day_of_week', 'is_weekend', 'is_holiday', 'is_peak_hour'], + 'operational': ['headway_seconds', 'current_speed_kmh'], + 'weather': ['temperature_c', 'precipitation_mm', 'wind_speed_kmh'], + 'target': ['time_to_arrival_seconds'] + } + + def __init__(self, data_path: str): + """ + Initialize dataset from parquet file. + + Args: + data_path: Path to parquet file from VP dataset builder + """ + self.data_path = Path(data_path) + self.df = pd.read_parquet(data_path) + self.df['vp_ts'] = pd.to_datetime(self.df['vp_ts']) + + # Store original size + self.original_size = len(self.df) + + def clean_data(self, + drop_missing_target: bool = True, + max_eta_seconds: float = 3600 * 2, # 2 hours + min_distance: float = 10.0) -> 'ETADataset': + """ + Clean dataset by removing invalid rows. + + Args: + drop_missing_target: Remove rows without valid ETA target + max_eta_seconds: Maximum reasonable ETA (filter outliers) + min_distance: Minimum distance to stop (meters) to keep + + Returns: + Self for chaining + """ + initial_rows = len(self.df) + + if drop_missing_target: + self.df = self.df.dropna(subset=['time_to_arrival_seconds']) + + # Filter outliers + self.df = self.df[ + (self.df['time_to_arrival_seconds'] >= 0) & + (self.df['time_to_arrival_seconds'] <= max_eta_seconds) + ] + + # Filter too-close stops (likely already passed) + if 'distance_to_stop' in self.df.columns: + self.df = self.df[self.df['distance_to_stop'] >= min_distance] + + print(f"Cleaned: {initial_rows} → {len(self.df)} rows " + f"({100 * len(self.df) / initial_rows:.1f}% retained)") + + return self + + def get_features(self, feature_groups: List[str]) -> List[str]: + """ + Get list of feature columns from specified groups. + + Args: + feature_groups: List of group names (e.g., ['temporal', 'position']) + + Returns: + List of column names that exist in the dataset + """ + features = [] + for group in feature_groups: + if group in self.FEATURE_GROUPS: + features.extend(self.FEATURE_GROUPS[group]) + + # Return only columns that exist + return [f for f in features if f in self.df.columns] + + def temporal_split(self, + train_frac: float = 0.7, + val_frac: float = 0.15) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """ + Split dataset by time (avoids data leakage). + + Args: + train_frac: Fraction for training + val_frac: Fraction for validation (remainder goes to test) + + Returns: + train_df, val_df, test_df + """ + # Sort by timestamp + df_sorted = self.df.sort_values('vp_ts').reset_index(drop=True) + + n = len(df_sorted) + train_end = int(n * train_frac) + val_end = int(n * (train_frac + val_frac)) + + train_df = df_sorted.iloc[:train_end].copy() + val_df = df_sorted.iloc[train_end:val_end].copy() + test_df = df_sorted.iloc[val_end:].copy() + + print(f"Temporal split: train={len(train_df)}, val={len(val_df)}, test={len(test_df)}") + + return train_df, val_df, test_df + + def route_split(self, + test_routes: Optional[List[str]] = None, + test_frac: float = 0.2) -> Tuple[pd.DataFrame, pd.DataFrame]: + """ + Split by route for cross-route generalization testing. + + Args: + test_routes: Specific routes for test set, or None to sample + test_frac: Fraction of routes for testing if test_routes is None + + Returns: + train_df, test_df + """ + if test_routes is None: + routes = self.df['route_id'].unique() + n_test = max(1, int(len(routes) * test_frac)) + test_routes = np.random.choice(routes, size=n_test, replace=False) + + test_df = self.df[self.df['route_id'].isin(test_routes)].copy() + train_df = self.df[~self.df['route_id'].isin(test_routes)].copy() + + print(f"Route split: {len(test_routes)} test routes") + print(f" Train: {len(train_df)} samples") + print(f" Test: {len(test_df)} samples") + + return train_df, test_df + + def get_route_stats(self) -> pd.DataFrame: + """Get statistics per route.""" + return self.df.groupby('route_id').agg({ + 'time_to_arrival_seconds': ['count', 'mean', 'std'], + 'distance_to_stop': ['mean', 'std'], + 'trip_id': 'nunique' + }).round(2) + + def summary(self) -> Dict: + """Get dataset summary statistics.""" + return { + 'total_samples': len(self.df), + 'date_range': (self.df['vp_ts'].min(), self.df['vp_ts'].max()), + 'routes': self.df['route_id'].nunique(), + 'trips': self.df['trip_id'].nunique(), + 'vehicles': self.df['vehicle_id'].nunique(), + 'stops': self.df['stop_id'].nunique(), + 'eta_mean_minutes': self.df['time_to_arrival_seconds'].mean() / 60, + 'eta_std_minutes': self.df['time_to_arrival_seconds'].std() / 60, + 'missing_weather': self.df['temperature_c'].isna().sum() if 'temperature_c' in self.df.columns else 'N/A' + } + + +def load_dataset(dataset_name: str = "sample_dataset") -> ETADataset: + """ + Load dataset from datasets directory. + + Args: + dataset_name: Name of dataset (without .parquet extension) + + Returns: + ETADataset instance + """ + datasets_dir = Path(__file__).parent.parent.parent / "datasets" + data_path = datasets_dir / f"{dataset_name}.parquet" + + if not data_path.exists(): + raise FileNotFoundError(f"Dataset not found: {data_path}") + + return ETADataset(str(data_path)) + + +def prepare_features_target(df: pd.DataFrame, + feature_cols: List[str], + target_col: str = 'time_to_arrival_seconds', + fill_na: bool = True) -> Tuple[pd.DataFrame, pd.Series]: + """ + Extract features and target, handle missing values. + + Args: + df: DataFrame with features and target + feature_cols: List of feature column names + target_col: Name of target column + fill_na: Whether to fill NaN values + + Returns: + X (features), y (target) + """ + # Get features that exist + available_features = [f for f in feature_cols if f in df.columns] + + X = df[available_features].copy() + y = df[target_col].copy() + + if fill_na: + # Fill numeric features with median, boolean with False + for col in X.columns: + if X[col].dtype == 'bool': + X[col] = X[col].fillna(False) + elif X[col].dtype in ['int64', 'float64']: + X[col] = X[col].fillna(X[col].median()) + + return X, y \ No newline at end of file diff --git a/eta_prediction/models/common/keys.py b/eta_prediction/models/common/keys.py new file mode 100644 index 0000000..796b7f0 --- /dev/null +++ b/eta_prediction/models/common/keys.py @@ -0,0 +1,257 @@ +""" +Model key generation and identification utilities. +Ensures consistent naming and versioning across the modeling pipeline. +""" + +from typing import Dict, Optional +from datetime import datetime + + +class ModelKey: + """ + Generates unique, descriptive keys for trained models. + + Format: {model_type}_{dataset}_{features}_{route_id}_{timestamp} + Example: polyreg_distance_sample_temporal-position_route_1_20250126_143022 + polyreg_distance_sample_temporal-position_global_20250126_143022 + """ + + @staticmethod + def generate(model_type: str, + dataset_name: str, + feature_groups: list, + route_id: Optional[str] = None, + version: Optional[str] = None, + **kwargs) -> str: + """ + Generate a unique model key. + + Args: + model_type: Type of model (e.g., 'polyreg_distance', 'ewma') + dataset_name: Name of training dataset + feature_groups: List of feature group names used + route_id: Optional route ID for route-specific models + version: Optional version string, defaults to timestamp + **kwargs: Additional metadata to include in key + + Returns: + Unique model key string + """ + # Create features string + features_str = '-'.join(sorted(feature_groups)) + + # Create version string + if version is None: + version = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Base key parts + key_parts = [ + model_type, + dataset_name, + features_str + ] + + # Add route scope (route-specific or global) + if route_id is not None: + key_parts.append(f"route_{route_id}") + else: + key_parts.append("global") + + # Add version + key_parts.append(version) + + # Add optional kwargs + for k, v in sorted(kwargs.items()): + if v is not None: + key_parts.append(f"{k}={v}") + + return '_'.join(key_parts) + + @staticmethod + def parse(key: str) -> Dict[str, str]: + """ + Parse a model key back into components. + + Args: + key: Model key string + + Returns: + Dictionary with parsed components + """ + parts = key.split('_') + + if len(parts) < 5: + raise ValueError(f"Invalid model key format: {key}") + + parsed = { + 'model_type': parts[0], + 'dataset': parts[1], + 'features': parts[2], + } + + # Parse route scope + if parts[3] == 'route' and len(parts) >= 5: + # Route-specific model: ..._route_1_20250126_143022 + parsed['route_id'] = parts[4] + parsed['scope'] = 'route' + parsed['version'] = '_'.join(parts[5:7]) if len(parts) >= 7 else parts[5] + extra_start = 7 + elif parts[3] == 'global': + # Global model: ..._global_20250126_143022 + parsed['route_id'] = None + parsed['scope'] = 'global' + parsed['version'] = '_'.join(parts[4:6]) if len(parts) >= 6 else parts[4] + extra_start = 6 + else: + # Legacy format without scope + parsed['route_id'] = None + parsed['scope'] = 'global' + parsed['version'] = '_'.join(parts[3:5]) if len(parts) >= 5 else parts[3] + extra_start = 5 + + # Parse additional kwargs (key=value format) + if len(parts) > extra_start: + for part in parts[extra_start:]: + if '=' in part: + k, v = part.split('=', 1) + parsed[k] = v + + return parsed + + @staticmethod + def is_route_specific(key: str) -> bool: + """ + Check if a model key is route-specific. + + Args: + key: Model key string + + Returns: + True if route-specific, False if global + """ + try: + parsed = ModelKey.parse(key) + return parsed.get('scope') == 'route' and parsed.get('route_id') is not None + except (ValueError, IndexError): + return False + + @staticmethod + def extract_route_id(key: str) -> Optional[str]: + """ + Extract route_id from a model key. + + Args: + key: Model key string + + Returns: + Route ID string or None if global model + """ + try: + parsed = ModelKey.parse(key) + return parsed.get('route_id') + except (ValueError, IndexError): + return None + + +class PredictionKey: + """ + Generates keys for prediction requests to enable caching and deduplication. + + Format: {route_id}_{stop_id}_{vp_hash} + """ + + @staticmethod + def generate(route_id: str, + stop_id: str, + vehicle_lat: float, + vehicle_lon: float, + timestamp: datetime, + stop_sequence: Optional[int] = None) -> str: + """ + Generate prediction key for a vehicle-stop pair. + + Args: + route_id: Route identifier + stop_id: Stop identifier + vehicle_lat: Vehicle latitude + vehicle_lon: Vehicle longitude + timestamp: Timestamp of vehicle position + stop_sequence: Optional stop sequence number + + Returns: + Prediction key string + """ + # Round coordinates to ~10m precision for caching + lat_rounded = round(vehicle_lat, 4) + lon_rounded = round(vehicle_lon, 4) + + # Create position hash + vp_hash = f"{lat_rounded},{lon_rounded}" + + # Build key + if stop_sequence is not None: + return f"{route_id}_{stop_id}_{stop_sequence}_{vp_hash}" + else: + return f"{route_id}_{stop_id}_{vp_hash}" + + +class ExperimentKey: + """ + Generates keys for experiments and model comparisons. + """ + + @staticmethod + def generate(experiment_name: str, + models: list, + dataset: str, + timestamp: Optional[str] = None) -> str: + """ + Generate experiment key. + + Args: + experiment_name: Name of experiment + models: List of model types being compared + dataset: Dataset name + timestamp: Optional timestamp, defaults to now + + Returns: + Experiment key string + """ + if timestamp is None: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + models_str = '-'.join(sorted(models)) + + return f"exp_{experiment_name}_{models_str}_{dataset}_{timestamp}" + + +def model_filename(model_key: str, extension: str = "pkl") -> str: + """ + Generate consistent filename for model artifacts. + + Args: + model_key: Model key from ModelKey.generate() + extension: File extension (pkl, joblib, json, etc.) + + Returns: + Filename string + """ + return f"{model_key}.{extension}" + + +def validate_model_key(key: str) -> bool: + """ + Validate that a string is a properly formatted model key. + + Args: + key: String to validate + + Returns: + True if valid, False otherwise + """ + try: + parsed = ModelKey.parse(key) + required = ['model_type', 'dataset', 'features', 'version'] + return all(k in parsed for k in required) + except (ValueError, IndexError): + return False \ No newline at end of file diff --git a/eta_prediction/models/common/metrics.py b/eta_prediction/models/common/metrics.py new file mode 100644 index 0000000..70814de --- /dev/null +++ b/eta_prediction/models/common/metrics.py @@ -0,0 +1,245 @@ +""" +Evaluation metrics for ETA prediction models. +Provides domain-specific metrics beyond standard regression metrics. +""" + +import numpy as np +import pandas as pd +from typing import Dict, Optional, Tuple +from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score + + +def mae_seconds(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """Mean Absolute Error in seconds.""" + return mean_absolute_error(y_true, y_pred) + + +def mae_minutes(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """Mean Absolute Error in minutes.""" + return mean_absolute_error(y_true, y_pred) / 60 + + +def rmse_seconds(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """Root Mean Squared Error in seconds.""" + return np.sqrt(mean_squared_error(y_true, y_pred)) + + +def rmse_minutes(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """Root Mean Squared Error in minutes.""" + return np.sqrt(mean_squared_error(y_true, y_pred)) / 60 + + +def mape(y_true: np.ndarray, y_pred: np.ndarray, epsilon: float = 1.0) -> float: + """ + Mean Absolute Percentage Error. + + Args: + y_true: True values + y_pred: Predicted values + epsilon: Small value to avoid division by zero + + Returns: + MAPE as percentage + """ + return 100 * np.mean(np.abs((y_true - y_pred) / (y_true + epsilon))) + + +def median_ae(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """Median Absolute Error in seconds (robust to outliers).""" + return np.median(np.abs(y_true - y_pred)) + + +def within_threshold(y_true: np.ndarray, + y_pred: np.ndarray, + threshold_seconds: float = 60) -> float: + """ + Fraction of predictions within threshold. + + Args: + y_true: True values in seconds + y_pred: Predicted values in seconds + threshold_seconds: Acceptable error threshold + + Returns: + Fraction of predictions within threshold (0-1) + """ + errors = np.abs(y_true - y_pred) + return np.mean(errors <= threshold_seconds) + + +def late_penalty_mae(y_true: np.ndarray, + y_pred: np.ndarray, + late_multiplier: float = 2.0) -> float: + """ + MAE with higher penalty for late predictions (user-centric). + + Args: + y_true: True values + y_pred: Predicted values + late_multiplier: Multiplier for underprediction errors + + Returns: + Weighted MAE + """ + errors = y_pred - y_true + weights = np.where(errors < 0, late_multiplier, 1.0) + return np.mean(np.abs(errors) * weights) + + +def quantile_error(y_true: np.ndarray, + y_pred: np.ndarray, + quantiles: list = [0.5, 0.9, 0.95]) -> Dict[str, float]: + """ + Absolute errors at different quantiles. + + Args: + y_true: True values + y_pred: Predicted values + quantiles: List of quantiles to compute + + Returns: + Dictionary mapping quantile to error + """ + errors = np.abs(y_true - y_pred) + return {f"q{int(q*100)}": np.quantile(errors, q) for q in quantiles} + + +def bias(y_true: np.ndarray, y_pred: np.ndarray) -> float: + """ + Mean bias (positive = overprediction, negative = underprediction). + + Returns: + Mean bias in seconds + """ + return np.mean(y_pred - y_true) + + +def compute_all_metrics(y_true: np.ndarray, + y_pred: np.ndarray, + prefix: str = "") -> Dict[str, float]: + """ + Compute comprehensive set of metrics. + + Args: + y_true: True values in seconds + y_pred: Predicted values in seconds + prefix: Optional prefix for metric names (e.g., "val_") + + Returns: + Dictionary of all metrics + """ + metrics = { + f"{prefix}mae_seconds": mae_seconds(y_true, y_pred), + f"{prefix}mae_minutes": mae_minutes(y_true, y_pred), + f"{prefix}rmse_seconds": rmse_seconds(y_true, y_pred), + f"{prefix}rmse_minutes": rmse_minutes(y_true, y_pred), + f"{prefix}mape": mape(y_true, y_pred), + f"{prefix}median_ae": median_ae(y_true, y_pred), + f"{prefix}bias_seconds": bias(y_true, y_pred), + f"{prefix}r2": r2_score(y_true, y_pred), + f"{prefix}within_60s": within_threshold(y_true, y_pred, 60), + f"{prefix}within_120s": within_threshold(y_true, y_pred, 120), + f"{prefix}within_300s": within_threshold(y_true, y_pred, 300), + f"{prefix}late_penalty_mae": late_penalty_mae(y_true, y_pred), + } + + # Add quantile errors + quantile_errs = quantile_error(y_true, y_pred) + for k, v in quantile_errs.items(): + metrics[f"{prefix}error_{k}"] = v + + return metrics + + +def compare_models(results: Dict[str, Dict[str, float]], + metric: str = "mae_seconds") -> pd.DataFrame: + """ + Compare multiple models on a metric. + + Args: + results: Dict mapping model_name -> metrics_dict + metric: Metric to compare (or 'all' for all metrics) + + Returns: + DataFrame with comparison + """ + if metric == 'all': + df = pd.DataFrame(results).T + else: + df = pd.DataFrame({ + 'model': list(results.keys()), + metric: [results[m].get(metric, np.nan) for m in results.keys()] + }) + df = df.sort_values(metric) + + return df + + +def error_analysis(y_true: np.ndarray, + y_pred: np.ndarray, + feature_df: Optional[pd.DataFrame] = None, + group_by: Optional[str] = None) -> pd.DataFrame: + """ + Analyze errors by different segments. + + Args: + y_true: True values + y_pred: Predicted values + feature_df: DataFrame with features for grouping + group_by: Column name to group by + + Returns: + DataFrame with error statistics per group + """ + errors = np.abs(y_true - y_pred) + + if feature_df is None or group_by is None: + # Overall statistics + return pd.DataFrame({ + 'count': [len(errors)], + 'mae': [np.mean(errors)], + 'median_ae': [np.median(errors)], + 'rmse': [np.sqrt(np.mean(errors**2))], + 'max_error': [np.max(errors)], + }) + + # Group-wise statistics + df = feature_df.copy() + df['error'] = errors + + stats = df.groupby(group_by)['error'].agg([ + ('count', 'count'), + ('mae', 'mean'), + ('median_ae', 'median'), + ('std', 'std'), + ('max', 'max') + ]).round(2) + + return stats.sort_values('mae', ascending=False) + + +def prediction_intervals(y_pred: np.ndarray, + residuals: np.ndarray, + confidence: float = 0.95) -> Tuple[np.ndarray, np.ndarray]: + """ + Compute prediction intervals based on residual distribution. + + Args: + y_pred: Point predictions + residuals: Training residuals (y_true - y_pred) + confidence: Confidence level (e.g., 0.95 for 95% interval) + + Returns: + lower_bound, upper_bound arrays + """ + alpha = 1 - confidence + lower_q = alpha / 2 + upper_q = 1 - alpha / 2 + + lower_percentile = np.percentile(residuals, lower_q * 100) + upper_percentile = np.percentile(residuals, upper_q * 100) + + lower_bound = y_pred + lower_percentile + upper_bound = y_pred + upper_percentile + + return lower_bound, upper_bound \ No newline at end of file diff --git a/eta_prediction/models/common/registry.py b/eta_prediction/models/common/registry.py new file mode 100644 index 0000000..f97c665 --- /dev/null +++ b/eta_prediction/models/common/registry.py @@ -0,0 +1,428 @@ +""" +Model registry for managing trained models and their metadata. +Provides save/load functionality with consistent structure. +""" + +import json +import os +import pickle +from pathlib import Path +from typing import Dict, Any, Optional, List, Union +from datetime import datetime +import pandas as pd + + +def _discover_project_root() -> Path: + """Try to locate the repository root by walking up for pyproject or .git.""" + path = Path(__file__).resolve() + for parent in [path.parent, *path.parents]: + if (parent / "pyproject.toml").exists() or (parent / ".git").exists(): + return parent + # Fallback to historical assumption (two levels up) + return path.parents[2] + + +PROJECT_ROOT = _discover_project_root() +DEFAULT_REGISTRY_DIR = PROJECT_ROOT / "models" / "trained" + + +def _find_existing_registry_dir() -> Path: + """ + Search upwards from the current working directory for an existing registry. + This allows running tools from nested folders (e.g., bytewax/) while still + reusing the canonical models/trained directory at the project root. + """ + cwd = Path.cwd().resolve() + search_roots = [cwd] + list(cwd.parents) + for root in search_roots: + candidate = root / "models" / "trained" + if (candidate / "registry.json").exists(): + return candidate.resolve() + return DEFAULT_REGISTRY_DIR.resolve() + + +class ModelRegistry: + """ + Manages model artifacts and metadata in a structured directory. + + Structure: + models/ + ├── trained/ + │ ├── {model_key}.pkl # Serialized model + │ └── {model_key}_meta.json # Model metadata + └── registry.json # Index of all models + """ + + def __init__(self, base_dir: Union[str, Path, None] = None): + """ + Initialize registry. + + Args: + base_dir: Base directory for model storage + """ + env_dir = os.getenv("MODEL_REGISTRY_DIR") + configured_dir = base_dir if base_dir is not None else env_dir + + if configured_dir is not None: + base_path = Path(configured_dir).expanduser() + if not base_path.is_absolute(): + base_path = (PROJECT_ROOT / base_path).resolve() + else: + base_path = base_path.resolve() + else: + base_path = _find_existing_registry_dir() + + self.base_dir = base_path + self.base_dir.mkdir(parents=True, exist_ok=True) + + self.registry_file = self.base_dir / "registry.json" + self._load_registry() + + def _load_registry(self): + """Load registry index from disk.""" + if self.registry_file.exists(): + with open(self.registry_file, 'r') as f: + self.registry = json.load(f) + else: + self.registry = {} + + def _save_registry(self): + """Save registry index to disk.""" + with open(self.registry_file, 'w') as f: + json.dump(self.registry, f, indent=2) + + def save_model(self, + model_key: str, + model: Any, + metadata: Dict[str, Any], + overwrite: bool = False) -> Path: + """ + Save model and metadata to registry. + + Args: + model_key: Unique model identifier + model: Trained model object (must be picklable) + metadata: Model metadata (training metrics, config, etc.) + overwrite: Whether to overwrite existing model + + Returns: + Path to saved model file + """ + model_path = self.base_dir / f"{model_key}.pkl" + meta_path = self.base_dir / f"{model_key}_meta.json" + + if model_path.exists() and not overwrite: + raise FileExistsError(f"Model {model_key} already exists. Set overwrite=True to replace.") + + # Save model artifact + with open(model_path, 'wb') as f: + pickle.dump(model, f) + + # Enrich metadata + metadata['model_key'] = model_key + metadata['saved_at'] = datetime.now().isoformat() + # Store registry paths relative to repo root for portability + relative_model_path = os.path.relpath(model_path, PROJECT_ROOT) + relative_meta_path = os.path.relpath(meta_path, PROJECT_ROOT) + metadata['model_path'] = relative_model_path + + # Save metadata + with open(meta_path, 'w') as f: + json.dump(metadata, f, indent=2) + + # Update registry + self.registry[model_key] = { + 'model_path': relative_model_path, + 'meta_path': relative_meta_path, + 'saved_at': metadata['saved_at'], + 'model_type': metadata.get('model_type', 'unknown'), + 'route_id': metadata.get('route_id'), # Track route scope + 'dataset': metadata.get('dataset', 'unknown') + } + self._save_registry() + + route_info = f" (route: {metadata.get('route_id')})" if metadata.get('route_id') else " (global)" + print(f"✓ Saved model: {model_key}{route_info}") + return model_path + + def load_model(self, model_key: str) -> Any: + """ + Load model from registry. + + Args: + model_key: Unique model identifier + + Returns: + Loaded model object + """ + if model_key not in self.registry: + raise KeyError(f"Model {model_key} not found in registry") + + model_path = Path(self.registry[model_key]['model_path']) + if not model_path.is_absolute(): + model_path = (PROJECT_ROOT / model_path).resolve() + + with open(model_path, 'rb') as f: + model = pickle.load(f) + + return model + + def load_metadata(self, model_key: str) -> Dict[str, Any]: + """ + Load model metadata. + + Args: + model_key: Unique model identifier + + Returns: + Metadata dictionary + """ + if model_key not in self.registry: + raise KeyError(f"Model {model_key} not found in registry") + + meta_path = Path(self.registry[model_key]['meta_path']) + if not meta_path.is_absolute(): + meta_path = (PROJECT_ROOT / meta_path).resolve() + + with open(meta_path, 'r') as f: + metadata = json.load(f) + + return metadata + + def list_models(self, + model_type: Optional[str] = None, + route_id: Optional[str] = None, + sort_by: str = 'saved_at') -> pd.DataFrame: + """ + List all models in registry. + + Args: + model_type: Filter by model type (e.g., 'polyreg_distance') + route_id: Filter by route_id (None for global models, 'all' for all models) + sort_by: Column to sort by + + Returns: + DataFrame with model information + """ + models = [] + + for key, info in self.registry.items(): + # Filter by model type + if model_type and info.get('model_type') != model_type: + continue + + # Filter by route_id + model_route_id = info.get('route_id') + if route_id is not None and route_id != 'all': + if route_id == 'global' and model_route_id is not None: + continue + elif route_id != 'global' and model_route_id != route_id: + continue + + # Load metadata for richer info + try: + meta = self.load_metadata(key) + models.append({ + 'model_key': key, + 'model_type': info.get('model_type', 'unknown'), + 'route_id': model_route_id or 'global', + 'saved_at': info['saved_at'], + 'dataset': meta.get('dataset', 'unknown'), + 'n_samples': meta.get('n_samples'), + 'mae_seconds': meta.get('metrics', {}).get('test_mae_seconds', None), + 'mae_minutes': meta.get('metrics', {}).get('test_mae_minutes', None), + 'rmse_seconds': meta.get('metrics', {}).get('test_rmse_seconds', None), + 'r2': meta.get('metrics', {}).get('test_r2', None), + }) + except Exception as e: + print(f"Warning: Could not load metadata for {key}: {e}") + models.append({ + 'model_key': key, + 'model_type': info.get('model_type', 'unknown'), + 'route_id': model_route_id or 'global', + 'saved_at': info['saved_at'], + }) + + df = pd.DataFrame(models) + if not df.empty and sort_by in df.columns: + df = df.sort_values(sort_by, ascending=False) + + return df + + def delete_model(self, model_key: str) -> bool: + """ + Delete model and metadata from registry. + + Args: + model_key: Unique model identifier + + Returns: + True if deleted successfully + """ + if model_key not in self.registry: + raise KeyError(f"Model {model_key} not found in registry") + + # Delete files + model_path = Path(self.registry[model_key]['model_path']) + meta_path = Path(self.registry[model_key]['meta_path']) + + if model_path.exists(): + model_path.unlink() + if meta_path.exists(): + meta_path.unlink() + + # Remove from registry + del self.registry[model_key] + self._save_registry() + + print(f"✓ Deleted model: {model_key}") + return True + + def get_best_model(self, + model_type: Optional[str] = None, + route_id: Optional[str] = None, + metric: str = 'test_mae_seconds', + minimize: bool = True) -> Optional[str]: + """ + Get best model by metric. + + Args: + model_type: Filter by model type + route_id: Filter by route_id (None = prefer route-specific if exists, else global) + metric: Metric to optimize (e.g., 'test_mae_seconds', 'test_rmse_seconds') + minimize: Whether to minimize (True) or maximize (False) metric + + Returns: + Model key of best model, or None if no models found + """ + candidates = [] + + for key in self.registry.keys(): + # Filter by model type + if model_type and self.registry[key].get('model_type') != model_type: + continue + + # Filter by route + model_route_id = self.registry[key].get('route_id') + + if route_id is not None: + # Explicit route requested + if route_id == 'global' and model_route_id is not None: + continue + elif route_id != 'global' and model_route_id != route_id: + continue + # If route_id is None, we'll prefer route-specific in the sorting logic + + try: + meta = self.load_metadata(key) + metric_value = meta.get('metrics', {}).get(metric) + + if metric_value is not None: + candidates.append({ + 'key': key, + 'metric_value': metric_value, + 'route_id': model_route_id, + 'is_route_specific': model_route_id is not None + }) + except Exception: + continue + + if not candidates: + return None + + # Sort by metric, with preference for route-specific when route_id is None + if route_id is None: + # Smart routing: prefer route-specific models if they exist + route_specific = [c for c in candidates if c['is_route_specific']] + global_models = [c for c in candidates if not c['is_route_specific']] + + # If route-specific models exist, use them; otherwise fall back to global + candidates_to_sort = route_specific if route_specific else global_models + else: + candidates_to_sort = candidates + + # Sort by metric value + candidates_to_sort.sort(key=lambda x: x['metric_value'], reverse=not minimize) + + return candidates_to_sort[0]['key'] if candidates_to_sort else None + + def get_routes(self, model_type: Optional[str] = None) -> List[str]: + """ + Get list of all routes that have trained models. + + Args: + model_type: Filter by model type + + Returns: + List of route IDs (excludes None/global) + """ + routes = set() + + for key, info in self.registry.items(): + if model_type and info.get('model_type') != model_type: + continue + + route_id = info.get('route_id') + if route_id is not None: + routes.add(route_id) + + return sorted(list(routes)) + + def compare_routes(self, + model_type: str, + metric: str = 'test_mae_minutes') -> pd.DataFrame: + """ + Compare model performance across routes. + + Args: + model_type: Model type to compare + metric: Metric to display + + Returns: + DataFrame with route comparison + """ + results = [] + + routes = self.get_routes(model_type) + + # Add global model if exists + global_key = self.get_best_model(model_type=model_type, route_id='global', metric=f'test_{metric}') + if global_key: + meta = self.load_metadata(global_key) + results.append({ + 'route_id': 'global', + 'n_samples': meta.get('n_samples'), + 'n_trips': meta.get('n_trips'), + metric: meta.get('metrics', {}).get(f'test_{metric}'), + 'model_key': global_key + }) + + # Add route-specific models + for route_id in routes: + best_key = self.get_best_model(model_type=model_type, route_id=route_id, metric=f'test_{metric}') + if best_key: + meta = self.load_metadata(best_key) + results.append({ + 'route_id': route_id, + 'n_samples': meta.get('n_samples'), + 'n_trips': meta.get('n_trips'), + metric: meta.get('metrics', {}).get(f'test_{metric}'), + 'model_key': best_key + }) + + df = pd.DataFrame(results) + if not df.empty: + df = df.sort_values('n_trips', ascending=False) + + return df + + +# Global registry instance +_registry = None + +def get_registry() -> ModelRegistry: + """Get or create global registry instance.""" + global _registry + if _registry is None: + _registry = ModelRegistry() + return _registry diff --git a/eta_prediction/models/common/utils.py b/eta_prediction/models/common/utils.py new file mode 100644 index 0000000..0f1566a --- /dev/null +++ b/eta_prediction/models/common/utils.py @@ -0,0 +1,276 @@ +""" +Utility functions for models package. +""" + +import numpy as np +import pandas as pd +from typing import Any, Dict, List, Optional +from datetime import datetime +import logging + + +def setup_logging(name: str = "eta_models", level: str = "INFO") -> logging.Logger: + """ + Setup consistent logging for models. + + Args: + name: Logger name + level: Logging level + + Returns: + Configured logger + """ + logger = logging.getLogger(name) + logger.setLevel(getattr(logging, level)) + + if not logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + + return logger + + +def safe_divide(numerator: np.ndarray, + denominator: np.ndarray, + fill_value: float = 0.0) -> np.ndarray: + """ + Safe division that handles division by zero. + + Args: + numerator: Numerator array + denominator: Denominator array + fill_value: Value to use when denominator is zero + + Returns: + Result array + """ + result = np.full_like(numerator, fill_value, dtype=float) + mask = denominator != 0 + result[mask] = numerator[mask] / denominator[mask] + return result + + +def clip_predictions(predictions: np.ndarray, + min_value: float = 0.0, + max_value: float = 7200.0) -> np.ndarray: + """ + Clip predictions to reasonable range. + + Args: + predictions: Raw predictions + min_value: Minimum ETA (seconds) + max_value: Maximum ETA (seconds, default 2 hours) + + Returns: + Clipped predictions + """ + return np.clip(predictions, min_value, max_value) + + +def add_lag_features(df: pd.DataFrame, + columns: List[str], + lags: List[int], + group_by: Optional[str] = None) -> pd.DataFrame: + """ + Add lagged features to dataframe. + + Args: + df: Input dataframe + columns: Columns to lag + lags: List of lag values (e.g., [1, 2, 3]) + group_by: Optional column to group by before lagging + + Returns: + DataFrame with lag features added + """ + df_copy = df.copy() + + for col in columns: + for lag in lags: + lag_col_name = f"{col}_lag{lag}" + + if group_by: + df_copy[lag_col_name] = df_copy.groupby(group_by)[col].shift(lag) + else: + df_copy[lag_col_name] = df_copy[col].shift(lag) + + return df_copy + + +def smooth_predictions(predictions: np.ndarray, + window_size: int = 3, + method: str = 'ewma', + alpha: float = 0.3) -> np.ndarray: + """ + Smooth predictions using moving average. + + Args: + predictions: Array of predictions + window_size: Window size for smoothing + method: 'mean', 'median', or 'ewma' + alpha: Alpha parameter for EWMA + + Returns: + Smoothed predictions + """ + if len(predictions) < window_size: + return predictions + + if method == 'mean': + return pd.Series(predictions).rolling(window_size, min_periods=1).mean().values + elif method == 'median': + return pd.Series(predictions).rolling(window_size, min_periods=1).median().values + elif method == 'ewma': + return pd.Series(predictions).ewm(alpha=alpha).mean().values + else: + raise ValueError(f"Unknown smoothing method: {method}") + + +def calculate_speed_kmh(distance_m: float, time_s: float) -> float: + """ + Calculate speed in km/h from distance and time. + + Args: + distance_m: Distance in meters + time_s: Time in seconds + + Returns: + Speed in km/h + """ + if time_s <= 0: + return 0.0 + return (distance_m / 1000) / (time_s / 3600) + + +def haversine_distance(lat1: float, lon1: float, + lat2: float, lon2: float) -> float: + """ + Calculate great-circle distance between two points. + + Args: + lat1, lon1: First point coordinates + lat2, lon2: Second point coordinates + + Returns: + Distance in meters + """ + R = 6371000 # Earth radius in meters + + phi1 = np.radians(lat1) + phi2 = np.radians(lat2) + delta_phi = np.radians(lat2 - lat1) + delta_lambda = np.radians(lon2 - lon1) + + a = np.sin(delta_phi / 2) ** 2 + \ + np.cos(phi1) * np.cos(phi2) * np.sin(delta_lambda / 2) ** 2 + c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) + + return R * c + + +def format_seconds(seconds: float) -> str: + """ + Format seconds as human-readable string. + + Args: + seconds: Time in seconds + + Returns: + Formatted string (e.g., "2m 30s", "1h 15m") + """ + if seconds < 60: + return f"{int(seconds)}s" + elif seconds < 3600: + minutes = int(seconds / 60) + secs = int(seconds % 60) + return f"{minutes}m {secs}s" + else: + hours = int(seconds / 3600) + minutes = int((seconds % 3600) / 60) + return f"{hours}h {minutes}m" + + +def print_metrics_table(metrics: Dict[str, float], title: str = "Metrics"): + """ + Pretty print metrics as a table. + + Args: + metrics: Dictionary of metric names to values + title: Table title + """ + print(f"\n{'='*50}") + print(f"{title:^50}") + print(f"{'='*50}") + + for name, value in metrics.items(): + if isinstance(value, float): + print(f"{name:.<40} {value:.4f}") + else: + print(f"{name:.<40} {value}") + + print(f"{'='*50}\n") + + +def train_test_summary(train_df: pd.DataFrame, + test_df: pd.DataFrame, + val_df: Optional[pd.DataFrame] = None): + """ + Print summary of train/test split. + + Args: + train_df: Training dataframe + test_df: Test dataframe + val_df: Optional validation dataframe + """ + print("\n" + "="*60) + print("DATASET SPLIT SUMMARY".center(60)) + print("="*60) + + print(f"\nTrain: {len(train_df):,} samples") + print(f" Date range: {train_df['vp_ts'].min()} to {train_df['vp_ts'].max()}") + print(f" Routes: {train_df['route_id'].nunique()}") + print(f" Mean ETA: {train_df['time_to_arrival_seconds'].mean()/60:.1f} min") + + if val_df is not None: + print(f"\nValidation: {len(val_df):,} samples") + print(f" Date range: {val_df['vp_ts'].min()} to {val_df['vp_ts'].max()}") + print(f" Routes: {val_df['route_id'].nunique()}") + print(f" Mean ETA: {val_df['time_to_arrival_seconds'].mean()/60:.1f} min") + + print(f"\nTest: {len(test_df):,} samples") + print(f" Date range: {test_df['vp_ts'].min()} to {test_df['vp_ts'].max()}") + print(f" Routes: {test_df['route_id'].nunique()}") + print(f" Mean ETA: {test_df['time_to_arrival_seconds'].mean()/60:.1f} min") + + print("="*60 + "\n") + + +def create_feature_importance_df(feature_names: List[str], + importances: np.ndarray, + top_n: int = 20) -> pd.DataFrame: + """ + Create sorted feature importance dataframe. + + Args: + feature_names: List of feature names + importances: Array of importance values + top_n: Number of top features to return + + Returns: + DataFrame sorted by importance + """ + df = pd.DataFrame({ + 'feature': feature_names, + 'importance': importances + }) + + df = df.sort_values('importance', ascending=False) + + if top_n: + df = df.head(top_n) + + return df \ No newline at end of file diff --git a/eta_prediction/models/evaluation/leaderboard.py b/eta_prediction/models/evaluation/leaderboard.py new file mode 100644 index 0000000..292a9f4 --- /dev/null +++ b/eta_prediction/models/evaluation/leaderboard.py @@ -0,0 +1,229 @@ +""" +Model leaderboard for comparing performance across models. +""" + +import pandas as pd +import numpy as np +from typing import List, Optional, Dict +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.registry import get_registry +from common.data import load_dataset +from common.metrics import compute_all_metrics + + +class ModelLeaderboard: + """ + Compare multiple models on standardized test sets. + """ + + def __init__(self): + self.registry = get_registry() + self.results = [] + + def evaluate_model(self, + model_key: str, + test_df: pd.DataFrame, + target_col: str = 'time_to_arrival_seconds') -> Dict: + """ + Evaluate single model on test set. + + Args: + model_key: Model identifier + test_df: Test dataframe + target_col: Target column name + + Returns: + Dictionary with metrics + """ + print(f"Evaluating {model_key}...") + + # Load model and metadata + model = self.registry.load_model(model_key) + metadata = self.registry.load_metadata(model_key) + + # Predict + y_true = test_df[target_col].values + y_pred = model.predict(test_df) + + # Compute metrics + metrics = compute_all_metrics(y_true, y_pred) + + # Add model info + result = { + 'model_key': model_key, + 'model_type': metadata.get('model_type', 'unknown'), + 'dataset': metadata.get('dataset', 'unknown'), + **metrics + } + + return result + + def compare_models(self, + model_keys: List[str], + dataset_name: str = "sample_dataset", + test_routes: Optional[List[str]] = None) -> pd.DataFrame: + """ + Compare multiple models on same test set. + + Args: + model_keys: List of model keys to compare + dataset_name: Dataset to evaluate on + test_routes: Specific routes for testing + + Returns: + DataFrame with comparison results + """ + print(f"\n{'='*60}") + print(f"MODEL LEADERBOARD".center(60)) + print(f"{'='*60}\n") + print(f"Dataset: {dataset_name}") + print(f"Models: {len(model_keys)}\n") + + # Load dataset + dataset = load_dataset(dataset_name) + dataset.clean_data() + + # Get test set + if test_routes: + _, test_df = dataset.route_split(test_routes=test_routes) + else: + _, _, test_df = dataset.temporal_split(train_frac=0.7, val_frac=0.15) + + print(f"Test set: {len(test_df)} samples\n") + + # Evaluate each model + results = [] + for model_key in model_keys: + try: + result = self.evaluate_model(model_key, test_df) + results.append(result) + except Exception as e: + print(f" Error evaluating {model_key}: {e}") + + # Create comparison dataframe + df = pd.DataFrame(results) + + # Sort by MAE (primary metric) + if 'mae_seconds' in df.columns: + df = df.sort_values('mae_seconds') + + self.results = results + + return df + + def print_leaderboard(self, + df: pd.DataFrame, + metrics: List[str] = ['mae_minutes', 'rmse_minutes', 'r2', + 'within_60s', 'bias_seconds']): + """ + Pretty print leaderboard. + + Args: + df: Results dataframe + metrics: Metrics to display + """ + print(f"\n{'='*80}") + print(f"LEADERBOARD RESULTS".center(80)) + print(f"{'='*80}\n") + + # Select columns + display_cols = ['model_type'] + [m for m in metrics if m in df.columns] + display_df = df[display_cols].copy() + + # Format numbers + for col in metrics: + if col in display_df.columns: + if col.startswith('within_'): + display_df[col] = (display_df[col] * 100).round(1).astype(str) + '%' + else: + display_df[col] = display_df[col].round(3) + + # Add rank + display_df.insert(0, 'rank', range(1, len(display_df) + 1)) + + print(display_df.to_string(index=False)) + print(f"\n{'='*80}\n") + + # Highlight winner + winner = df.iloc[0] + print(f"🏆 Best Model: {winner['model_type']}") + print(f" MAE: {winner['mae_minutes']:.3f} minutes") + print(f" RMSE: {winner['rmse_minutes']:.3f} minutes") + print(f" R²: {winner['r2']:.3f}") + + def model_comparison_summary(self, df: pd.DataFrame) -> str: + """ + Generate text summary of model comparison. + + Args: + df: Results dataframe + + Returns: + Summary string + """ + best = df.iloc[0] + worst = df.iloc[-1] + + improvement = (worst['mae_seconds'] - best['mae_seconds']) / worst['mae_seconds'] * 100 + + summary = f""" +Model Comparison Summary +======================== + +Total Models Evaluated: {len(df)} +Test Samples: {df.iloc[0].get('test_samples', 'N/A')} + +Best Model: {best['model_type']} + - MAE: {best['mae_minutes']:.2f} minutes + - RMSE: {best['rmse_minutes']:.2f} minutes + - R²: {best['r2']:.3f} + - Within 60s: {best['within_60s']*100:.1f}% + +Baseline (Worst): {worst['model_type']} + - MAE: {worst['mae_minutes']:.2f} minutes + +Improvement: {improvement:.1f}% reduction in MAE from baseline to best model. +""" + return summary + + +def quick_compare(model_keys: List[str], + dataset_name: str = "sample_dataset") -> pd.DataFrame: + """ + Quick comparison function. + + Args: + model_keys: List of model keys + dataset_name: Dataset name + + Returns: + Comparison dataframe + """ + leaderboard = ModelLeaderboard() + df = leaderboard.compare_models(model_keys, dataset_name) + leaderboard.print_leaderboard(df) + + return df + + +if __name__ == "__main__": + # Example: Compare all model types + + # You would need to train these first + model_keys = [ + "historical_mean_sample_dataset_temporal-route_20250126_143022", + "polyreg_distance_sample_dataset_distance_20250126_143022_degree=2", + "polyreg_time_sample_dataset_distance-operational-temporal_20250126_143022_degree=2", + "ewma_sample_dataset_temporal-route_20250126_143022_alpha=0_3" + ] + + # Run comparison + results_df = quick_compare(model_keys) + + # Save results + results_df.to_csv("models/leaderboard_results.csv", index=False) + print("\nResults saved to models/leaderboard_results.csv") \ No newline at end of file diff --git a/eta_prediction/models/evaluation/roll_validate.py b/eta_prediction/models/evaluation/roll_validate.py new file mode 100644 index 0000000..4df4837 --- /dev/null +++ b/eta_prediction/models/evaluation/roll_validate.py @@ -0,0 +1,260 @@ +""" +Rolling window (walk-forward) validation for time series models. +Evaluates model performance over time with realistic train/test splits. +""" + +import pandas as pd +import numpy as np +from typing import Dict, List, Callable, Optional +from datetime import timedelta +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.data import load_dataset +from common.metrics import compute_all_metrics +from common.utils import print_metrics_table + + +class RollingValidator: + """ + Perform rolling window validation on time series data. + """ + + def __init__(self, + train_window_days: int = 7, + test_window_days: int = 1, + step_days: int = 1): + """ + Initialize validator. + + Args: + train_window_days: Size of training window in days + test_window_days: Size of test window in days + step_days: Step size between windows + """ + self.train_window = timedelta(days=train_window_days) + self.test_window = timedelta(days=test_window_days) + self.step = timedelta(days=step_days) + + self.results = [] + + def validate(self, + dataset_name: str, + train_fn: Callable, + predict_fn: Callable, + target_col: str = 'time_to_arrival_seconds') -> pd.DataFrame: + """ + Perform rolling window validation. + + Args: + dataset_name: Dataset to validate on + train_fn: Function(train_df) -> model + predict_fn: Function(model, test_df) -> predictions + target_col: Target column name + + Returns: + DataFrame with results per window + """ + print(f"\n{'='*60}") + print(f"ROLLING WINDOW VALIDATION".center(60)) + print(f"{'='*60}\n") + print(f"Train window: {self.train_window.days} days") + print(f"Test window: {self.test_window.days} days") + print(f"Step size: {self.step.days} days\n") + + # Load dataset + dataset = load_dataset(dataset_name) + dataset.clean_data() + + df = dataset.df.sort_values('vp_ts').reset_index(drop=True) + + # Get date range + start_date = df['vp_ts'].min() + end_date = df['vp_ts'].max() + + print(f"Data range: {start_date} to {end_date}") + print(f"Total duration: {(end_date - start_date).days} days\n") + + # Generate windows + current_start = start_date + window_num = 1 + results = [] + + while current_start + self.train_window + self.test_window <= end_date: + train_end = current_start + self.train_window + test_start = train_end + test_end = test_start + self.test_window + + print(f"Window {window_num}:") + print(f" Train: {current_start.date()} to {train_end.date()}") + print(f" Test: {test_start.date()} to {test_end.date()}") + + # Split data + train_df = df[(df['vp_ts'] >= current_start) & (df['vp_ts'] < train_end)] + test_df = df[(df['vp_ts'] >= test_start) & (df['vp_ts'] < test_end)] + + print(f" Train samples: {len(train_df)}, Test samples: {len(test_df)}") + + if len(train_df) == 0 or len(test_df) == 0: + print(f" Skipping (insufficient data)\n") + current_start += self.step + window_num += 1 + continue + + try: + # Train model + model = train_fn(train_df) + + # Predict + y_true = test_df[target_col].values + y_pred = predict_fn(model, test_df) + + # Compute metrics + metrics = compute_all_metrics(y_true, y_pred) + + # Store results + result = { + 'window': window_num, + 'train_start': current_start, + 'train_end': train_end, + 'test_start': test_start, + 'test_end': test_end, + 'train_samples': len(train_df), + 'test_samples': len(test_df), + **metrics + } + results.append(result) + + print(f" MAE: {metrics['mae_minutes']:.2f} min, RMSE: {metrics['rmse_minutes']:.2f} min") + print() + + except Exception as e: + print(f" Error: {e}\n") + + # Move to next window + current_start += self.step + window_num += 1 + + # Create results dataframe + results_df = pd.DataFrame(results) + + if not results_df.empty: + self._print_summary(results_df) + + self.results = results_df + return results_df + + def _print_summary(self, results_df: pd.DataFrame): + """Print summary statistics.""" + print(f"\n{'='*60}") + print(f"VALIDATION SUMMARY".center(60)) + print(f"{'='*60}\n") + + print(f"Total windows: {len(results_df)}") + print(f"\nAverage Metrics:") + print(f" MAE: {results_df['mae_minutes'].mean():.3f} ± {results_df['mae_minutes'].std():.3f} minutes") + print(f" RMSE: {results_df['rmse_minutes'].mean():.3f} ± {results_df['rmse_minutes'].std():.3f} minutes") + print(f" R²: {results_df['r2'].mean():.3f} ± {results_df['r2'].std():.3f}") + print(f" Within 60s: {results_df['within_60s'].mean()*100:.1f}%") + + print(f"\nBest Window: {results_df.loc[results_df['mae_minutes'].idxmin(), 'window']}") + print(f" MAE: {results_df['mae_minutes'].min():.3f} minutes") + + print(f"\nWorst Window: {results_df.loc[results_df['mae_minutes'].idxmax(), 'window']}") + print(f" MAE: {results_df['mae_minutes'].max():.3f} minutes") + + print(f"\n{'='*60}\n") + + def plot_results(self, results_df: Optional[pd.DataFrame] = None, + metric: str = 'mae_minutes'): + """ + Plot metric over time (requires matplotlib). + + Args: + results_df: Results dataframe (uses self.results if None) + metric: Metric to plot + """ + if results_df is None: + results_df = self.results + + if results_df.empty: + print("No results to plot") + return + + try: + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(12, 6)) + + ax.plot(results_df['window'], results_df[metric], marker='o') + ax.set_xlabel('Window Number') + ax.set_ylabel(metric.replace('_', ' ').title()) + ax.set_title(f'Rolling Window Validation: {metric}') + ax.grid(True, alpha=0.3) + + # Add mean line + mean_val = results_df[metric].mean() + ax.axhline(mean_val, color='r', linestyle='--', + label=f'Mean: {mean_val:.3f}') + ax.legend() + + plt.tight_layout() + plt.savefig(f'rolling_validation_{metric}.png', dpi=150) + print(f"Plot saved to rolling_validation_{metric}.png") + + except ImportError: + print("matplotlib not available for plotting") + + +def quick_rolling_validate(model_class, + model_params: Dict, + dataset_name: str = "sample_dataset", + train_window_days: int = 7) -> pd.DataFrame: + """ + Quick rolling validation for a model class. + + Args: + model_class: Model class to instantiate + model_params: Parameters for model initialization + dataset_name: Dataset name + train_window_days: Training window size + + Returns: + Results dataframe + """ + def train_fn(train_df): + model = model_class(**model_params) + model.fit(train_df) + return model + + def predict_fn(model, test_df): + return model.predict(test_df) + + validator = RollingValidator( + train_window_days=train_window_days, + test_window_days=1, + step_days=1 + ) + + results_df = validator.validate(dataset_name, train_fn, predict_fn) + + return results_df + + +if __name__ == "__main__": + # Example: Rolling validation for EWMA model + import sys + sys.path.append(str(Path(__file__).parent.parent)) + from ewma.train import EWMAModel + + results = quick_rolling_validate( + model_class=EWMAModel, + model_params={'alpha': 0.3, 'group_by': ['route_id', 'stop_sequence']}, + train_window_days=7 + ) + + # Save results + results.to_csv("models/rolling_validation_results.csv", index=False) + print("Results saved to models/rolling_validation_results.csv") \ No newline at end of file diff --git a/eta_prediction/models/ewma/predict.py b/eta_prediction/models/ewma/predict.py new file mode 100644 index 0000000..14ba453 --- /dev/null +++ b/eta_prediction/models/ewma/predict.py @@ -0,0 +1,155 @@ +""" +Prediction interface for EWMA model. +""" + +import pandas as pd +import numpy as np +from typing import Dict, Optional +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.registry import get_registry +from common.utils import format_seconds + + +def predict_eta(model_key: str, + route_id: str, + stop_sequence: int, + hour: Optional[int] = None) -> Dict: + """ + Predict ETA using EWMA model. + + Args: + model_key: Model identifier + route_id: Route ID + stop_sequence: Stop sequence number + hour: Hour of day (if model uses hourly grouping) + + Returns: + Dictionary with prediction and metadata + """ + # Load model + registry = get_registry() + model = registry.load_model(model_key) + metadata = registry.load_metadata(model_key) + + # Prepare input + input_data = { + 'route_id': [route_id], + 'stop_sequence': [stop_sequence] + } + + if hour is not None and 'hour' in model.group_by: + input_data['hour'] = [hour] + + input_df = pd.DataFrame(input_data) + + # Predict + eta_seconds = model.predict(input_df)[0] + + # Check if EWMA value exists + key = tuple(input_df.iloc[0][col] for col in model.group_by) + has_ewma = key in model.ewma_values + n_observations = model.observation_counts.get(key, 0) if has_ewma else 0 + + return { + 'eta_seconds': float(eta_seconds), + 'eta_minutes': float(eta_seconds / 60), + 'eta_formatted': format_seconds(eta_seconds), + 'model_key': model_key, + 'model_type': 'ewma', + 'alpha': metadata.get('alpha'), + 'has_ewma_value': has_ewma, + 'n_observations': n_observations, + 'using_global_mean': not has_ewma or n_observations < model.min_observations + } + + +def predict_and_update(model_key: str, + route_id: str, + stop_sequence: int, + observed_eta: float, + hour: Optional[int] = None, + save_updated: bool = False) -> Dict: + """ + Predict ETA and update model with observed value (online learning). + + Args: + model_key: Model identifier + route_id: Route ID + stop_sequence: Stop sequence + observed_eta: Actual observed ETA in seconds + hour: Hour of day + save_updated: Whether to save updated model back to registry + + Returns: + Dictionary with prediction, error, and updated EWMA + """ + # Get prediction first + result = predict_eta(model_key, route_id, stop_sequence, hour) + prediction = result['eta_seconds'] + + # Load model for update + registry = get_registry() + model = registry.load_model(model_key) + + # Prepare input for update + input_data = { + 'route_id': [route_id], + 'stop_sequence': [stop_sequence] + } + if hour is not None and 'hour' in model.group_by: + input_data['hour'] = [hour] + + input_df = pd.DataFrame(input_data) + + # Update model + model.update(input_df, np.array([observed_eta])) + + # Get new EWMA value + key = tuple(input_df.iloc[0][col] for col in model.group_by) + new_ewma = model.ewma_values.get(key) + + # Save if requested + if save_updated: + metadata = registry.load_metadata(model_key) + registry.save_model(model_key, model, metadata, overwrite=True) + + return { + **result, + 'observed_eta_seconds': observed_eta, + 'error_seconds': observed_eta - prediction, + 'updated_ewma_seconds': new_ewma, + 'model_updated': True + } + + +def batch_predict(model_key: str, input_df: pd.DataFrame) -> pd.DataFrame: + """Batch prediction.""" + registry = get_registry() + model = registry.load_model(model_key) + + result_df = input_df.copy() + result_df['predicted_eta_seconds'] = model.predict(input_df) + result_df['predicted_eta_minutes'] = result_df['predicted_eta_seconds'] / 60 + + return result_df + + +if __name__ == "__main__": + # Example: predict and update + result = predict_and_update( + model_key="ewma_sample_dataset_temporal-route_20250126_143022_alpha=0_3", + route_id="1", + stop_sequence=5, + observed_eta=180.0, # 3 minutes + hour=8 + ) + + print("Prediction and Update:") + print(f" Predicted: {result['eta_formatted']}") + print(f" Observed: {format_seconds(result['observed_eta_seconds'])}") + print(f" Error: {result['error_seconds']:.1f} seconds") + print(f" Updated EWMA: {result['updated_ewma_seconds']/60:.2f} minutes") \ No newline at end of file diff --git a/eta_prediction/models/ewma/train.py b/eta_prediction/models/ewma/train.py new file mode 100644 index 0000000..b205bf7 --- /dev/null +++ b/eta_prediction/models/ewma/train.py @@ -0,0 +1,324 @@ +""" +Exponentially Weighted Moving Average (EWMA) Model +Adapts predictions based on recent observations with exponential decay. +""" + +import pandas as pd +import numpy as np +from typing import Dict, Optional, Tuple +from collections import defaultdict +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.data import load_dataset +from common.metrics import compute_all_metrics +from common.keys import ModelKey +from common.registry import get_registry +from common.utils import print_metrics_table, train_test_summary, clip_predictions + + +class EWMAModel: + """ + EWMA-based ETA prediction. + + Maintains exponentially weighted moving averages for each (route, stop) pair. + Updates incrementally as new observations arrive. + + ETA_new = alpha * observed + (1 - alpha) * ETA_old + """ + + def __init__(self, + alpha: float = 0.3, + group_by: list = ['route_id', 'stop_sequence'], + min_observations: int = 3): + """ + Initialize EWMA model. + + Args: + alpha: Smoothing parameter (0-1, higher = more weight on recent) + group_by: Features to group by + min_observations: Min observations before using EWMA + """ + self.alpha = alpha + self.group_by = group_by + self.min_observations = min_observations + + self.ewma_values = {} # (route, stop, ...) -> current EWMA + self.observation_counts = {} # (route, stop, ...) -> count + self.global_mean = None + + def _make_key(self, row: pd.Series) -> tuple: + """Create lookup key from row.""" + return tuple(row[col] for col in self.group_by) + + def fit(self, train_df: pd.DataFrame, target_col: str = 'time_to_arrival_seconds'): + """ + Train EWMA model by processing observations in time order. + + Args: + train_df: Training dataframe (should be time-sorted) + target_col: Target column name + """ + # Sort by timestamp + df_sorted = train_df.sort_values('vp_ts').reset_index(drop=True) + + self.global_mean = df_sorted[target_col].mean() + + # Process observations sequentially + for _, row in df_sorted.iterrows(): + key = self._make_key(row) + observed = row[target_col] + + if key not in self.ewma_values: + # Initialize with first observation + self.ewma_values[key] = observed + self.observation_counts[key] = 1 + else: + # Update EWMA + old_ewma = self.ewma_values[key] + new_ewma = self.alpha * observed + (1 - self.alpha) * old_ewma + self.ewma_values[key] = new_ewma + self.observation_counts[key] += 1 + + print(f"Trained EWMA model (alpha={self.alpha})") + print(f" Unique groups: {len(self.ewma_values)}") + print(f" Total observations: {len(df_sorted)}") + print(f" Global mean: {self.global_mean/60:.2f} minutes") + + def predict(self, X: pd.DataFrame) -> np.ndarray: + """ + Predict ETAs using current EWMA values. + + Args: + X: DataFrame with group_by columns + + Returns: + Array of predicted ETAs + """ + predictions = [] + + for _, row in X.iterrows(): + key = self._make_key(row) + + if key in self.ewma_values: + count = self.observation_counts[key] + if count >= self.min_observations: + predictions.append(self.ewma_values[key]) + else: + # Not enough observations, use global mean + predictions.append(self.global_mean) + else: + # New group, use global mean + predictions.append(self.global_mean) + + return clip_predictions(np.array(predictions)) + + def update(self, X: pd.DataFrame, y: np.ndarray): + """ + Update EWMA values with new observations (online learning). + + Args: + X: Features + y: Observed values + """ + for (_, row), observed in zip(X.iterrows(), y): + key = self._make_key(row) + + if key not in self.ewma_values: + self.ewma_values[key] = observed + self.observation_counts[key] = 1 + else: + old_ewma = self.ewma_values[key] + new_ewma = self.alpha * observed + (1 - self.alpha) * old_ewma + self.ewma_values[key] = new_ewma + self.observation_counts[key] += 1 + + def get_coverage(self, X: pd.DataFrame) -> float: + """Get fraction of predictions with EWMA values.""" + covered = 0 + for _, row in X.iterrows(): + key = self._make_key(row) + if key in self.ewma_values and self.observation_counts[key] >= self.min_observations: + covered += 1 + return covered / len(X) + + def get_group_stats(self) -> pd.DataFrame: + """Get statistics about learned groups.""" + stats = [] + for key, ewma in self.ewma_values.items(): + count = self.observation_counts[key] + stats.append({ + **{col: val for col, val in zip(self.group_by, key)}, + 'ewma_eta_minutes': ewma / 60, + 'n_observations': count + }) + + return pd.DataFrame(stats).sort_values('n_observations', ascending=False) + + +def train_ewma(dataset_name: str = "sample_dataset", + route_id: Optional[str] = None, + alpha: float = 0.3, + group_by: list = ['route_id', 'stop_sequence'], + min_observations: int = 3, + test_size: float = 0.2, + save_model: bool = True, + pre_split: Optional[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]] = None) -> Dict: + """ + Train and evaluate EWMA model. + + Args: + dataset_name: Name of dataset + route_id: Optional route ID for route-specific training + alpha: EWMA smoothing parameter + group_by: Grouping columns + min_observations: Minimum observations threshold + test_size: Test fraction + save_model: Whether to save + pre_split: Optional (train, val, test) DataFrames to reuse + + Returns: + Dictionary with model, metrics, metadata + """ + print(f"\n{'='*60}") + print(f"Training EWMA Model".center(60)) + print(f"{'='*60}\n") + + route_info = f" (route: {route_id})" if route_id else " (global)" + print(f"Scope{route_info}") + print(f"Config: alpha={alpha}, group_by={group_by}, min_obs={min_observations}") + + # Load dataset + print(f"\nLoading dataset: {dataset_name}") + dataset = load_dataset(dataset_name) + dataset.clean_data() + + # Filter by route if specified + if route_id is not None: + df = dataset.df + df_filtered = df[df['route_id'] == route_id].copy() + print(f"Filtered to route {route_id}: {len(df_filtered):,} samples") + + if len(df_filtered) == 0: + raise ValueError(f"No data found for route {route_id}") + + dataset.df = df_filtered + + if pre_split is not None: + train_df, val_df, test_df = (df.copy() for df in pre_split) + else: + # Split data temporally (important for time series) + train_df, val_df, test_df = dataset.temporal_split( + train_frac=1-test_size-0.1, + val_frac=0.1 + ) + + train_test_summary(train_df, test_df, val_df) + + # Train model + print("Training model...") + model = EWMAModel( + alpha=alpha, + group_by=group_by, + min_observations=min_observations + ) + model.fit(train_df) + + # Evaluate on validation (with optional online updates) + print("\nValidation Performance:") + y_val = val_df['time_to_arrival_seconds'].values + val_preds = model.predict(val_df) + val_metrics = compute_all_metrics(y_val, val_preds, prefix="val_") + print_metrics_table(val_metrics, "Validation Metrics") + + val_coverage = model.get_coverage(val_df) + print(f"Validation coverage: {val_coverage*100:.1f}%") + + # Optionally update model with validation data + print("\nUpdating model with validation data...") + model.update(val_df, y_val) + + # Evaluate on test set + print("\nTest Performance:") + y_test = test_df['time_to_arrival_seconds'].values + test_preds = model.predict(test_df) + test_metrics = compute_all_metrics(y_test, test_preds, prefix="test_") + print_metrics_table(test_metrics, "Test Metrics") + + test_coverage = model.get_coverage(test_df) + print(f"Test coverage: {test_coverage*100:.1f}%") + + # Show some group stats + group_stats = model.get_group_stats() + print(f"\nTop 10 groups by observation count:") + print(group_stats.head(10)) + + # Prepare metadata + metadata = { + 'model_type': 'ewma', + 'dataset': dataset_name, + 'route_id': route_id, + 'alpha': alpha, + 'group_by': group_by, + 'min_observations': min_observations, + 'n_groups': len(model.ewma_values), + 'n_samples': len(train_df) + len(val_df) + len(test_df), + 'n_trips': dataset.df['trip_id'].nunique() if route_id else None, + 'train_samples': len(train_df), + 'test_samples': len(test_df), + 'test_coverage': float(test_coverage), + 'global_mean_eta': float(model.global_mean), + 'metrics': {**val_metrics, **test_metrics} + } + + # Save model + if save_model: + model_key = ModelKey.generate( + model_type='ewma', + dataset_name=dataset_name, + feature_groups=['temporal', 'route'], + route_id=route_id, + alpha=str(alpha).replace('.', '_') + ) + + registry = get_registry() + registry.save_model(model_key, model, metadata) + metadata['model_key'] = model_key + + return { + 'model': model, + 'metrics': metadata['metrics'], + 'metadata': metadata + } + + +if __name__ == "__main__": + # Train with different alpha values + + # Conservative (slow adaptation) + result1 = train_ewma(alpha=0.1) + + # Balanced + result2 = train_ewma(alpha=0.3) + + # Aggressive (fast adaptation) + result3 = train_ewma(alpha=0.5) + + # With hourly grouping + result4 = train_ewma( + alpha=0.3, + group_by=['route_id', 'stop_sequence', 'hour'] + ) + + # Compare + print("\n" + "="*60) + print("Model Comparison (Test MAE)") + print("="*60) + results = [result1, result2, result3, result4] + labels = ["alpha=0.1", "alpha=0.3", "alpha=0.5", "alpha=0.3+hour"] + + for label, result in zip(labels, results): + mae_min = result['metrics']['test_mae_minutes'] + print(f"{label:20s}: {mae_min:.3f} minutes") diff --git a/eta_prediction/models/historical_mean/predict.py b/eta_prediction/models/historical_mean/predict.py new file mode 100644 index 0000000..ff5ce67 --- /dev/null +++ b/eta_prediction/models/historical_mean/predict.py @@ -0,0 +1,136 @@ +""" +Prediction interface for Historical Mean model. +""" + +import pandas as pd +import numpy as np +from typing import Dict, Optional +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.registry import get_registry +from common.utils import format_seconds + + +def predict_eta(model_key: str, + route_id: str, + stop_sequence: int, + hour: int, + day_of_week: Optional[int] = None, + is_peak_hour: Optional[bool] = None) -> Dict: + """ + Predict ETA using historical mean model. + + Args: + model_key: Model identifier in registry + route_id: Route ID + stop_sequence: Stop sequence number + hour: Hour of day (0-23) + day_of_week: Day of week (0=Monday, optional) + is_peak_hour: Peak hour flag (optional) + + Returns: + Dictionary with prediction and metadata + """ + # Load model + registry = get_registry() + model = registry.load_model(model_key) + metadata = registry.load_metadata(model_key) + + # Prepare input dataframe + input_data = { + 'route_id': [route_id], + 'stop_sequence': [stop_sequence], + 'hour': [hour] + } + + if day_of_week is not None and 'day_of_week' in model.group_by: + input_data['day_of_week'] = [day_of_week] + + if is_peak_hour is not None and 'is_peak_hour' in model.group_by: + input_data['is_peak_hour'] = [is_peak_hour] + + input_df = pd.DataFrame(input_data) + + # Predict + eta_seconds = model.predict(input_df)[0] + + # Check if prediction came from historical data or fallback + coverage = model.get_coverage(input_df) + has_historical_data = coverage > 0 + + return { + 'eta_seconds': float(eta_seconds), + 'eta_minutes': float(eta_seconds / 60), + 'eta_formatted': format_seconds(eta_seconds), + 'model_key': model_key, + 'model_type': 'historical_mean', + 'has_historical_data': has_historical_data, + 'global_mean_eta': metadata.get('global_mean_eta'), + 'group_by': metadata.get('group_by') + } + + +def batch_predict(model_key: str, input_df: pd.DataFrame) -> pd.DataFrame: + """ + Batch prediction for multiple inputs. + + Args: + model_key: Model identifier in registry + input_df: DataFrame with features + + Returns: + DataFrame with predictions added + """ + registry = get_registry() + model = registry.load_model(model_key) + + result_df = input_df.copy() + result_df['predicted_eta_seconds'] = model.predict(input_df) + result_df['predicted_eta_minutes'] = result_df['predicted_eta_seconds'] / 60 + + # Add coverage information + result_df['has_historical_data'] = False + merged = input_df[model.group_by].merge( + model.lookup_table, + on=model.group_by, + how='left', + indicator=True + ) + result_df.loc[merged['_merge'] == 'both', 'has_historical_data'] = True + + return result_df + + +if __name__ == "__main__": + # Example usage + + # Single prediction + result = predict_eta( + model_key="historical_mean_sample_dataset_temporal-route_20250126_143022", + route_id="1", + stop_sequence=5, + hour=8, + day_of_week=0 # Monday + ) + + print("Single Prediction:") + print(f" ETA: {result['eta_formatted']}") + print(f" Has historical data: {result['has_historical_data']}") + + # Batch prediction example + test_data = pd.DataFrame({ + 'route_id': ['1', '1', '2'], + 'stop_sequence': [5, 10, 3], + 'hour': [8, 17, 12] + }) + + predictions = batch_predict( + model_key="historical_mean_sample_dataset_temporal-route_20250126_143022", + input_df=test_data + ) + + print("\nBatch Predictions:") + print(predictions[['route_id', 'stop_sequence', 'predicted_eta_minutes', 'has_historical_data']]) \ No newline at end of file diff --git a/eta_prediction/models/historical_mean/train.py b/eta_prediction/models/historical_mean/train.py new file mode 100644 index 0000000..d327b34 --- /dev/null +++ b/eta_prediction/models/historical_mean/train.py @@ -0,0 +1,243 @@ +""" +Historical Mean Baseline Model +Predicts ETA based on historical average travel times grouped by route, stop, and time features. +""" + +import pandas as pd +import numpy as np +from typing import Dict, List, Optional, Tuple +import sys +from pathlib import Path + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent)) + +from common.data import load_dataset, prepare_features_target +from common.metrics import compute_all_metrics +from common.keys import ModelKey +from common.registry import get_registry +from common.utils import print_metrics_table, train_test_summary + + +class HistoricalMeanModel: + """ + Baseline model using historical mean ETAs. + + Groups data by route, stop, and temporal features, then computes + mean ETA for each group. At prediction time, looks up the appropriate group mean. + """ + + def __init__(self, group_by: List[str] = ['route_id', 'stop_sequence', 'hour']): + """ + Initialize model. + + Args: + group_by: List of columns to group by for computing means + """ + self.group_by = group_by + self.lookup_table = None + self.global_mean = None + self.feature_cols = None + + def fit(self, train_df: pd.DataFrame, target_col: str = 'time_to_arrival_seconds'): + """ + Train model by computing historical means. + + Args: + train_df: Training dataframe with features and target + target_col: Name of target column + """ + # Store feature columns for later + self.feature_cols = self.group_by + + # Compute global mean as fallback + self.global_mean = train_df[target_col].mean() + + # Compute group means + self.lookup_table = train_df.groupby(self.group_by)[target_col].agg([ + ('mean', 'mean'), + ('std', 'std'), + ('count', 'count') + ]).reset_index() + + print(f"Trained on {len(train_df)} samples") + print(f"Created {len(self.lookup_table)} unique groups") + print(f"Global mean ETA: {self.global_mean/60:.2f} minutes") + + def predict(self, X: pd.DataFrame) -> np.ndarray: + """ + Predict ETAs for input data. + + Args: + X: DataFrame with features matching group_by columns + + Returns: + Array of predicted ETAs in seconds + """ + if self.lookup_table is None: + raise ValueError("Model not trained. Call fit() first.") + + # Merge with lookup table + merged = X[self.group_by].merge( + self.lookup_table, + on=self.group_by, + how='left' + ) + + # Use group mean, fallback to global mean + predictions = merged['mean'].fillna(self.global_mean).values + + return predictions + + def get_coverage(self, X: pd.DataFrame) -> float: + """ + Get fraction of predictions with matching historical data. + + Args: + X: DataFrame with features + + Returns: + Coverage ratio (0-1) + """ + merged = X[self.group_by].merge( + self.lookup_table, + on=self.group_by, + how='left', + indicator=True + ) + + return (merged['_merge'] == 'both').mean() + + +def train_historical_mean(dataset_name: str = "sample_dataset", + route_id: Optional[str] = None, + group_by: List[str] = ['route_id', 'stop_sequence', 'hour'], + test_size: float = 0.2, + save_model: bool = True, + pre_split: Optional[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]] = None) -> Dict: + """ + Train and evaluate historical mean model. + + Args: + dataset_name: Name of dataset in datasets/ directory + route_id: Optional route ID for route-specific training + group_by: List of columns to group by + test_size: Fraction of data for testing + save_model: Whether to save to registry + pre_split: Optional (train, val, test) DataFrames to reuse + + Returns: + Dictionary with model, metrics, and metadata + """ + print(f"\n{'='*60}") + print(f"Training Historical Mean Model".center(60)) + print(f"{'='*60}\n") + + route_info = f" (route: {route_id})" if route_id else " (global)" + print(f"Scope{route_info}") + print(f"Group by: {group_by}") + + # Load dataset + print(f"\nLoading dataset: {dataset_name}") + dataset = load_dataset(dataset_name) + dataset.clean_data() + + # Filter by route if specified + if route_id is not None: + df = dataset.df + df_filtered = df[df['route_id'] == route_id].copy() + print(f"Filtered to route {route_id}: {len(df_filtered):,} samples") + + if len(df_filtered) == 0: + raise ValueError(f"No data found for route {route_id}") + + dataset.df = df_filtered + + if pre_split is not None: + train_df, val_df, test_df = (df.copy() for df in pre_split) + else: + train_df, val_df, test_df = dataset.temporal_split( + train_frac=1-test_size-0.1, + val_frac=0.1 + ) + + train_test_summary(train_df, test_df, val_df) + + # Train model + print("Training model...") + model = HistoricalMeanModel(group_by=group_by) + model.fit(train_df) + + # Evaluate on validation set + print("\nValidation Performance:") + y_val = val_df['time_to_arrival_seconds'].values + val_preds = model.predict(val_df) + val_metrics = compute_all_metrics(y_val, val_preds, prefix="val_") + print_metrics_table(val_metrics, "Validation Metrics") + + val_coverage = model.get_coverage(val_df) + print(f"Validation coverage: {val_coverage*100:.1f}%") + + # Evaluate on test set + print("\nTest Performance:") + y_test = test_df['time_to_arrival_seconds'].values + test_preds = model.predict(test_df) + test_metrics = compute_all_metrics(y_test, test_preds, prefix="test_") + print_metrics_table(test_metrics, "Test Metrics") + + test_coverage = model.get_coverage(test_df) + print(f"Test coverage: {test_coverage*100:.1f}%") + + # Prepare metadata + metadata = { + 'model_type': 'historical_mean', + 'dataset': dataset_name, + 'route_id': route_id, + 'group_by': group_by, + 'n_samples': len(train_df) + len(val_df) + len(test_df), + 'n_trips': dataset.df['trip_id'].nunique() if route_id else None, + 'train_samples': len(train_df), + 'test_samples': len(test_df), + 'unique_groups': len(model.lookup_table), + 'global_mean_eta': float(model.global_mean), + 'test_coverage': float(test_coverage), + 'metrics': {**val_metrics, **test_metrics} + } + + # Save model + if save_model: + model_key = ModelKey.generate( + model_type='historical_mean', + dataset_name=dataset_name, + feature_groups=['temporal', 'route'], + route_id=route_id + ) + + registry = get_registry() + registry.save_model(model_key, model, metadata) + metadata['model_key'] = model_key + + return { + 'model': model, + 'metrics': metadata['metrics'], + 'metadata': metadata + } + + +if __name__ == "__main__": + # Train with different grouping strategies + + # Basic: route + stop + hour + result1 = train_historical_mean( + group_by=['route_id', 'stop_sequence', 'hour'] + ) + + # With day of week + result2 = train_historical_mean( + group_by=['route_id', 'stop_sequence', 'hour', 'day_of_week'] + ) + + # With peak hour indicator + result3 = train_historical_mean( + group_by=['route_id', 'stop_sequence', 'is_peak_hour'] + ) diff --git a/eta_prediction/models/polyreg_distance/predict.py b/eta_prediction/models/polyreg_distance/predict.py new file mode 100644 index 0000000..2fa1ddd --- /dev/null +++ b/eta_prediction/models/polyreg_distance/predict.py @@ -0,0 +1,112 @@ +""" +Prediction interface for Polynomial Regression Distance model. +""" + +import pandas as pd +import numpy as np +from typing import Dict, Optional +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.registry import get_registry +from common.utils import format_seconds + + +def predict_eta(model_key: str, + distance_to_stop: float, + route_id: Optional[str] = None) -> Dict: + """ + Predict ETA using polynomial regression distance model. + + Args: + model_key: Model identifier in registry + distance_to_stop: Distance to stop in meters + route_id: Route ID (required for route-specific models) + + Returns: + Dictionary with prediction and metadata + """ + # Load model + registry = get_registry() + model = registry.load_model(model_key) + metadata = registry.load_metadata(model_key) + + # Prepare input + input_data = {'distance_to_stop': [distance_to_stop]} + if model.route_specific: + if route_id is None: + raise ValueError("route_id required for route-specific model") + input_data['route_id'] = [route_id] + + input_df = pd.DataFrame(input_data) + + # Predict + eta_seconds = model.predict(input_df)[0] + + # Get coefficients for this route + coefs = model.get_coefficients(route_id if model.route_specific else None) + + return { + 'eta_seconds': float(eta_seconds), + 'eta_minutes': float(eta_seconds / 60), + 'eta_formatted': format_seconds(eta_seconds), + 'model_key': model_key, + 'model_type': 'polyreg_distance', + 'distance_to_stop_m': distance_to_stop, + 'route_specific': metadata.get('route_specific', False), + 'degree': metadata.get('degree'), + 'coefficients': coefs + } + + +def batch_predict(model_key: str, input_df: pd.DataFrame) -> pd.DataFrame: + """ + Batch prediction for multiple inputs. + + Args: + model_key: Model identifier in registry + input_df: DataFrame with distance_to_stop (and route_id if needed) + + Returns: + DataFrame with predictions added + """ + registry = get_registry() + model = registry.load_model(model_key) + + result_df = input_df.copy() + result_df['predicted_eta_seconds'] = model.predict(input_df) + result_df['predicted_eta_minutes'] = result_df['predicted_eta_seconds'] / 60 + + return result_df + + +if __name__ == "__main__": + # Example usage + + # Single prediction + result = predict_eta( + model_key="polyreg_distance_sample_dataset_distance_20250126_143022_degree=2", + distance_to_stop=1500.0, # 1.5 km + route_id="1" + ) + + print("Single Prediction:") + print(f" Distance: {result['distance_to_stop_m']} meters") + print(f" ETA: {result['eta_formatted']}") + print(f" Degree: {result['degree']}") + + # Batch prediction + test_data = pd.DataFrame({ + 'route_id': ['1', '1', '2'], + 'distance_to_stop': [500.0, 1500.0, 3000.0] + }) + + predictions = batch_predict( + model_key="polyreg_distance_sample_dataset_distance_20250126_143022_degree=2", + input_df=test_data + ) + + print("\nBatch Predictions:") + print(predictions[['route_id', 'distance_to_stop', 'predicted_eta_minutes']]) \ No newline at end of file diff --git a/eta_prediction/models/polyreg_distance/train.py b/eta_prediction/models/polyreg_distance/train.py new file mode 100644 index 0000000..16a88e8 --- /dev/null +++ b/eta_prediction/models/polyreg_distance/train.py @@ -0,0 +1,318 @@ +""" +Polynomial Regression Model - Distance-based +Fits polynomial features on distance_to_stop with optional route-specific models. +""" + +import pandas as pd +import numpy as np +from typing import Dict, Optional, Tuple +from sklearn.preprocessing import PolynomialFeatures +from sklearn.linear_model import Ridge +from sklearn.pipeline import Pipeline +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.data import load_dataset, prepare_features_target +from common.metrics import compute_all_metrics +from common.keys import ModelKey +from common.registry import get_registry +from common.utils import print_metrics_table, train_test_summary, clip_predictions + + +class PolyRegDistanceModel: + """ + Polynomial regression on distance with optional route-specific models. + + Features: distance_to_stop, (distance)^2, (distance)^3, ... + Can fit separate models per route for better performance. + """ + + def __init__(self, + degree: int = 2, + alpha: float = 1.0, + route_specific: bool = False): + """ + Initialize model. + + Args: + degree: Polynomial degree (2 or 3 recommended) + alpha: Ridge regression alpha (regularization strength) + route_specific: Whether to fit separate model per route + """ + self.degree = degree + self.alpha = alpha + self.route_specific = route_specific + self.models = {} # route_id -> model mapping + self.global_model = None + self.feature_cols = ['distance_to_stop'] + + def _create_pipeline(self) -> Pipeline: + """Create sklearn pipeline with polynomial features and ridge regression.""" + return Pipeline([ + ('poly', PolynomialFeatures(degree=self.degree, include_bias=True)), + ('ridge', Ridge(alpha=self.alpha)) + ]) + + def fit(self, train_df: pd.DataFrame, target_col: str = 'time_to_arrival_seconds'): + """ + Train model(s). + + Args: + train_df: Training dataframe with distance_to_stop and target + target_col: Name of target column + """ + if 'distance_to_stop' not in train_df.columns: + raise ValueError("distance_to_stop column required") + + if self.route_specific: + # Fit separate model per route + for route_id, route_df in train_df.groupby('route_id'): + X = route_df[['distance_to_stop']].values + y = route_df[target_col].values + + model = self._create_pipeline() + model.fit(X, y) + self.models[route_id] = model + + print(f"Trained {len(self.models)} route-specific models (degree={self.degree})") + else: + # Fit single global model + X = train_df[['distance_to_stop']].values + y = train_df[target_col].values + + self.global_model = self._create_pipeline() + self.global_model.fit(X, y) + + print(f"Trained global model (degree={self.degree}, alpha={self.alpha})") + + def predict(self, X: pd.DataFrame) -> np.ndarray: + """ + Predict ETAs. + + Args: + X: DataFrame with distance_to_stop (and route_id if route_specific) + + Returns: + Array of predicted ETAs in seconds + """ + if self.route_specific: + if 'route_id' not in X.columns: + raise ValueError("route_id required for route-specific model") + + # Create predictions array matching input length + predictions = np.zeros(len(X)) + + # Reset index to get positional indices + X_reset = X.reset_index(drop=True) + + for route_id, route_df in X_reset.groupby('route_id'): + # Get positional indices (0-based from reset_index) + pos_indices = route_df.index.values + X_route = route_df[['distance_to_stop']].values + + if route_id in self.models: + predictions[pos_indices] = self.models[route_id].predict(X_route) + elif self.global_model is not None: + # Fallback to global model + predictions[pos_indices] = self.global_model.predict(X_route) + else: + # No model available - use simple linear estimate (30 km/h avg) + predictions[pos_indices] = X_route.flatten() / 30000 * 3600 + else: + if self.global_model is None: + raise ValueError("Model not trained") + + X_dist = X[['distance_to_stop']].values + predictions = self.global_model.predict(X_dist) + + # Clip to reasonable range + return clip_predictions(predictions) + + def get_coefficients(self, route_id: Optional[str] = None) -> Dict: + """ + Get model coefficients. + + Args: + route_id: Route ID for route-specific model, None for global + + Returns: + Dictionary with coefficients + """ + if route_id and route_id in self.models: + model = self.models[route_id] + elif self.global_model: + model = self.global_model + else: + return {} + + coefs = model.named_steps['ridge'].coef_ + intercept = model.named_steps['ridge'].intercept_ + + return { + 'intercept': float(intercept), + 'coefficients': coefs.tolist(), + 'degree': self.degree + } + + +def train_polyreg_distance(dataset_name: str = "sample_dataset", + route_id: Optional[str] = None, + degree: int = 2, + alpha: float = 1.0, + route_specific: bool = False, + test_size: float = 0.2, + save_model: bool = True, + pre_split: Optional[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]] = None) -> Dict: + """ + Train and evaluate polynomial regression distance model. + + Args: + dataset_name: Name of dataset in datasets/ directory + route_id: Optional route ID for route-specific training (overrides route_specific) + degree: Polynomial degree + alpha: Ridge regularization strength + route_specific: Whether to fit per-route models (ignored if route_id specified) + test_size: Fraction of data for testing + save_model: Whether to save to registry + pre_split: Optional (train, val, test) DataFrames to reuse + + Returns: + Dictionary with model, metrics, and metadata + """ + print(f"\n{'='*60}") + print(f"Training Polynomial Regression Distance Model".center(60)) + print(f"{'='*60}\n") + + route_info = f" (route: {route_id})" if route_id else " (global)" + print(f"Scope{route_info}") + print(f"Config: degree={degree}, alpha={alpha}, route_specific={route_specific}") + + # Load dataset + print(f"\nLoading dataset: {dataset_name}") + dataset = load_dataset(dataset_name) + dataset.clean_data() + + # Filter by route if specified (single-route model) + if route_id is not None: + df = dataset.df + df_filtered = df[df['route_id'] == route_id].copy() + print(f"Filtered to route {route_id}: {len(df_filtered):,} samples") + + if len(df_filtered) == 0: + raise ValueError(f"No data found for route {route_id}") + + dataset.df = df_filtered + # When training on single route, don't need route_specific flag + route_specific = False + + if pre_split is not None: + train_df, val_df, test_df = (df.copy() for df in pre_split) + else: + train_df, val_df, test_df = dataset.temporal_split( + train_frac=1-test_size-0.1, + val_frac=0.1 + ) + + train_test_summary(train_df, test_df, val_df) + + # Train model + print("Training model...") + model = PolyRegDistanceModel( + degree=degree, + alpha=alpha, + route_specific=route_specific + ) + model.fit(train_df) + + # Evaluate on validation set + print("\nValidation Performance:") + y_val = val_df['time_to_arrival_seconds'].values + val_preds = model.predict(val_df) + val_metrics = compute_all_metrics(y_val, val_preds, prefix="val_") + print_metrics_table(val_metrics, "Validation Metrics") + + # Evaluate on test set + print("\nTest Performance:") + y_test = test_df['time_to_arrival_seconds'].values + test_preds = model.predict(test_df) + test_metrics = compute_all_metrics(y_test, test_preds, prefix="test_") + print_metrics_table(test_metrics, "Test Metrics") + + # Get sample coefficients + sample_coefs = model.get_coefficients(route_id=route_id) + if sample_coefs: + print(f"\nModel coefficients:") + print(f" Intercept: {sample_coefs['intercept']:.2f}") + print(f" Coefficients: {[f'{c:.6f}' for c in sample_coefs['coefficients'][:5]]}") + + # Prepare metadata + metadata = { + 'model_type': 'polyreg_distance', + 'dataset': dataset_name, + 'route_id': route_id, + 'degree': degree, + 'alpha': alpha, + 'route_specific': route_specific, + 'n_models': len(model.models) if route_specific else 1, + 'n_samples': len(train_df) + len(val_df) + len(test_df), + 'n_trips': dataset.df['trip_id'].nunique() if route_id else None, + 'train_samples': len(train_df), + 'test_samples': len(test_df), + 'metrics': {**val_metrics, **test_metrics} + } + + # Save model + if save_model: + model_key = ModelKey.generate( + model_type='polyreg_distance', + dataset_name=dataset_name, + feature_groups=['distance'], + route_id=route_id, + degree=degree, + route_specific='yes' if route_specific else 'no' + ) + + registry = get_registry() + registry.save_model(model_key, model, metadata) + metadata['model_key'] = model_key + + return { + 'model': model, + 'metrics': metadata['metrics'], + 'metadata': metadata + } + + +if __name__ == "__main__": + # Train different configurations + + # Degree 2, global model + result1 = train_polyreg_distance( + degree=2, + alpha=1.0, + route_specific=False + ) + + # Degree 3, global model + result2 = train_polyreg_distance( + degree=3, + alpha=1.0, + route_specific=False + ) + + # Degree 2, route-specific + result3 = train_polyreg_distance( + degree=2, + alpha=1.0, + route_specific=True + ) + + # Compare results + print("\n" + "="*60) + print("Model Comparison (Test MAE)") + print("="*60) + for i, result in enumerate([result1, result2, result3], 1): + mae_min = result['metrics']['test_mae_minutes'] + print(f"Model {i}: {mae_min:.3f} minutes") diff --git a/eta_prediction/models/polyreg_time/predict.py b/eta_prediction/models/polyreg_time/predict.py new file mode 100644 index 0000000..107e996 --- /dev/null +++ b/eta_prediction/models/polyreg_time/predict.py @@ -0,0 +1,99 @@ +""" +Prediction interface for Polynomial Regression Time model. +""" + +import pandas as pd +from typing import Dict, Optional +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.registry import get_registry +from common.utils import format_seconds + + +def predict_eta(model_key: str, + distance_to_stop: float, + progress_on_segment: Optional[float] = None, + progress_ratio: Optional[float] = None, + hour: Optional[int] = None, + day_of_week: Optional[int] = None, + is_peak_hour: Optional[bool] = None, + is_weekend: Optional[bool] = None, + is_holiday: Optional[bool] = None, + temperature_c: Optional[float] = None, + precipitation_mm: Optional[float] = None, + wind_speed_kmh: Optional[float] = None) -> Dict: + """ + Predict ETA using polynomial regression time model. + + Args: + model_key: Model identifier + distance_to_stop: Distance in meters + progress_on_segment: 0-1 fraction along the current stop-to-stop segment + progress_ratio: 0-1 fraction along the overall route + hour: Hour of day (0-23) + day_of_week: Day of week (0=Monday) + is_peak_hour: Peak hour flag + is_weekend: Weekend flag + is_holiday: Holiday flag + temperature_c: Temperature + precipitation_mm: Precipitation + wind_speed_kmh: Wind speed + + Returns: + Dictionary with prediction and metadata + """ + # Load model + registry = get_registry() + model = registry.load_model(model_key) + metadata = registry.load_metadata(model_key) + + # Prepare input + input_data = {'distance_to_stop': [distance_to_stop]} + + # Add optional features + optional_features = { + 'progress_on_segment': progress_on_segment, + 'progress_ratio': progress_ratio, + 'hour': hour, + 'day_of_week': day_of_week, + 'is_peak_hour': is_peak_hour, + 'is_weekend': is_weekend, + 'is_holiday': is_holiday, + 'temperature_c': temperature_c, + 'precipitation_mm': precipitation_mm, + 'wind_speed_kmh': wind_speed_kmh + } + + for key, value in optional_features.items(): + if value is not None: + input_data[key] = [value] + + input_df = pd.DataFrame(input_data) + + # Predict + eta_seconds = model.predict(input_df)[0] + + return { + 'eta_seconds': float(eta_seconds), + 'eta_minutes': float(eta_seconds / 60), + 'eta_formatted': format_seconds(eta_seconds), + 'model_key': model_key, + 'model_type': 'polyreg_time', + 'distance_to_stop_m': distance_to_stop, + 'features_used': list(input_data.keys()) + } + + +def batch_predict(model_key: str, input_df: pd.DataFrame) -> pd.DataFrame: + """Batch prediction.""" + registry = get_registry() + model = registry.load_model(model_key) + + result_df = input_df.copy() + result_df['predicted_eta_seconds'] = model.predict(input_df) + result_df['predicted_eta_minutes'] = result_df['predicted_eta_seconds'] / 60 + + return result_df diff --git a/eta_prediction/models/polyreg_time/train.py b/eta_prediction/models/polyreg_time/train.py new file mode 100644 index 0000000..b9ab4d1 --- /dev/null +++ b/eta_prediction/models/polyreg_time/train.py @@ -0,0 +1,539 @@ +""" +Polynomial Regression Model - Time Enhanced +Fits polynomial features with temporal, spatial, and optional weather features. +""" + +import pandas as pd +import numpy as np +from typing import Dict, List, Optional, Tuple +from sklearn.preprocessing import PolynomialFeatures, StandardScaler, OneHotEncoder +from sklearn.linear_model import Ridge +from sklearn.pipeline import Pipeline +from sklearn.compose import ColumnTransformer +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.data import load_dataset +from common.metrics import compute_all_metrics +from common.keys import ModelKey +from common.registry import get_registry +from common.utils import print_metrics_table, train_test_summary, clip_predictions + + +def _make_one_hot_encoder() -> OneHotEncoder: + """Instantiate OneHotEncoder compatible with sklearn>=1.2 (sparse_output) and older.""" + try: + return OneHotEncoder(handle_unknown='ignore', sparse_output=False) + except TypeError: + return OneHotEncoder(handle_unknown='ignore', sparse=False) + + +class PolyRegTimeModel: + """ + Enhanced polynomial regression with temporal/spatial features. + + Features: + - Core: distance_to_stop (polynomialized) + - Temporal: hour, day_of_week, is_weekend, is_peak_hour + - Spatial: progress_on_segment, progress_ratio + - Weather: temperature_c, precipitation_mm, wind_speed_kmh + """ + + def __init__(self, + poly_degree: int = 2, + alpha: float = 1.0, + include_temporal: bool = True, + include_spatial: bool = True, + include_weather: bool = False, + handle_nan: str = 'drop'): # 'drop', 'impute', or 'error' + """ + Initialize model. + + Args: + poly_degree: Polynomial degree for distance + alpha: Ridge regularization strength + include_temporal: Include time-of-day features + include_spatial: Include spatial progress/segment features + include_weather: Include weather features + handle_nan: How to handle NaN - 'drop', 'impute', or 'error' + """ + self.poly_degree = poly_degree + self.alpha = alpha + self.include_temporal = include_temporal + self.include_spatial = include_spatial + self.include_weather = include_weather + self.handle_nan = handle_nan + self.model = None + self.feature_cols = None + self.available_features = None + self.categorical_features: List[str] = [] + self.numeric_features: List[str] = [] + + def _get_feature_groups(self) -> Dict[str, List[str]]: + """Define feature groups.""" + return { + 'core': ['distance_to_stop'], + 'spatial_numeric': ['progress_on_segment', 'progress_ratio'] + if self.include_spatial else [], + 'temporal': ['hour', 'day_of_week', 'is_weekend', 'is_peak_hour'] + if self.include_temporal else [], + 'weather': ['temperature_c', 'precipitation_mm', 'wind_speed_kmh'] + if self.include_weather else [] + } + + def _clean_data(self, df: pd.DataFrame, + target_col: str = 'time_to_arrival_seconds') -> pd.DataFrame: + """ + Clean data and handle NaN values. + + Args: + df: Input dataframe + target_col: Target column name + + Returns: + Cleaned dataframe + """ + print(f"\n{'='*60}") + print("Data Cleaning") + print(f"{'='*60}") + print(f"Initial rows: {len(df):,}") + + # Get all potential features + feature_groups = self._get_feature_groups() + all_features = [] + for group_features in feature_groups.values(): + all_features.extend(group_features) + + # Check which features are available and have acceptable NaN levels + available = [] + missing = [] + high_nan = [] + + for feat in all_features: + if feat not in df.columns: + missing.append(feat) + continue + + nan_ratio = df[feat].isna().sum() / len(df) + + if nan_ratio > 0.3: # More than 30% NaN + high_nan.append((feat, f"{nan_ratio*100:.1f}%")) + else: + available.append(feat) + + # Print feature availability + if missing: + print(f"\n⚠️ Missing features: {', '.join(missing)}") + if high_nan: + print(f"⚠️ High NaN features (>30%):") + for feat, ratio in high_nan: + print(f" - {feat}: {ratio} NaN") + + print(f"\n✓ Available features ({len(available)}):") + for feat in available: + nan_count = df[feat].isna().sum() + if nan_count > 0: + print(f" - {feat} ({nan_count:,} NaN)") + else: + print(f" - {feat}") + + # Store available features + self.available_features = available + + # Handle NaN based on strategy + if self.handle_nan == 'error': + # Check for any NaN in available features + target + check_cols = available + [target_col] + nan_counts = df[check_cols].isna().sum() + if nan_counts.sum() > 0: + print("\nNaN values found:") + for col, count in nan_counts[nan_counts > 0].items(): + print(f" {col}: {count}") + raise ValueError("NaN values found and handle_nan='error'") + df_clean = df + + elif self.handle_nan == 'drop': + # Drop rows with NaN in available features or target + check_cols = available + [target_col] + initial_len = len(df) + df_clean = df.dropna(subset=check_cols) + dropped = initial_len - len(df_clean) + + if dropped > 0: + pct = (dropped / initial_len) * 100 + print(f"\n✓ Dropped {dropped:,} rows ({pct:.2f}%) with NaN values") + + elif self.handle_nan == 'impute': + # Impute NaN values + df_clean = df.copy() + imputed = [] + + for feat in available: + nan_count = df_clean[feat].isna().sum() + if nan_count > 0: + if pd.api.types.is_numeric_dtype(df_clean[feat]): + fill_val = df_clean[feat].median() + df_clean[feat] = df_clean[feat].fillna(fill_val) + imputed.append(f"{feat} (median={fill_val:.2f})") + else: + mode_val = df_clean[feat].mode()[0] + df_clean[feat] = df_clean[feat].fillna(mode_val) + imputed.append(f"{feat} (mode={mode_val})") + + if imputed: + print(f"\n✓ Imputed features:") + for imp in imputed: + print(f" - {imp}") + + # Still drop rows with target NaN + target_nan = df_clean[target_col].isna().sum() + if target_nan > 0: + df_clean = df_clean.dropna(subset=[target_col]) + print(f"\n✓ Dropped {target_nan:,} rows with target NaN") + + else: + raise ValueError(f"Invalid handle_nan: {self.handle_nan}") + + print(f"\nFinal rows: {len(df_clean):,}") + + # Validate no NaN remains + remaining_nan = df_clean[available + [target_col]].isna().sum().sum() + if remaining_nan > 0: + raise ValueError(f"ERROR: {remaining_nan} NaN values remain after cleaning!") + + print("✓ No NaN values in features or target") + + return df_clean + + def _create_pipeline(self) -> Pipeline: + """Create sklearn pipeline.""" + if not self.feature_cols: + raise ValueError("Feature columns must be set before building pipeline") + + transformers = [] + + if 'distance_to_stop' in self.feature_cols: + transformers.append(( + 'poly_distance', + Pipeline([ + ('poly', PolynomialFeatures( + degree=self.poly_degree, + include_bias=False + )), + ('scale', StandardScaler()) + ]), + ['distance_to_stop'] + )) + + numeric_others = [ + col for col in self.numeric_features + if col != 'distance_to_stop' + ] + if numeric_others: + transformers.append(( + 'scale_numeric', + StandardScaler(), + numeric_others + )) + + if self.categorical_features: + transformers.append(( + 'encode_categorical', + _make_one_hot_encoder(), + self.categorical_features + )) + + return Pipeline([ + ('features', ColumnTransformer( + transformers, + remainder='drop', + sparse_threshold=0.0 + )), + ('ridge', Ridge(alpha=self.alpha)) + ]) + + def fit(self, train_df: pd.DataFrame, + target_col: str = 'time_to_arrival_seconds'): + """ + Train model. + + Args: + train_df: Training dataframe + target_col: Target column name + """ + # Clean data + train_clean = self._clean_data(train_df, target_col) + + # Build feature list from available features + feature_groups = self._get_feature_groups() + self.feature_cols = [] + + group_order = [ + 'core', + 'spatial_numeric', + 'temporal', + 'weather', + ] + for group in group_order: + for feat in feature_groups.get(group, []): + if feat in self.available_features and feat not in self.feature_cols: + self.feature_cols.append(feat) + + if not self.feature_cols: + raise ValueError("No features available after cleaning!") + + self.categorical_features = [] + self.numeric_features = list(self.feature_cols) + + print(f"\n{'='*60}") + print("Model Training") + print(f"{'='*60}") + print(f"Features ({len(self.feature_cols)}): {', '.join(self.feature_cols)}") + + # Prepare data + X = train_clean[self.feature_cols] + y = train_clean[target_col].values + + # Create and fit model + self.model = self._create_pipeline() + self.model.fit(X, y) + + print(f"✓ Model trained (poly_degree={self.poly_degree}, alpha={self.alpha})") + + def predict(self, X: pd.DataFrame) -> np.ndarray: + """ + Predict ETAs. + + Args: + X: DataFrame with features + + Returns: + Array of predicted ETAs in seconds + """ + if self.model is None: + raise ValueError("Model not trained") + + if self.feature_cols is None: + raise ValueError("Feature columns not set") + + # Check for missing features + missing = [f for f in self.feature_cols if f not in X.columns] + if missing: + raise ValueError(f"Missing features in input: {missing}") + + # Handle NaN in prediction data + X_pred = X[self.feature_cols].copy() + + if self.handle_nan == 'impute': + for col in self.numeric_features: + if X_pred[col].isna().any(): + X_pred[col] = X_pred[col].fillna(X_pred[col].median()) + for col in self.categorical_features: + if X_pred[col].isna().any(): + mode_series = X_pred[col].mode() + filler = mode_series.iloc[0] if not mode_series.empty else "missing" + X_pred[col] = X_pred[col].fillna(filler) + elif self.handle_nan == 'drop': + for col in self.numeric_features: + if X_pred[col].isna().any(): + X_pred[col] = X_pred[col].fillna(0.0) + for col in self.categorical_features: + if X_pred[col].isna().any(): + X_pred[col] = X_pred[col].fillna("missing") + + predictions = self.model.predict(X_pred[self.feature_cols]) + + return clip_predictions(predictions) + + def get_feature_importance(self) -> Dict[str, float]: + """Get feature coefficients (approximate importance).""" + if self.model is None: + return {} + + coefs = self.model.named_steps['ridge'].coef_ + transformer = self.model.named_steps['features'] + feature_names = transformer.get_feature_names_out() + + importance = { + name: abs(coef) + for name, coef in zip(feature_names, coefs) + } + + return dict(sorted(importance.items(), key=lambda x: x[1], reverse=True)) + + +def train_polyreg_time(dataset_name: str = "sample_dataset", + route_id: Optional[str] = None, + poly_degree: int = 2, + alpha: float = 1.0, + include_temporal: bool = True, + include_spatial: bool = True, + include_weather: bool = False, + handle_nan: str = 'drop', + test_size: float = 0.2, + save_model: bool = True, + pre_split: Optional[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]] = None) -> Dict: + """ + Train and evaluate polynomial regression time model. + + Args: + dataset_name: Dataset name + route_id: Optional route ID for route-specific training + poly_degree: Polynomial degree for distance + alpha: Ridge regularization + include_temporal: Include temporal features + include_weather: Include weather features + handle_nan: 'drop', 'impute', or 'error' + test_size: Test set fraction + save_model: Save to registry + pre_split: Optional (train, val, test) DataFrames to reuse + + Returns: + Dictionary with model, metrics, metadata + """ + print(f"\n{'='*60}") + print(f"Polynomial Regression Time Model".center(60)) + print(f"{'='*60}\n") + + route_info = f" (route: {route_id})" if route_id else " (global)" + print(f"Scope{route_info}") + print(f"Config:") + print(f" poly_degree={poly_degree}, alpha={alpha}") + print(f" temporal={include_temporal}, spatial={include_spatial}") + print(f" weather={include_weather}, handle_nan='{handle_nan}'") + + # Load dataset + print(f"\nLoading dataset: {dataset_name}") + dataset = load_dataset(dataset_name) + dataset.clean_data() + + # Filter by route if specified + if route_id is not None: + df = dataset.df + df_filtered = df[df['route_id'] == route_id].copy() + print(f"Filtered to route {route_id}: {len(df_filtered):,} samples") + + if len(df_filtered) == 0: + raise ValueError(f"No data found for route {route_id}") + + dataset.df = df_filtered + + if pre_split is not None: + train_df, val_df, test_df = (df.copy() for df in pre_split) + else: + train_df, val_df, test_df = dataset.temporal_split( + train_frac=1-test_size-0.1, + val_frac=0.1 + ) + + train_test_summary(train_df, test_df, val_df) + + # Train model + print("\n" + "="*60) + print("Training") + print("="*60) + model = PolyRegTimeModel( + poly_degree=poly_degree, + alpha=alpha, + include_temporal=include_temporal, + include_spatial=include_spatial, + include_weather=include_weather, + handle_nan=handle_nan + ) + model.fit(train_df) + + # Validation + print(f"\n{'='*60}") + print("Validation Performance") + print("="*60) + y_val = val_df['time_to_arrival_seconds'].values + val_preds = model.predict(val_df) + val_metrics = compute_all_metrics(y_val, val_preds, prefix="val_") + print_metrics_table(val_metrics, "Validation") + + # Test + print(f"\n{'='*60}") + print("Test Performance") + print("="*60) + y_test = test_df['time_to_arrival_seconds'].values + test_preds = model.predict(test_df) + test_metrics = compute_all_metrics(y_test, test_preds, prefix="test_") + print_metrics_table(test_metrics, "Test") + + # Feature importance + importance = model.get_feature_importance() + if importance: + print(f"\nTop 5 Features by Coefficient:") + for i, (feat, coef) in enumerate(list(importance.items())[:5], 1): + print(f" {i}. {feat}: {coef:.6f}") + + # Metadata + metadata = { + 'model_type': 'polyreg_time', + 'dataset': dataset_name, + 'route_id': route_id, + 'poly_degree': poly_degree, + 'alpha': alpha, + 'include_temporal': include_temporal, + 'include_spatial': include_spatial, + 'include_weather': include_weather, + 'handle_nan': handle_nan, + 'n_features': len(model.feature_cols) if model.feature_cols else 0, + 'features': model.feature_cols, + 'n_samples': len(train_df) + len(val_df) + len(test_df), + 'n_trips': dataset.df['trip_id'].nunique() if route_id else None, + 'train_samples': len(train_df), + 'test_samples': len(test_df), + 'metrics': {**val_metrics, **test_metrics} + } + + # Save + if save_model: + feature_groups = [] + if include_temporal: + feature_groups.append('temporal') + if include_spatial: + feature_groups.append('spatial') + if include_weather: + feature_groups.append('weather') + model_key = ModelKey.generate( + model_type='polyreg_time', + dataset_name=dataset_name, + feature_groups=feature_groups, + route_id=route_id, + degree=poly_degree, + handle_nan=handle_nan + ) + + registry = get_registry() + registry.save_model(model_key, model, metadata) + metadata['model_key'] = model_key + print(f"\n✓ Model saved: {model_key}") + + return { + 'model': model, + 'metrics': metadata['metrics'], + 'metadata': metadata + } + + +if __name__ == "__main__": + # Example: Train with different configurations + + # Basic model - drop NaN + result1 = train_polyreg_time( + poly_degree=2, + include_temporal=True, + include_weather=False, + handle_nan='drop' + ) + + # With imputation + result2 = train_polyreg_time( + poly_degree=2, + include_temporal=True, + include_weather=False, + handle_nan='impute' + ) diff --git a/eta_prediction/models/train_all_models.py b/eta_prediction/models/train_all_models.py new file mode 100644 index 0000000..5412996 --- /dev/null +++ b/eta_prediction/models/train_all_models.py @@ -0,0 +1,393 @@ +""" +Train all models either globally or per-route for comparative analysis. +""" + +import argparse +import sys +from pathlib import Path +import pandas as pd +from typing import Tuple + +sys.path.append(str(Path(__file__).parent)) + +from common.data import load_dataset +from historical_mean.train import train_historical_mean +from polyreg_distance.train import train_polyreg_distance +from polyreg_time.train import train_polyreg_time +from ewma.train import train_ewma +from xgb.train import train_xgboost + +DEFAULT_TEST_SIZE = 0.2 +DEFAULT_VAL_FRAC = 0.1 +DEFAULT_TRAIN_FRAC = 1.0 - DEFAULT_TEST_SIZE - DEFAULT_VAL_FRAC + + +def _temporal_split_df(df: pd.DataFrame, + train_frac: float, + val_frac: float) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """Deterministically split a dataframe by vp_ts.""" + df_sorted = df.sort_values('vp_ts').reset_index(drop=True) + n = len(df_sorted) + train_end = int(n * train_frac) + val_end = int(n * (train_frac + val_frac)) + train_df = df_sorted.iloc[:train_end].copy() + val_df = df_sorted.iloc[train_end:val_end].copy() + test_df = df_sorted.iloc[val_end:].copy() + return train_df, val_df, test_df + + +def _clone_splits(splits: Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """Return deep copies of precomputed splits so callers can mutate freely.""" + return tuple(split.copy() for split in splits) + + +def train_all_models(dataset_name: str, + by_route: bool = False, + model_types: list = None, + save: bool = True): + """ + Train all baseline models, optionally per-route. + + Args: + dataset_name: Dataset to train on + by_route: If True, train separate model for each route + model_types: List of model types to train (None = all) + save: Whether to save models to registry + """ + + if model_types is None: + model_types = [ + 'historical_mean', + 'polyreg_distance', + 'polyreg_time', + 'ewma', + 'xgboost', # NEW + ] + + print(f"\n{'='*80}") + print(f"TRAINING ALL MODELS".center(80)) + print(f"{'='*80}") + print(f"Dataset: {dataset_name}") + print(f"Mode: {'Route-Specific' if by_route else 'Global'}") + print(f"Models: {', '.join(model_types)}") + print(f"Save: {save}") + print(f"{'='*80}\n") + + # Load dataset to get routes / global data + dataset = load_dataset(dataset_name) + dataset.clean_data() + + if by_route: + routes = sorted(dataset.df['route_id'].unique()) + print(f"Found {len(routes)} routes: {routes}\n") + + # Get trip counts per route for summary + route_stats = dataset.df.groupby('route_id').agg({ + 'trip_id': 'nunique', + 'time_to_arrival_seconds': 'count' + }).rename(columns={ + 'trip_id': 'n_trips', + 'time_to_arrival_seconds': 'n_samples' + }) + + print("Route Statistics:") + print(route_stats.to_string()) + print() + + # Train models for each route + results = {} + + for route_id in routes: + n_trips = route_stats.loc[route_id, 'n_trips'] + n_samples = route_stats.loc[route_id, 'n_samples'] + route_df = dataset.df[dataset.df['route_id'] == route_id].copy() + if route_df.empty: + print(f"\nSkipping route {route_id}: no samples after cleaning") + continue + route_splits = _temporal_split_df( + route_df, + DEFAULT_TRAIN_FRAC, + DEFAULT_VAL_FRAC, + ) + + print(f"\n{'#'*80}") + print(f"ROUTE {route_id} ({n_trips} trips, {n_samples} samples)".center(80)) + print(f"{'#'*80}\n") + + route_results = {} + + try: + if 'historical_mean' in model_types: + print(f"\n>>> Training Historical Mean for route {route_id}...") + result = train_historical_mean( + dataset_name=dataset_name, + route_id=route_id, + save_model=save, + pre_split=_clone_splits(route_splits), + ) + route_results['historical_mean'] = result + + if 'polyreg_distance' in model_types: + print(f"\n>>> Training PolyReg Distance for route {route_id}...") + result = train_polyreg_distance( + dataset_name=dataset_name, + route_id=route_id, + degree=2, + save_model=save, + pre_split=_clone_splits(route_splits), + ) + route_results['polyreg_distance'] = result + + if 'polyreg_time' in model_types: + print(f"\n>>> Training PolyReg Time for route {route_id}...") + result = train_polyreg_time( + dataset_name=dataset_name, + route_id=route_id, + poly_degree=2, + include_temporal=True, + save_model=save, + pre_split=_clone_splits(route_splits), + ) + route_results['polyreg_time'] = result + + if 'ewma' in model_types: + print(f"\n>>> Training EWMA for route {route_id}...") + result = train_ewma( + dataset_name=dataset_name, + route_id=route_id, + alpha=0.3, + save_model=save, + pre_split=_clone_splits(route_splits), + ) + route_results['ewma'] = result + + # NEW: XGBoost route-specific + if 'xgboost' in model_types: + print(f"\n>>> Training XGBoost for route {route_id}...") + result = train_xgboost( + dataset_name=dataset_name, + route_id=route_id, + save_model=save, + pre_split=_clone_splits(route_splits), + ) + route_results['xgboost'] = result + + results[route_id] = route_results + + except Exception as e: + print(f"\n❌ ERROR training models for route {route_id}: {e}") + import traceback + traceback.print_exc() + continue + + # Print summary + print(f"\n{'='*80}") + print("TRAINING SUMMARY".center(80)) + print(f"{'='*80}\n") + + summary_data = [] + for route_id in routes: + if route_id not in results: + continue + + n_trips = route_stats.loc[route_id, 'n_trips'] + n_samples = route_stats.loc[route_id, 'n_samples'] + + for model_type in model_types: + if model_type in results[route_id]: + metrics = results[route_id][model_type]['metrics'] + summary_data.append({ + 'route_id': route_id, + 'n_trips': n_trips, + 'n_samples': n_samples, # NEW: number of observations + 'model_type': model_type, + 'test_mae_min': metrics.get('test_mae_minutes', None), + 'test_rmse_sec': metrics.get('test_rmse_seconds', None), + 'test_r2': metrics.get('test_r2', None) + }) + + if summary_data: + summary_df = pd.DataFrame(summary_data) + summary_df = summary_df.sort_values( + ['n_trips', 'model_type'], + ascending=[False, True] + ) + + print("Performance by Route and Model:") + print(summary_df.to_string(index=False)) + + # Show correlation between training data size (trips) and performance + if len(summary_df) > 2: + print(f"\n{'='*80}") + print("Data Size vs Performance Analysis".center(80)) + print(f"{'='*80}\n") + + for model_type in model_types: + model_data = summary_df[summary_df['model_type'] == model_type] + if len(model_data) > 1: + corr = model_data['n_trips'].corr(model_data['test_mae_min']) + print(f"{model_type:20s}: trips vs MAE correlation = {corr:+.3f}") + + # NEW: Correlation between number of observations and performance + print(f"\n{'='*80}") + print("Observations vs Performance Analysis".center(80)) + print(f"{'='*80}\n") + + for model_type in model_types: + model_data = summary_df[summary_df['model_type'] == model_type] + if len(model_data) > 1: + corr_mae = model_data['n_samples'].corr(model_data['test_mae_min']) + corr_rmse = model_data['n_samples'].corr(model_data['test_rmse_sec']) + print( + f"{model_type:20s}: " + f"obs vs MAE = {corr_mae:+.3f}, " + f"obs vs RMSE = {corr_rmse:+.3f}" + ) + + return results + + else: + # Train global models + print("Training GLOBAL models across all routes...\n") + + results = {} + global_splits = dataset.temporal_split( + train_frac=DEFAULT_TRAIN_FRAC, + val_frac=DEFAULT_VAL_FRAC, + ) + + try: + if 'historical_mean' in model_types: + print("\n>>> Training Historical Mean (global)...") + results['historical_mean'] = train_historical_mean( + dataset_name=dataset_name, + save_model=save, + pre_split=_clone_splits(global_splits), + ) + + if 'polyreg_distance' in model_types: + print("\n>>> Training PolyReg Distance (global)...") + results['polyreg_distance'] = train_polyreg_distance( + dataset_name=dataset_name, + degree=2, + save_model=save, + pre_split=_clone_splits(global_splits), + ) + + if 'polyreg_time' in model_types: + print("\n>>> Training PolyReg Time (global)...") + results['polyreg_time'] = train_polyreg_time( + dataset_name=dataset_name, + poly_degree=2, + include_temporal=True, + save_model=save, + pre_split=_clone_splits(global_splits), + ) + + if 'ewma' in model_types: + print("\n>>> Training EWMA (global)...") + results['ewma'] = train_ewma( + dataset_name=dataset_name, + alpha=0.3, + save_model=save, + pre_split=_clone_splits(global_splits), + ) + + # NEW: XGBoost global + if 'xgboost' in model_types: + print("\n>>> Training XGBoost (global)...") + results['xgboost'] = train_xgboost( + dataset_name=dataset_name, + route_id=None, # global model + save_model=save, + pre_split=_clone_splits(global_splits), + ) + + except Exception as e: + print(f"\n❌ ERROR training global models: {e}") + import traceback + traceback.print_exc() + + # Print summary + print(f"\n{'='*80}") + print("TRAINING SUMMARY".center(80)) + print(f"{'='*80}\n") + + for model_type, result in results.items(): + metrics = result['metrics'] + print(f"{model_type:20s}: MAE = {metrics['test_mae_minutes']:.3f} min, " + f"RMSE = {metrics['test_rmse_seconds']:.1f} sec, " + f"R² = {metrics.get('test_r2', 0):.3f}") + + return results + + +def main(): + parser = argparse.ArgumentParser( + description='Train ETA prediction models' + ) + + parser.add_argument( + '--dataset', + type=str, + default='sample_dataset', + help='Dataset name (default: sample_dataset)' + ) + + parser.add_argument( + '--by-route', + action='store_true', + help='Train separate models for each route' + ) + + parser.add_argument( + '--models', + type=str, + nargs='+', + choices=[ + 'historical_mean', + 'polyreg_distance', + 'polyreg_time', + 'ewma', + 'xgboost', # NEW + 'all' + ], + default=['all'], + help='Models to train (default: all)' + ) + + parser.add_argument( + '--no-save', + action='store_true', + help='Do not save models to registry (dry run)' + ) + + args = parser.parse_args() + + # Parse model types + if 'all' in args.models: + model_types = [ + 'historical_mean', + 'polyreg_distance', + 'polyreg_time', + 'ewma', + 'xgboost', # NEW + ] + else: + model_types = args.models + + # Train models + results = train_all_models( + dataset_name=args.dataset, + by_route=args.by_route, + model_types=model_types, + save=not args.no_save + ) + + print(f"\n{'='*80}") + print("✓ TRAINING COMPLETE".center(80)) + print(f"{'='*80}\n") + + +if __name__ == "__main__": + main() diff --git a/eta_prediction/models/xgb/predict.py b/eta_prediction/models/xgb/predict.py new file mode 100644 index 0000000..6695d47 --- /dev/null +++ b/eta_prediction/models/xgb/predict.py @@ -0,0 +1,113 @@ +""" +Prediction interface for XGBoost Time model. +""" + +import sys +from pathlib import Path +from typing import Dict, Optional + +import pandas as pd + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.registry import get_registry +from common.utils import format_seconds + + +def predict_eta( + model_key: str, + distance_to_stop: float, + progress_on_segment: Optional[float] = None, + progress_ratio: Optional[float] = None, + hour: Optional[int] = None, + day_of_week: Optional[int] = None, + is_peak_hour: Optional[bool] = None, + is_weekend: Optional[bool] = None, + is_holiday: Optional[bool] = None, + temperature_c: Optional[float] = None, + precipitation_mm: Optional[float] = None, + wind_speed_kmh: Optional[float] = None, +) -> Dict: + """ + Predict ETA using XGBoost time model. + + Args: + model_key: Model identifier + distance_to_stop: Distance in meters + progress_on_segment: 0-1 fraction of the current stop segment + progress_ratio: 0-1 fraction along the entire route + hour: Hour of day (0–23) + day_of_week: Day of week (0=Monday) + is_peak_hour: Peak hour flag + is_weekend: Weekend flag + is_holiday: Holiday flag + temperature_c: Temperature (°C) + precipitation_mm: Precipitation (mm) + wind_speed_kmh: Wind speed (km/h) + + Returns: + Dictionary with prediction and metadata. + """ + # Load model & metadata + registry = get_registry() + model = registry.load_model(model_key) + metadata = registry.load_metadata(model_key) + + # Prepare input + input_data = {"distance_to_stop": [distance_to_stop]} + + # Add optional features (only if provided) + optional_features = { + "progress_on_segment": progress_on_segment, + "progress_ratio": progress_ratio, + "hour": hour, + "day_of_week": day_of_week, + "is_peak_hour": is_peak_hour, + "is_weekend": is_weekend, + "is_holiday": is_holiday, + "temperature_c": temperature_c, + "precipitation_mm": precipitation_mm, + "wind_speed_kmh": wind_speed_kmh, + } + + for key, value in optional_features.items(): + if value is not None: + input_data[key] = [value] + + input_df = pd.DataFrame(input_data) + + # Predict + eta_seconds = float(model.predict(input_df)[0]) + + return { + "eta_seconds": eta_seconds, + "eta_minutes": eta_seconds / 60.0, + "eta_formatted": format_seconds(eta_seconds), + "model_key": model_key, + "model_type": metadata.get("model_type", "xgboost"), + "distance_to_stop_m": distance_to_stop, + "features_used": list(input_data.keys()), + } + + +def batch_predict(model_key: str, input_df: pd.DataFrame) -> pd.DataFrame: + """ + Batch prediction for XGBoost time model. + + Args: + model_key: Model identifier + input_df: DataFrame with required feature columns + + Returns: + DataFrame with additional prediction columns: + - predicted_eta_seconds + - predicted_eta_minutes + """ + registry = get_registry() + model = registry.load_model(model_key) + + result_df = input_df.copy() + result_df["predicted_eta_seconds"] = model.predict(input_df) + result_df["predicted_eta_minutes"] = result_df["predicted_eta_seconds"] / 60.0 + + return result_df diff --git a/eta_prediction/models/xgb/train.py b/eta_prediction/models/xgb/train.py new file mode 100644 index 0000000..5de5b27 --- /dev/null +++ b/eta_prediction/models/xgb/train.py @@ -0,0 +1,592 @@ +""" +XGBoost Regression Model - Time & Spatial Features +Fits a gradient boosted tree model with temporal, spatial, +and optional weather features for ETA prediction. +""" + +import sys +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import numpy as np +import pandas as pd +from sklearn.compose import ColumnTransformer +from sklearn.preprocessing import OneHotEncoder +from xgboost import XGBRegressor + +sys.path.append(str(Path(__file__).parent.parent)) + +from common.data import load_dataset +from common.metrics import compute_all_metrics +from common.keys import ModelKey +from common.registry import get_registry +from common.utils import print_metrics_table, train_test_summary, clip_predictions + + +def _make_one_hot_encoder() -> OneHotEncoder: + """Return a OneHotEncoder that works across sklearn versions.""" + try: + return OneHotEncoder(handle_unknown="ignore", sparse_output=False) + except TypeError: + return OneHotEncoder(handle_unknown="ignore", sparse=False) + + +class XGBTimeModel: + """ + Gradient boosted tree model (XGBoost) with temporal/spatial features. + + Features: + - Core: distance_to_stop + - Temporal: hour, day_of_week, is_weekend, is_peak_hour + - Spatial: progress_on_segment, progress_ratio + - Weather: temperature_c, precipitation_mm, wind_speed_kmh + """ + + def __init__( + self, + max_depth: int = 5, + n_estimators: int = 200, + learning_rate: float = 0.05, + subsample: float = 0.8, + colsample_bytree: float = 0.8, + include_temporal: bool = True, + include_spatial: bool = True, + include_weather: bool = False, + handle_nan: str = "drop", # 'drop', 'impute', or 'error' + random_state: int = 42, + n_jobs: int = 4, + ) -> None: + """ + Initialize model. + + Args: + max_depth: Maximum tree depth + n_estimators: Number of boosting stages + learning_rate: Learning rate (eta) + subsample: Row subsample ratio per tree + colsample_bytree: Column subsample ratio per tree + include_temporal: Include time-of-day features + include_spatial: Include spatial progress/segment features + include_weather: Include weather features + handle_nan: How to handle NaN - 'drop', 'impute', or 'error' + random_state: Random seed + n_jobs: Number of parallel threads + """ + self.max_depth = max_depth + self.n_estimators = n_estimators + self.learning_rate = learning_rate + self.subsample = subsample + self.colsample_bytree = colsample_bytree + self.include_temporal = include_temporal + self.include_spatial = include_spatial + self.include_weather = include_weather + self.handle_nan = handle_nan + self.random_state = random_state + self.n_jobs = n_jobs + + self.model: Optional[XGBRegressor] = None + self.feature_cols: Optional[List[str]] = None + self.available_features: Optional[List[str]] = None + self.preprocessor: Optional[ColumnTransformer] = None + self.categorical_features: List[str] = [] + self.numeric_features: List[str] = [] + + def _get_feature_groups(self) -> Dict[str, List[str]]: + """Define feature groups.""" + return { + "core": ["distance_to_stop"], + "spatial_numeric": ( + ["progress_on_segment", "progress_ratio"] + if self.include_spatial + else [] + ), + "temporal": ( + ["hour", "day_of_week", "is_weekend", "is_peak_hour"] + if self.include_temporal + else [] + ), + "weather": ( + ["temperature_c", "precipitation_mm", "wind_speed_kmh"] + if self.include_weather + else [] + ), + } + + def _build_preprocessor(self) -> None: + """Create a ColumnTransformer to handle numeric + categorical features.""" + if not self.feature_cols: + raise ValueError("Feature columns must be set before building the preprocessor") + + transformers = [] + numeric_features = [ + col for col in self.feature_cols if col not in self.categorical_features + ] + self.numeric_features = numeric_features + + if numeric_features: + transformers.append(( + "numeric", + "passthrough", + numeric_features, + )) + + if self.categorical_features: + transformers.append(( + "categorical", + _make_one_hot_encoder(), + self.categorical_features, + )) + + self.preprocessor = ColumnTransformer( + transformers, + remainder="drop", + sparse_threshold=0.0, + ) + + def _clean_data( + self, + df: pd.DataFrame, + target_col: str = "time_to_arrival_seconds", + ) -> pd.DataFrame: + """ + Clean data and handle NaN values. + + Args: + df: Input dataframe + target_col: Target column name + + Returns: + Cleaned dataframe + """ + print(f"\n{'=' * 60}") + print("Data Cleaning (XGBoost)".center(60)) + print(f"{'=' * 60}") + print(f"Initial rows: {len(df):,}") + + # Get all potential features + feature_groups = self._get_feature_groups() + all_features: List[str] = [] + for group_features in feature_groups.values(): + all_features.extend(group_features) + + # Check which features are available and have acceptable NaN levels + available: List[str] = [] + missing: List[str] = [] + high_nan: List[tuple[str, str]] = [] + + for feat in all_features: + if feat not in df.columns: + missing.append(feat) + continue + + nan_ratio = df[feat].isna().sum() / len(df) + + if nan_ratio > 0.3: # More than 30% NaN + high_nan.append((feat, f"{nan_ratio * 100:.1f}%")) + else: + available.append(feat) + + # Print feature availability + if missing: + print(f"\n⚠️ Missing features: {', '.join(missing)}") + if high_nan: + print("⚠️ High NaN features (>30%):") + for feat, ratio in high_nan: + print(f" - {feat}: {ratio} NaN") + + print(f"\n✓ Available features ({len(available)}):") + for feat in available: + nan_count = df[feat].isna().sum() + if nan_count > 0: + print(f" - {feat} ({nan_count:,} NaN)") + else: + print(f" - {feat}") + + # Store available features + self.available_features = available + + # Handle NaN based on strategy + if self.handle_nan == "error": + # Check for any NaN in available features + target + check_cols = available + [target_col] + nan_counts = df[check_cols].isna().sum() + if nan_counts.sum() > 0: + print("\nNaN values found:") + for col, count in nan_counts[nan_counts > 0].items(): + print(f" {col}: {count}") + raise ValueError("NaN values found and handle_nan='error'") + df_clean = df + + elif self.handle_nan == "drop": + # Drop rows with NaN in available features or target + check_cols = available + [target_col] + initial_len = len(df) + df_clean = df.dropna(subset=check_cols) + dropped = initial_len - len(df_clean) + + if dropped > 0: + pct = (dropped / initial_len) * 100 + print(f"\n✓ Dropped {dropped:,} rows ({pct:.2f}%) with NaN values") + + elif self.handle_nan == "impute": + # Impute NaN values + df_clean = df.copy() + imputed: List[str] = [] + + for feat in available: + nan_count = df_clean[feat].isna().sum() + if nan_count > 0: + if pd.api.types.is_numeric_dtype(df_clean[feat]): + fill_val = df_clean[feat].median() + df_clean[feat] = df_clean[feat].fillna(fill_val) + imputed.append(f"{feat} (median={fill_val:.2f})") + else: + mode_val = df_clean[feat].mode()[0] + df_clean[feat] = df_clean[feat].fillna(mode_val) + imputed.append(f"{feat} (mode={mode_val})") + + if imputed: + print(f"\n✓ Imputed features:") + for imp in imputed: + print(f" - {imp}") + + # Still drop rows with target NaN + target_nan = df_clean[target_col].isna().sum() + if target_nan > 0: + df_clean = df_clean.dropna(subset=[target_col]) + print(f"\n✓ Dropped {target_nan:,} rows with target NaN") + + else: + raise ValueError(f"Invalid handle_nan: {self.handle_nan}") + + print(f"\nFinal rows: {len(df_clean):,}") + + # Validate no NaN remains + remaining_nan = df_clean[available + [target_col]].isna().sum().sum() + if remaining_nan > 0: + raise ValueError( + f"ERROR: {remaining_nan} NaN values remain after cleaning!" + ) + + print("✓ No NaN values in features or target") + + return df_clean + + def fit( + self, + train_df: pd.DataFrame, + target_col: str = "time_to_arrival_seconds", + ) -> None: + """ + Train model. + + Args: + train_df: Training dataframe + target_col: Target column name + """ + # Clean data + train_clean = self._clean_data(train_df, target_col) + + # Build feature list from available features + feature_groups = self._get_feature_groups() + self.feature_cols = [] + + group_order = [ + "core", + "spatial_numeric", + "spatial_categorical", + "temporal", + "weather", + ] + for group in group_order: + for feat in feature_groups.get(group, []): + if feat in self.available_features and feat not in self.feature_cols: + self.feature_cols.append(feat) + + if not self.feature_cols: + raise ValueError("No features available after cleaning!") + + self.categorical_features = [] + self._build_preprocessor() + + print(f"\n{'=' * 60}") + print("Model Training (XGBoost)".center(60)) + print(f"{'=' * 60}") + print(f"Features ({len(self.feature_cols)}): {', '.join(self.feature_cols)}") + + # Prepare data + X = train_clean[self.feature_cols] + y = train_clean[target_col].values + + X_transformed = self.preprocessor.fit_transform(X) + + # Create and fit model + self.model = XGBRegressor( + max_depth=self.max_depth, + n_estimators=self.n_estimators, + learning_rate=self.learning_rate, + subsample=self.subsample, + colsample_bytree=self.colsample_bytree, + objective="reg:squarederror", + random_state=self.random_state, + n_jobs=self.n_jobs, + ) + self.model.fit(X_transformed, y) + + print( + f"✓ XGBoost model trained " + f"(max_depth={self.max_depth}, n_estimators={self.n_estimators}, " + f"learning_rate={self.learning_rate})" + ) + + def predict(self, X: pd.DataFrame) -> np.ndarray: + """ + Predict ETAs. + + Args: + X: DataFrame with features + + Returns: + Array of predicted ETAs in seconds + """ + if self.model is None: + raise ValueError("Model not trained") + + if self.feature_cols is None or self.preprocessor is None: + raise ValueError("Feature/preprocessor not set") + + # Check for missing features + missing = [f for f in self.feature_cols if f not in X.columns] + if missing: + raise ValueError(f"Missing features in input: {missing}") + + # Handle NaN in prediction data + X_pred = X[self.feature_cols].copy() + + if self.handle_nan == "impute": + for col in self.numeric_features: + if X_pred[col].isna().any(): + X_pred[col] = X_pred[col].fillna(X_pred[col].median()) + for col in self.categorical_features: + if X_pred[col].isna().any(): + mode_series = X_pred[col].mode() + filler = mode_series.iloc[0] if not mode_series.empty else "missing" + X_pred[col] = X_pred[col].fillna(filler) + elif self.handle_nan == "drop": + for col in self.numeric_features: + if X_pred[col].isna().any(): + X_pred[col] = X_pred[col].fillna(0.0) + for col in self.categorical_features: + if X_pred[col].isna().any(): + X_pred[col] = X_pred[col].fillna("missing") + + X_transformed = self.preprocessor.transform(X_pred[self.feature_cols]) + predictions = self.model.predict(X_transformed) + + return clip_predictions(predictions) + + def get_feature_importance(self) -> Dict[str, float]: + """Get feature importances as reported by XGBoost.""" + if self.model is None or self.preprocessor is None: + return {} + + if not hasattr(self.model, "feature_importances_"): + return {} + + importances = self.model.feature_importances_ + feature_names = self.preprocessor.get_feature_names_out() + + # Map back to feature names + importance = { + feat: float(imp) + for feat, imp in zip(feature_names, importances) + } + + # Sort by importance + return dict(sorted(importance.items(), key=lambda x: x[1], reverse=True)) + + +def train_xgboost( + dataset_name: str = "sample_dataset", + route_id: Optional[str] = None, + max_depth: int = 5, + n_estimators: int = 200, + learning_rate: float = 0.05, + subsample: float = 0.8, + colsample_bytree: float = 0.8, + include_temporal: bool = True, + include_spatial: bool = True, + include_weather: bool = False, + handle_nan: str = "drop", + test_size: float = 0.2, + save_model: bool = True, + pre_split: Optional[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]] = None, +) -> Dict: + """ + Train and evaluate XGBoost time model. + + Args: + dataset_name: Dataset name + route_id: Optional route ID for route-specific training + max_depth: Maximum tree depth + n_estimators: Number of boosting rounds + learning_rate: Learning rate (eta) + subsample: Row subsample ratio per tree + colsample_bytree: Column subsample ratio per tree + include_temporal: Include temporal features + include_spatial: Include spatial features + include_weather: Include weather features + handle_nan: 'drop', 'impute', or 'error' + test_size: Test set fraction (test); val is fixed at 0.1 + save_model: Save to registry + pre_split: Optional (train, val, test) DataFrames to reuse + + Returns: + Dictionary with model, metrics, metadata + """ + print(f"\n{'=' * 60}") + print("XGBoost Time Model".center(60)) + print(f"{'=' * 60}\n") + + route_info = f" (route: {route_id})" if route_id else " (global)" + print(f"Scope{route_info}") + print("Config:") + print(f" max_depth={max_depth}, n_estimators={n_estimators}, learning_rate={learning_rate}") + print(f" subsample={subsample}, colsample_bytree={colsample_bytree}") + print(f" temporal={include_temporal}, spatial={include_spatial}") + print(f" weather={include_weather}, handle_nan='{handle_nan}'") + + # Load dataset + print(f"\nLoading dataset: {dataset_name}") + dataset = load_dataset(dataset_name) + dataset.clean_data() + + # Filter by route if specified + if route_id is not None: + df = dataset.df + df_filtered = df[df["route_id"] == route_id].copy() + print(f"Filtered to route {route_id}: {len(df_filtered):,} samples") + + if len(df_filtered) == 0: + raise ValueError(f"No data found for route {route_id}") + + dataset.df = df_filtered + + # Split data (train / val / test) + if pre_split is not None: + train_df, val_df, test_df = (df.copy() for df in pre_split) + else: + train_df, val_df, test_df = dataset.temporal_split( + train_frac=1 - test_size - 0.1, + val_frac=0.1, + ) + + train_test_summary(train_df, test_df, val_df) + + # Train model + print("\n" + "=" * 60) + print("Training".center(60)) + print("=" * 60) + + model = XGBTimeModel( + max_depth=max_depth, + n_estimators=n_estimators, + learning_rate=learning_rate, + subsample=subsample, + colsample_bytree=colsample_bytree, + include_temporal=include_temporal, + include_spatial=include_spatial, + include_weather=include_weather, + handle_nan=handle_nan, + ) + model.fit(train_df) + + # Validation + print(f"\n{'=' * 60}") + print("Validation Performance".center(60)) + print(f"{'=' * 60}") + y_val = val_df["time_to_arrival_seconds"].values + val_preds = model.predict(val_df) + val_metrics = compute_all_metrics(y_val, val_preds, prefix="val_") + print_metrics_table(val_metrics, "Validation") + + # Test + print(f"\n{'=' * 60}") + print("Test Performance".center(60)) + print(f"{'=' * 60}") + y_test = test_df["time_to_arrival_seconds"].values + test_preds = model.predict(test_df) + test_metrics = compute_all_metrics(y_test, test_preds, prefix="test_") + print_metrics_table(test_metrics, "Test") + + # Feature importance + importance = model.get_feature_importance() + if importance: + print(f"\nTop 5 Features by Importance:") + for i, (feat, imp) in enumerate(list(importance.items())[:5], 1): + print(f" {i}. {feat}: {imp:.6f}") + + # Metadata + metadata = { + "model_type": "xgboost", + "dataset": dataset_name, + "route_id": route_id, + "max_depth": max_depth, + "n_estimators": n_estimators, + "learning_rate": learning_rate, + "subsample": subsample, + "colsample_bytree": colsample_bytree, + "include_temporal": include_temporal, + "include_spatial": include_spatial, + "include_weather": include_weather, + "handle_nan": handle_nan, + "n_features": len(model.feature_cols) if model.feature_cols else 0, + "features": model.feature_cols, + "n_samples": len(train_df) + len(val_df) + len(test_df), + "n_trips": dataset.df["trip_id"].nunique() if route_id else None, + "train_samples": len(train_df), + "test_samples": len(test_df), + "metrics": {**val_metrics, **test_metrics}, + } + + # Save + if save_model: + feature_groups = [] + if include_temporal: + feature_groups.append("temporal") + if include_spatial: + feature_groups.append("spatial") + if include_weather: + feature_groups.append("weather") + + model_key = ModelKey.generate( + model_type="xgboost", + dataset_name=dataset_name, + feature_groups=feature_groups, + route_id=route_id, + max_depth=max_depth, + n_estimators=n_estimators, + learning_rate=learning_rate, + handle_nan=handle_nan, + ) + + registry = get_registry() + registry.save_model(model_key, model, metadata) + metadata["model_key"] = model_key + print(f"\n✓ Model saved: {model_key}") + + return { + "model": model, + "metrics": metadata["metrics"], + "metadata": metadata, + } + + +if __name__ == "__main__": + # Example: basic global model + _ = train_xgboost( + dataset_name="sample_dataset", + include_temporal=True, + include_weather=False, + handle_nan="drop", + ) diff --git a/eta_prediction/pyproject.toml b/eta_prediction/pyproject.toml new file mode 100644 index 0000000..64a71e7 --- /dev/null +++ b/eta_prediction/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "eta_prediction" +version = "0.1.0" +description = "Utilities for exploring GTFS-Realtime feeds (Vehicle Positions, Trip Updates, Alerts)." +authors = [ + { name = "Jæ" } +] +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.11" + +dependencies = [ + "Django>=5.0", + "celery>=5.4", + "redis>=5.0", + "psycopg[binary,pool]>=3.2", + "django-environ>=0.11", + "python-dotenv>=1.0", + "requests>=2.32", + "gtfs-realtime-bindings>=1.0.0", + "numpy>=1.26", + "pandas>=2.3.3", + "fastparquet>=2024.11.0", + "psycopg>=3.2.11", + "matplotlib>=3.9.4", + "scikit-learn>=1.7.2", + "xgboost>=3.1.2", +] + +[tool.uv.workspace] +members = [ + "feature_engineering/proj", + "feature_engineering", +] diff --git a/eta_prediction/uv.lock b/eta_prediction/uv.lock new file mode 100644 index 0000000..c2f1277 --- /dev/null +++ b/eta_prediction/uv.lock @@ -0,0 +1,1819 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] + +[manifest] +members = [ + "eta-prediction", + "feature-engineering", +] + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "asgiref" +version = "3.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483, upload-time = "2025-10-05T09:15:06.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050, upload-time = "2025-10-05T09:15:05.11Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "billiard" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/6a/1405343016bce8354b29d90aad6b0bf6485b5e60404516e4b9a3a9646cf0/billiard-4.2.2.tar.gz", hash = "sha256:e815017a062b714958463e07ba15981d802dc53d41c5b69d28c5a7c238f8ecf3", size = 155592, upload-time = "2025-09-20T14:44:40.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/80/ef8dff49aae0e4430f81842f7403e14e0ca59db7bbaf7af41245b67c6b25/billiard-4.2.2-py3-none-any.whl", hash = "sha256:4bc05dcf0d1cc6addef470723aac2a6232f3c7ed7475b0b580473a9145829457", size = 86896, upload-time = "2025-09-20T14:44:39.157Z" }, +] + +[[package]] +name = "black" +version = "24.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468, upload-time = "2024-10-07T19:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270, upload-time = "2024-10-07T19:25:24.291Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061, upload-time = "2024-10-07T19:23:52.18Z" }, + { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293, upload-time = "2024-10-07T19:24:41.7Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256, upload-time = "2024-10-07T19:27:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534, upload-time = "2024-10-07T19:26:44.953Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892, upload-time = "2024-10-07T19:24:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796, upload-time = "2024-10-07T19:25:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986, upload-time = "2024-10-07T19:28:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085, upload-time = "2024-10-07T19:28:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928, upload-time = "2024-10-07T19:24:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875, upload-time = "2024-10-07T19:24:42.762Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898, upload-time = "2024-10-07T19:20:48.317Z" }, +] + +[[package]] +name = "celery" +version = "5.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "cligj" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/0d/837dbd5d8430fd0f01ed72c4cfb2f548180f4c68c635df84ce87956cff32/cligj-0.7.2.tar.gz", hash = "sha256:a4bc13d623356b373c2c27c53dbd9c68cae5d526270bfa71f6c6fa69669c6b27", size = 9803, upload-time = "2021-05-28T21:23:27.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/86/43fa9f15c5b9fb6e82620428827cd3c284aa933431405d1bcf5231ae3d3e/cligj-0.7.2-py3-none-any.whl", hash = "sha256:c1ca117dbce1fe20a5809dc96f01e1c2840f6dcc939b3ddbb1111bf330ba82df", size = 7069, upload-time = "2021-05-28T21:23:26.877Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cramjam" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/12/34bf6e840a79130dfd0da7badfb6f7810b8fcfd60e75b0539372667b41b6/cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4", size = 99100, upload-time = "2025-07-27T21:25:07.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/89/8001f6a9b6b6e9fa69bec5319789083475d6f26d52aaea209d3ebf939284/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04cfa39118570e70e920a9b75c733299784b6d269733dbc791d9aaed6edd2615", size = 3559272, upload-time = "2025-07-27T21:22:01.988Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/001d00070ca92e5fbe6aacc768e455568b0cde46b0eb944561a4ea132300/cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66a18f68506290349a256375d7aa2f645b9f7993c10fc4cc211db214e4e61d2b", size = 1861743, upload-time = "2025-07-27T21:22:03.754Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/041a3af01bf3f6158f120070f798546d4383b962b63c35cd91dcbf193e17/cramjam-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50e7d65533857736cd56f6509cf2c4866f28ad84dd15b5bdbf2f8a81e77fa28a", size = 1699631, upload-time = "2025-07-27T21:22:05.192Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/5358b238808abebd0c949c42635c3751204ca7cf82b29b984abe9f5e33c8/cramjam-2.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f71989668458fc327ac15396db28d92df22f8024bb12963929798b2729d2df5", size = 2025603, upload-time = "2025-07-27T21:22:06.726Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/19dba7c03a27408d8d11b5a7a4a7908459cfd4e6f375b73264dc66517bf6/cramjam-2.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee77ac543f1e2b22af1e8be3ae589f729491b6090582340aacd77d1d757d9569", size = 1766283, upload-time = "2025-07-27T21:22:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ad/40e4b3408501d886d082db465c33971655fe82573c535428e52ab905f4d0/cramjam-2.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad52784120e7e4d8a0b5b0517d185b8bf7f74f5e17272857ddc8951a628d9be1", size = 1854407, upload-time = "2025-07-27T21:22:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/36/6e/c1b60ceb6d7ea6ff8b0bf197520aefe23f878bf2bfb0de65f2b0c2f82cd1/cramjam-2.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b86f8e6d9c1b3f9a75b2af870c93ceee0f1b827cd2507387540e053b35d7459", size = 2035793, upload-time = "2025-07-27T21:22:12.504Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ad/32a8d5f4b1e3717787945ec6d71bd1c6e6bccba4b7e903fc0d9d4e4b08c3/cramjam-2.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:320d61938950d95da2371b46c406ec433e7955fae9f396c8e1bf148ffc187d11", size = 2067499, upload-time = "2025-07-27T21:22:14.067Z" }, + { url = "https://files.pythonhosted.org/packages/ff/cd/3b5a662736ea62ff7fa4c4a10a85e050bfdaad375cc53dc80427e8afe41c/cramjam-2.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41eafc8c1653a35a5c7e75ad48138f9f60085cc05cd99d592e5298552d944e9f", size = 1981853, upload-time = "2025-07-27T21:22:15.908Z" }, + { url = "https://files.pythonhosted.org/packages/26/8e/1dbcfaaa7a702ee82ee683ec3a81656934dd7e04a7bc4ee854033686f98a/cramjam-2.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03a7316c6bf763dfa34279335b27702321da44c455a64de58112968c0818ec4a", size = 2034514, upload-time = "2025-07-27T21:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/f11709bfdce74af79a88b410dcb76dedc97612166e759136931bf63cfd7b/cramjam-2.11.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:244c2ed8bd7ccbb294a2abe7ca6498db7e89d7eb5e744691dc511a7dc82e65ca", size = 2155343, upload-time = "2025-07-27T21:22:18.854Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/3b98b61841a5376d9a9b8468ae58753a8e6cf22be9534a0fa5af4d8621cc/cramjam-2.11.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:405f8790bad36ce0b4bbdb964ad51507bfc7942c78447f25cb828b870a1d86a0", size = 2169367, upload-time = "2025-07-27T21:22:20.389Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/bd5db5c49dbebc8b002f1c4983101b28d2e7fc9419753db1c31ec22b03ef/cramjam-2.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b1b751a5411032b08fb3ac556160229ca01c6bbe4757bb3a9a40b951ebaac23", size = 2159334, upload-time = "2025-07-27T21:22:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/34/32/203c57acdb6eea727e7078b2219984e64ed4ad043c996ed56321301ba167/cramjam-2.11.0-cp311-cp311-win32.whl", hash = "sha256:5251585608778b9ac8effed544933df7ad85b4ba21ee9738b551f17798b215ac", size = 1605313, upload-time = "2025-07-27T21:22:24.126Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bd/102d6deb87a8524ac11cddcd31a7612b8f20bf9b473c3c645045e3b957c7/cramjam-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:dca88bc8b68ce6d35dafd8c4d5d59a238a56c43fa02b74c2ce5f9dfb0d1ccb46", size = 1710991, upload-time = "2025-07-27T21:22:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/7c84c913a5fae85b773a9dcf8874390f9d68ba0fcc6630efa7ff1541b950/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dba5c14b8b4f73ea1e65720f5a3fe4280c1d27761238378be8274135c60bbc6e", size = 3553368, upload-time = "2025-07-27T21:22:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/4f6d185d8a744776f53035e72831ff8eefc2354f46ab836f4bd3c4f6c138/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11eb40722b3fcf3e6890fba46c711bf60f8dc26360a24876c85e52d76c33b25b", size = 1860014, upload-time = "2025-07-27T21:22:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a8/626c76263085c6d5ded0e71823b411e9522bfc93ba6cc59855a5869296e7/cramjam-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeb26e2898994b6e8319f19a4d37c481512acdcc6d30e1b5ecc9d8ec57e835cb", size = 1693512, upload-time = "2025-07-27T21:22:30.999Z" }, + { url = "https://files.pythonhosted.org/packages/e9/52/0851a16a62447532e30ba95a80e638926fdea869a34b4b5b9d0a020083ba/cramjam-2.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f8d82081ed7d8fe52c982bd1f06e4c7631a73fe1fb6d4b3b3f2404f87dc40fe", size = 2025285, upload-time = "2025-07-27T21:22:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/122e444f59dbc216451d8e3d8282c9665dc79eaf822f5f1470066be1b695/cramjam-2.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:092a3ec26e0a679305018380e4f652eae1b6dfe3fc3b154ee76aa6b92221a17c", size = 1761327, upload-time = "2025-07-27T21:22:34.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bc/3a0189aef1af2b29632c039c19a7a1b752bc21a4053582a5464183a0ad3d/cramjam-2.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:529d6d667c65fd105d10bd83d1cd3f9869f8fd6c66efac9415c1812281196a92", size = 1854075, upload-time = "2025-07-27T21:22:36.157Z" }, + { url = "https://files.pythonhosted.org/packages/2e/80/8a6343b13778ce52d94bb8d5365a30c3aa951276b1857201fe79d7e2ad25/cramjam-2.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:555eb9c90c450e0f76e27d9ff064e64a8b8c6478ab1a5594c91b7bc5c82fd9f0", size = 2032710, upload-time = "2025-07-27T21:22:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/cd1778a207c29eda10791e3dfa018b588001928086e179fc71254793c625/cramjam-2.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5edf4c9e32493035b514cf2ba0c969d81ccb31de63bd05490cc8bfe3b431674e", size = 2068353, upload-time = "2025-07-27T21:22:39.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f0/5c2a5cd5711032f3b191ca50cb786c17689b4a9255f9f768866e6c9f04d9/cramjam-2.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2fe41f48c4d58d923803383b0737f048918b5a0d10390de9628bb6272b107", size = 1978104, upload-time = "2025-07-27T21:22:41.106Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8b/b363a5fb2c3347504fe9a64f8d0f1e276844f0e532aa7162c061cd1ffee4/cramjam-2.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca14cf1cabdb0b77d606db1bb9e9ca593b1dbd421fcaf251ec9a5431ec449f3", size = 2030779, upload-time = "2025-07-27T21:22:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/78/7b/d83dad46adb6c988a74361f81ad9c5c22642be53ad88616a19baedd06243/cramjam-2.11.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:309e95bf898829476bccf4fd2c358ec00e7ff73a12f95a3cdeeba4bb1d3683d5", size = 2155297, upload-time = "2025-07-27T21:22:44.6Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/60d9be4cb33d8740a4aa94c7513f2ef3c4eba4fd13536f086facbafade71/cramjam-2.11.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:86dca35d2f15ef22922411496c220f3c9e315d5512f316fe417461971cc1648d", size = 2169255, upload-time = "2025-07-27T21:22:46.534Z" }, + { url = "https://files.pythonhosted.org/packages/11/b0/4a595f01a243aec8ad272b160b161c44351190c35d98d7787919d962e9e5/cramjam-2.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:193c6488bd2f514cbc0bef5c18fad61a5f9c8d059dd56edf773b3b37f0e85496", size = 2155651, upload-time = "2025-07-27T21:22:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/47/7776659aaa677046b77f527106e53ddd47373416d8fcdb1e1a881ec5dc06/cramjam-2.11.0-cp312-cp312-win32.whl", hash = "sha256:514e2c008a8b4fa823122ca3ecab896eac41d9aa0f5fc881bd6264486c204e32", size = 1603568, upload-time = "2025-07-27T21:22:50.084Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/d53002729cfd94c5844ddfaf1233c86d29f2dbfc1b764a6562c41c044199/cramjam-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:53fed080476d5f6ad7505883ec5d1ec28ba36c2273db3b3e92d7224fe5e463db", size = 1709287, upload-time = "2025-07-27T21:22:51.534Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/406c5dc0f8e82385519d8c299c40fd6a56d97eca3fcd6f5da8dad48de75b/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c289729cc1c04e88bafa48b51082fb462b0a57dbc96494eab2be9b14dca62af", size = 3553330, upload-time = "2025-07-27T21:22:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/00/ad/4186884083d6e4125b285903e17841827ab0d6d0cffc86216d27ed91e91d/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:045201ee17147e36cf43d8ae2fa4b4836944ac672df5874579b81cf6d40f1a1f", size = 1859756, upload-time = "2025-07-27T21:22:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/54/01/91b485cf76a7efef638151e8a7d35784dae2c4ff221b1aec2c083e4b106d/cramjam-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:619cd195d74c9e1d2a3ad78d63451d35379c84bd851aec552811e30842e1c67a", size = 1693609, upload-time = "2025-07-27T21:22:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/d0c80d279b2976870fc7d10f15dcb90a3c10c06566c6964b37c152694974/cramjam-2.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6eb3ae5ab72edb2ed68bdc0f5710f0a6cad7fd778a610ec2c31ee15e32d3921e", size = 2024912, upload-time = "2025-07-27T21:22:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/d6/70/88f2a5cb904281ed5d3c111b8f7d5366639817a5470f059bcd26833fc870/cramjam-2.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7da3f4b19e3078f9635f132d31b0a8196accb2576e3213ddd7a77f93317c20", size = 1760715, upload-time = "2025-07-27T21:22:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/cf5b02081132537d28964fb385fcef9ed9f8a017dd7d8c59d317e53ba50d/cramjam-2.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57286b289cd557ac76c24479d8ecfb6c3d5b854cce54ccc7671f9a2f5e2a2708", size = 1853782, upload-time = "2025-07-27T21:23:01.07Z" }, + { url = "https://files.pythonhosted.org/packages/57/27/63525087ed40a53d1867021b9c4858b80cc86274ffe7225deed067d88d92/cramjam-2.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28952fbbf8b32c0cb7fa4be9bcccfca734bf0d0989f4b509dc7f2f70ba79ae06", size = 2032354, upload-time = "2025-07-27T21:23:03.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ef/dbba082c6ebfb6410da4dd39a64e654d7194fcfd4567f85991a83fa4ec32/cramjam-2.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ed2e4099812a438b545dfbca1928ec825e743cd253bc820372d6ef8c3adff4", size = 2068007, upload-time = "2025-07-27T21:23:04.526Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/d902b9358a46a086938feae83b2251720e030f06e46006f4c1fc0ac9da20/cramjam-2.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9aecd5c3845d415bd6c9957c93de8d93097e269137c2ecb0e5a5256374bdc8", size = 1977485, upload-time = "2025-07-27T21:23:06.058Z" }, + { url = "https://files.pythonhosted.org/packages/e8/03/982f54553244b0afcbdb2ad2065d460f0ab05a72a96896a969a1ca136a1e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:362fcf4d6f5e1242a4540812455f5a594949190f6fbc04f2ffbfd7ae0266d788", size = 2030447, upload-time = "2025-07-27T21:23:07.679Z" }, + { url = "https://files.pythonhosted.org/packages/74/5f/748e54cdb665ec098ec519e23caacc65fc5ae58718183b071e33fc1c45b4/cramjam-2.11.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:13240b3dea41b1174456cb9426843b085dc1a2bdcecd9ee2d8f65ac5703374b0", size = 2154949, upload-time = "2025-07-27T21:23:09.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/81/c4e6cb06ed69db0dc81f9a8b1dc74995ebd4351e7a1877143f7031ff2700/cramjam-2.11.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:c54eed83726269594b9086d827decc7d2015696e31b99bf9b69b12d9063584fe", size = 2168925, upload-time = "2025-07-27T21:23:10.976Z" }, + { url = "https://files.pythonhosted.org/packages/13/5b/966365523ce8290a08e163e3b489626c5adacdff2b3da9da1b0823dfb14e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f8195006fdd0fc0a85b19df3d64a3ef8a240e483ae1dfc7ac6a4316019eb5df2", size = 2154950, upload-time = "2025-07-27T21:23:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7d/7f8eb5c534b72b32c6eb79d74585bfee44a9a5647a14040bb65c31c2572d/cramjam-2.11.0-cp313-cp313-win32.whl", hash = "sha256:ccf30e3fe6d770a803dcdf3bb863fa44ba5dc2664d4610ba2746a3c73599f2e4", size = 1603199, upload-time = "2025-07-27T21:23:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/37/05/47b5e0bf7c41a3b1cdd3b7c2147f880c93226a6bef1f5d85183040cbdece/cramjam-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee36348a204f0a68b03400f4736224e9f61d1c6a1582d7f875c1ca56f0254268", size = 1708924, upload-time = "2025-07-27T21:23:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/de/07/a1051cdbbe6d723df16d756b97f09da7c1adb69e29695c58f0392bc12515/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100", size = 3554141, upload-time = "2025-07-27T21:23:17.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/58487d2e16ef3d04f51a7c7f0e69823e806744b4c21101e89da4873074bc/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3", size = 1860353, upload-time = "2025-07-27T21:23:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/67/b4/67f6254d166ffbcc9d5fa1b56876eaa920c32ebc8e9d3d525b27296b693b/cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968", size = 1693832, upload-time = "2025-07-27T21:23:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/55/a3/4e0b31c0d454ae70c04684ed7c13d3c67b4c31790c278c1e788cb804fa4a/cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0", size = 2027080, upload-time = "2025-07-27T21:23:23.303Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c7/5e8eed361d1d3b8be14f38a54852c5370cc0ceb2c2d543b8ba590c34f080/cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e", size = 1761543, upload-time = "2025-07-27T21:23:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/09/0c/06b7f8b0ce9fde89470505116a01fc0b6cb92d406c4fb1e46f168b5d3fa5/cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9", size = 1854636, upload-time = "2025-07-27T21:23:26.987Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c6/6ebc02c9d5acdf4e5f2b1ec6e1252bd5feee25762246798ae823b3347457/cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25", size = 2032715, upload-time = "2025-07-27T21:23:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/a122971c23f5ca4b53e4322c647ac7554626c95978f92d19419315dddd05/cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936", size = 2069039, upload-time = "2025-07-27T21:23:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f6121b90b86b9093c066889274d26a1de3f29969d45c2ed1ecbe2033cb78/cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4", size = 1979566, upload-time = "2025-07-27T21:23:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/f95bc57fd7f4166ce6da816cfa917fb7df4bb80e669eb459d85586498414/cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403", size = 2030905, upload-time = "2025-07-27T21:23:33.696Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/e429de4e8bc86ee65e090dae0f87f45abd271742c63fb2d03c522ffde28a/cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839", size = 2155592, upload-time = "2025-07-27T21:23:35.375Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6c/65a7a0207787ad39ad804af4da7f06a60149de19481d73d270b540657234/cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778", size = 2170839, upload-time = "2025-07-27T21:23:37.197Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/5c5db505ba692bc844246b066e23901d5905a32baf2f33719c620e65887f/cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3", size = 2157236, upload-time = "2025-07-27T21:23:38.854Z" }, + { url = "https://files.pythonhosted.org/packages/b0/22/88e6693e60afe98901e5bbe91b8dea193e3aa7f42e2770f9c3339f5c1065/cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6", size = 1604136, upload-time = "2025-07-27T21:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f8/01618801cd59ccedcc99f0f96d20be67d8cfc3497da9ccaaad6b481781dd/cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c", size = 1710272, upload-time = "2025-07-27T21:23:42.236Z" }, + { url = "https://files.pythonhosted.org/packages/40/81/6cdb3ed222d13ae86bda77aafe8d50566e81a1169d49ed195b6263610704/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e", size = 3559671, upload-time = "2025-07-27T21:23:44.504Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/52b7e54fe5ba1ef0270d9fdc43dabd7971f70ea2d7179be918c997820247/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86", size = 1867876, upload-time = "2025-07-27T21:23:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/28/30d5b8d10acd30db3193bc562a313bff722888eaa45cfe32aa09389f2b24/cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800", size = 1695562, upload-time = "2025-07-27T21:23:48.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/86/ec806f986e01b896a650655024ea52a13e25c3ac8a3a382f493089483cdc/cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51", size = 2025056, upload-time = "2025-07-27T21:23:50.404Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/c2c17586b90848d29d63181f7d14b8bd3a7d00975ad46e3edf2af8af7e1f/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb", size = 1764084, upload-time = "2025-07-27T21:23:52.265Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68bc334fadb434a61df10071dc8606702aa4f5b6cdb2df62474fc21d2845/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5", size = 1854859, upload-time = "2025-07-27T21:23:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4e/b48e67835b5811ec5e9cb2e2bcba9c3fd76dab3e732569fe801b542c6ca9/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3", size = 2035970, upload-time = "2025-07-27T21:23:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/d2ac33d572b4d90f7f0f2c8a1d60fb48f06b128fdc2c05f9b49891bb0279/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89", size = 2069320, upload-time = "2025-07-27T21:23:57.494Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4c/85cec77af4a74308ba5fca8e296c4e2f80ec465c537afc7ab1e0ca2f9a00/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e", size = 1982668, upload-time = "2025-07-27T21:23:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/55/45/938546d1629e008cc3138df7c424ef892719b1796ff408a2ab8550032e5e/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4", size = 2034028, upload-time = "2025-07-27T21:24:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/b5a53e20505555f1640e66dcf70394bcf51a1a3a072aa18ea35135a0f9ed/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c", size = 2155513, upload-time = "2025-07-27T21:24:02.92Z" }, + { url = "https://files.pythonhosted.org/packages/84/12/8d3f6ceefae81bbe45a347fdfa2219d9f3ac75ebc304f92cd5fcb4fbddc5/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65", size = 2170035, upload-time = "2025-07-27T21:24:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3be6f0a1398f976070672be64f61895f8839857618a2d8cc0d3ab529d3dc/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656", size = 2160229, upload-time = "2025-07-27T21:24:06.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/66cfc3635511b20014bbb3f2ecf0095efb3049e9e96a4a9e478e4f3d7b78/cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8", size = 1610267, upload-time = "2025-07-27T21:24:08.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c6/c71e82e041c95ffe6a92ac707785500aa2a515a4339c2c7dd67e3c449249/cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188", size = 1713108, upload-time = "2025-07-27T21:24:10.147Z" }, + { url = "https://files.pythonhosted.org/packages/81/da/b3301962ccd6fce9fefa1ecd8ea479edaeaa38fadb1f34d5391d2587216a/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:52d5db3369f95b27b9f3c14d067acb0b183333613363ed34268c9e04560f997f", size = 3573546, upload-time = "2025-07-27T21:24:52.944Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c2/410ddb8ad4b9dfb129284666293cb6559479645da560f7077dc19d6bee9e/cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4820516366d455b549a44d0e2210ee7c4575882dda677564ce79092588321d54", size = 1873654, upload-time = "2025-07-27T21:24:54.958Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/f68a443c64f7ce7aff5bed369b0aa5b2fac668fa3dfd441837e316e97a1f/cramjam-2.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d9e5db525dc0a950a825202f84ee68d89a072479e07da98795a3469df942d301", size = 1702846, upload-time = "2025-07-27T21:24:57.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/02/0ff358ab773def1ee3383587906c453d289953171e9c92db84fdd01bf172/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62ab4971199b2270005359cdc379bc5736071dc7c9a228581c5122d9ffaac50c", size = 1773683, upload-time = "2025-07-27T21:24:59.28Z" }, + { url = "https://files.pythonhosted.org/packages/e9/31/3298e15f87c9cf2aabdbdd90b153d8644cf989cb42a45d68a1b71e1f7aaf/cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24758375cc5414d3035ca967ebb800e8f24604ececcba3c67d6f0218201ebf2d", size = 1994136, upload-time = "2025-07-27T21:25:01.565Z" }, + { url = "https://files.pythonhosted.org/packages/c7/90/20d1747255f1ee69a412e319da51ea594c18cca195e7a4d4c713f045eff5/cramjam-2.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c2eea545fef1065c7dd4eda991666fd9c783fbc1d226592ccca8d8891c02f23", size = 1714982, upload-time = "2025-07-27T21:25:05.79Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "django" +version = "5.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/96/bd84e2bb997994de8bcda47ae4560991084e86536541d7214393880f01a8/django-5.2.7.tar.gz", hash = "sha256:e0f6f12e2551b1716a95a63a1366ca91bbcd7be059862c1b18f989b1da356cdd", size = 10865812, upload-time = "2025-10-01T14:22:12.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/ef/81f3372b5dd35d8d354321155d1a38894b2b766f576d0abffac4d8ae78d9/django-5.2.7-py3-none-any.whl", hash = "sha256:59a13a6515f787dec9d97a0438cd2efac78c8aca1c80025244b0fe507fe0754b", size = 8307145, upload-time = "2025-10-01T14:22:49.476Z" }, +] + +[[package]] +name = "django-environ" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804, upload-time = "2025-01-13T17:03:37.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957, upload-time = "2025-01-13T17:03:32.918Z" }, +] + +[[package]] +name = "eta-prediction" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "celery" }, + { name = "django" }, + { name = "django-environ" }, + { name = "fastparquet" }, + { name = "gtfs-realtime-bindings" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "psycopg", extra = ["binary", "pool"] }, + { name = "python-dotenv" }, + { name = "redis" }, + { name = "requests" }, + { name = "scikit-learn" }, + { name = "xgboost" }, +] + +[package.metadata] +requires-dist = [ + { name = "celery", specifier = ">=5.4" }, + { name = "django", specifier = ">=5.0" }, + { name = "django-environ", specifier = ">=0.11" }, + { name = "fastparquet", specifier = ">=2024.11.0" }, + { name = "gtfs-realtime-bindings", specifier = ">=1.0.0" }, + { name = "matplotlib", specifier = ">=3.9.4" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "psycopg", specifier = ">=3.2.11" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.2" }, + { name = "python-dotenv", specifier = ">=1.0" }, + { name = "redis", specifier = ">=5.0" }, + { name = "requests", specifier = ">=2.32" }, + { name = "scikit-learn", specifier = ">=1.7.2" }, + { name = "xgboost", specifier = ">=3.1.2" }, +] + +[[package]] +name = "fastparquet" +version = "2024.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cramjam" }, + { name = "fsspec" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/66/862da14f5fde4eff2cedc0f51a8dc34ba145088e5041b45b2d57ac54f922/fastparquet-2024.11.0.tar.gz", hash = "sha256:e3b1fc73fd3e1b70b0de254bae7feb890436cb67e99458b88cb9bd3cc44db419", size = 467192, upload-time = "2024-11-15T19:30:10.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/51/e0d6e702523ac923ede6c05e240f4a02533ccf2cea9fec7a43491078e920/fastparquet-2024.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:374cdfa745aa7d5188430528d5841cf823eb9ad16df72ad6dadd898ccccce3be", size = 909934, upload-time = "2024-11-12T20:37:37.049Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c8/5c0fb644c19a8d80b2ae4d8aa7d90c2d85d0bd4a948c5c700bea5c2802ea/fastparquet-2024.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c8401bfd86cccaf0ab7c0ade58c91ae19317ff6092e1d4ad96c2178197d8124", size = 683844, upload-time = "2024-11-12T20:37:38.456Z" }, + { url = "https://files.pythonhosted.org/packages/33/4a/1e532fd1a0d4d8af7ffc7e3a8106c0bcd13ed914a93a61e299b3832dd3d2/fastparquet-2024.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9cca4c6b5969df5561c13786f9d116300db1ec22c7941e237cfca4ce602f59b", size = 1791698, upload-time = "2024-11-12T20:37:41.101Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e8/e1ede861bea68394a755d8be1aa2e2d60a3b9f6b551bfd56aeca74987e2e/fastparquet-2024.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a9387e77ac608d8978774caaf1e19de67eaa1386806e514dcb19f741b19cfe5", size = 1804289, upload-time = "2024-11-12T20:37:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/957090cccaede805583ca3f3e46e2762d0f9bf8860ecbce65197e47d84c1/fastparquet-2024.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6595d3771b3d587a31137e985f751b4d599d5c8e9af9c4858e373fdf5c3f8720", size = 1753638, upload-time = "2024-11-12T20:37:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/344787c685fd1531f07ae712a855a7c34d13deaa26c3fd4a9231bea7dbab/fastparquet-2024.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:053695c2f730b78a2d3925df7cd5c6444d6c1560076af907993361cc7accf3e2", size = 1814407, upload-time = "2024-11-12T20:37:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ec/ab9d5685f776a1965797eb68c4364c72edf57cd35beed2df49b34425d1df/fastparquet-2024.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a52eecc6270ae15f0d51347c3f762703dd667ca486f127dc0a21e7e59856ae5", size = 1874462, upload-time = "2024-11-12T20:37:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/90/4f/7a4ea9a7ddf0a3409873f0787f355806f9e0b73f42f2acecacdd9a8eff0a/fastparquet-2024.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:e29ff7a367fafa57c6896fb6abc84126e2466811aefd3e4ad4070b9e18820e54", size = 671023, upload-time = "2024-11-12T20:37:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/08/76/068ac7ec9b4fc783be21a75a6a90b8c0654da4d46934d969e524ce287787/fastparquet-2024.11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dbad4b014782bd38b58b8e9f514fe958cfa7a6c4e187859232d29fd5c5ddd849", size = 915968, upload-time = "2024-11-12T20:37:52.861Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9e/6d3b4188ad64ed51173263c07109a5f18f9c84a44fa39ab524fca7420cda/fastparquet-2024.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:403d31109d398b6be7ce84fa3483fc277c6a23f0b321348c0a505eb098a041cb", size = 685399, upload-time = "2024-11-12T20:37:54.899Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6c/809220bc9fbe83d107df2d664c3fb62fb81867be8f5218ac66c2e6b6a358/fastparquet-2024.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbbb9057a26acf0abad7adf58781ee357258b7708ee44a289e3bee97e2f55d42", size = 1758557, upload-time = "2024-11-12T20:37:56.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/b3b3e6ca2e531484289024138cd4709c22512b3fe68066d7f9849da4a76c/fastparquet-2024.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63e0e416e25c15daa174aad8ba991c2e9e5b0dc347e5aed5562124261400f87b", size = 1781052, upload-time = "2024-11-12T20:37:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/21/fe/97ed45092d0311c013996dae633122b7a51c5d9fe8dcbc2c840dc491201e/fastparquet-2024.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2d7f02f57231e6c86d26e9ea71953737202f20e948790e5d4db6d6a1a150dc", size = 1715797, upload-time = "2024-11-12T20:38:00.694Z" }, + { url = "https://files.pythonhosted.org/packages/24/df/02fa6aee6c0d53d1563b5bc22097076c609c4c5baa47056b0b4bed456fcf/fastparquet-2024.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fbe4468146b633d8f09d7b196fea0547f213cb5ce5f76e9d1beb29eaa9593a93", size = 1795682, upload-time = "2024-11-12T20:38:02.38Z" }, + { url = "https://files.pythonhosted.org/packages/b0/25/f4f87557589e1923ee0e3bebbc84f08b7c56962bf90f51b116ddc54f2c9f/fastparquet-2024.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:29d5c718817bcd765fc519b17f759cad4945974421ecc1931d3bdc3e05e57fa9", size = 1857842, upload-time = "2024-11-12T20:38:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f9/98cd0c39115879be1044d59c9b76e8292776e99bb93565bf990078fd11c4/fastparquet-2024.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:74a0b3c40ab373442c0fda96b75a36e88745d8b138fcc3a6143e04682cbbb8ca", size = 673269, upload-time = "2024-12-11T21:22:48.073Z" }, + { url = "https://files.pythonhosted.org/packages/47/e3/e7db38704be5db787270d43dde895eaa1a825ab25dc245e71df70860ec12/fastparquet-2024.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:59e5c5b51083d5b82572cdb7aed0346e3181e3ac9d2e45759da2e804bdafa7ee", size = 912523, upload-time = "2024-11-12T20:38:06.003Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/e3387c99293dae441634e7724acaa425b27de19a00ee3d546775dace54a9/fastparquet-2024.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdadf7b6bad789125b823bfc5b0a719ba5c4a2ef965f973702d3ea89cff057f6", size = 683779, upload-time = "2024-11-12T20:38:07.442Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/d112d0573d086b578bf04302a502e9a7605ea8f1244a7b8577cd945eec78/fastparquet-2024.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46b2db02fc2a1507939d35441c8ab211d53afd75d82eec9767d1c3656402859b", size = 1751113, upload-time = "2024-11-12T20:38:09.36Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a7/040507cee3a7798954e8fdbca21d2dbc532774b02b882d902b8a4a6849ef/fastparquet-2024.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3afdef2895c9f459135a00a7ed3ceafebfbce918a9e7b5d550e4fae39c1b64d", size = 1780496, upload-time = "2024-11-12T20:38:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/bc/75/d0d9f7533d780ec167eede16ad88073ee71696150511126c31940e7f73aa/fastparquet-2024.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36b5c9bd2ffaaa26ff45d59a6cefe58503dd748e0c7fad80dd905749da0f2b9e", size = 1713608, upload-time = "2024-11-12T20:38:12.848Z" }, + { url = "https://files.pythonhosted.org/packages/30/fa/1d95bc86e45e80669c4f374b2ca26a9e5895a1011bb05d6341b4a7414693/fastparquet-2024.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b7df5d3b61a19d76e209fe8d3133759af1c139e04ebc6d43f3cc2d8045ef338", size = 1792779, upload-time = "2024-11-12T20:38:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/13/3d/c076beeb926c79593374c04662a9422a76650eef17cd1c8e10951340764a/fastparquet-2024.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b35823ac7a194134e5f82fa4a9659e42e8f9ad1f2d22a55fbb7b9e4053aabbb", size = 1851322, upload-time = "2024-11-12T20:38:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/1d0d47e64816002824d4a876644e8c65540fa23f91b701f0daa726931545/fastparquet-2024.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d20632964e65530374ff7cddd42cc06aa0a1388934903693d6d22592a5ba827b", size = 673266, upload-time = "2024-11-12T20:38:17.661Z" }, +] + +[[package]] +name = "feature-engineering" +version = "0.1.0" +source = { editable = "feature_engineering" } +dependencies = [ + { name = "geopandas" }, + { name = "joblib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyproj" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "shapely" }, + { name = "tqdm" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=24.3,<25" }, + { name = "geopandas", specifier = ">=0.14,<1" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13,<6" }, + { name = "joblib", specifier = ">=1.3,<2" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11,<2" }, + { name = "numpy", specifier = ">=1.26,<2" }, + { name = "pandas", specifier = ">=2.2,<3" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.4,<4" }, + { name = "pyproj", specifier = ">=3.6,<4" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4,<8" }, + { name = "scikit-learn", specifier = ">=1.3,<2" }, + { name = "scipy", specifier = ">=1.11,<2" }, + { name = "shapely", specifier = ">=2.1,<3" }, + { name = "tqdm", specifier = ">=4.66,<5" }, +] +provides-extras = ["dev"] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "fiona" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "certifi" }, + { name = "click" }, + { name = "click-plugins" }, + { name = "cligj" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/e0/71b63839cc609e1d62cea2fc9774aa605ece7ea78af823ff7a8f1c560e72/fiona-1.10.1.tar.gz", hash = "sha256:b00ae357669460c6491caba29c2022ff0acfcbde86a95361ea8ff5cd14a86b68", size = 444606, upload-time = "2024-09-16T20:15:47.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b9/7a8356cfaff8ef162bad44283554d3171e13032635b4f8e10e694a9596ee/fiona-1.10.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:98fe556058b370da07a84f6537c286f87eb4af2343d155fbd3fba5d38ac17ed7", size = 16144293, upload-time = "2024-09-16T20:14:34.519Z" }, + { url = "https://files.pythonhosted.org/packages/65/0c/e8070b15c8303f60bd4444a120842597ccd6ed550548948e2e36cffbaa93/fiona-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:be29044d4aeebae92944b738160dc5f9afc4cdf04f551d59e803c5b910e17520", size = 14752213, upload-time = "2024-09-16T20:14:37.763Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/3f80ba2fda9b8686681f0a1b18c8e95ad152ada1d6fb1d3f25281d9229fd/fiona-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94bd3d448f09f85439e4b77c38b9de1aebe3eef24acc72bd631f75171cdfde51", size = 17272183, upload-time = "2024-09-16T20:14:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/c1d53b4d77926414ffdf5bd38344e900e378ae9ccb2a65754cdb6d5344c2/fiona-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:30594c0cd8682c43fd01e7cdbe000f94540f8fa3b7cb5901e805c88c4ff2058b", size = 24489398, upload-time = "2024-09-16T20:14:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/73/ab/036c418d531afb74abe4ca9a8be487b863901fe7b42ddba1ba2fb0681d77/fiona-1.10.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7338b8c68beb7934bde4ec9f49eb5044e5e484b92d940bc3ec27defdb2b06c67", size = 16114589, upload-time = "2024-09-16T20:14:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/693c1cca53023aaf6e3adc11422080f5fa427484e7b85e48f19c40d6357f/fiona-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c77fcfd3cdb0d3c97237965f8c60d1696a64923deeeb2d0b9810286cbe25911", size = 14754603, upload-time = "2024-09-16T20:14:53.829Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/be204fb409b59876ef4658710a022794f16f779a3e9e7df654acc38b2104/fiona-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537872cbc9bda7fcdf73851c91bc5338fca2b502c4c17049ccecaa13cde1f18f", size = 17223639, upload-time = "2024-09-16T20:14:57.146Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/914fd3c4c32043c2c512fa5021e83b2348e1b7a79365d75a0a37cb545362/fiona-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:41cde2c52c614457e9094ea44b0d30483540789e62fe0fa758c2a2963e980817", size = 24464921, upload-time = "2024-09-16T20:15:01.121Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/665ce969cab6339c19527318534236e5e4184ee03b38cd474497ebd22f4d/fiona-1.10.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:a00b05935c9900678b2ca660026b39efc4e4b916983915d595964eb381763ae7", size = 16106571, upload-time = "2024-09-16T20:15:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/23/c8/150094fbc4220d22217f480cc67b6ee4c2f4324b4b58cd25527cd5905937/fiona-1.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f78b781d5bcbbeeddf1d52712f33458775dbb9fd1b2a39882c83618348dd730f", size = 14738178, upload-time = "2024-09-16T20:15:06.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/83/63da54032c0c03d4921b854111e33d3a1dadec5d2b7e741fba6c8c6486a6/fiona-1.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ceeb38e3cd30d91d68858d0817a1bb0c4f96340d334db4b16a99edb0902d35", size = 17221414, upload-time = "2024-09-16T20:15:09.606Z" }, + { url = "https://files.pythonhosted.org/packages/60/14/5ef47002ef19bd5cfbc7a74b21c30ef83f22beb80609314ce0328989ceda/fiona-1.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:15751c90e29cee1e01fcfedf42ab85987e32f0b593cf98d88ed52199ef5ca623", size = 24461486, upload-time = "2024-09-16T20:15:13.399Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, + { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" }, + { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" }, + { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" }, + { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" }, + { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e0/bab50af11c2d75c9c4a2a26a5254573c0bd97cea152254401510950486fa/fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19", size = 304847, upload-time = "2025-09-02T19:10:49.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, +] + +[[package]] +name = "geopandas" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fiona" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyproj" }, + { name = "shapely" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/79/79af2645d40d590a466f8329ab04c2d4fffc811e6713d1c1580dcfdf285c/geopandas-0.14.4.tar.gz", hash = "sha256:56765be9d58e2c743078085db3bd07dc6be7719f0dbe1dfdc1d705cb80be7c25", size = 1106304, upload-time = "2024-04-28T13:49:27.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/b0/69fa7a0f55122847506a42fea6988d03b34136938082f142151bc9d9f7e7/geopandas-0.14.4-py3-none-any.whl", hash = "sha256:3bb6473cb59d51e1a7fe2dbc24a1a063fb0ebdeddf3ce08ddbf8c7ddc99689aa", size = 1109913, upload-time = "2024-04-28T13:49:24.25Z" }, +] + +[[package]] +name = "gtfs-realtime-bindings" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/55/ed46db9267d2615851bfba3c22b6c0e7f88751efb5d5d1468291935c7f65/gtfs-realtime-bindings-1.0.0.tar.gz", hash = "sha256:2e8ced8904400cc93ab7e8520adb6934cfa601edacc6f593fc2cb4448662bb47", size = 6197, upload-time = "2023-02-23T17:53:20.8Z" } + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "5.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "kombu" +version = "5.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, + { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, + { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" }, + { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, + { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/c4/120d2dfd92dff2c776d68f361ff8705fdea2ca64e20b612fab0fd3f581ac/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:50a36e01c4a090b9f9c47d92cec54964de6b9fcb3362d0e19b8ffc6323c21b60", size = 296766525, upload-time = "2025-11-18T05:49:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/44dbb46b3d1b0ec61afda8e84837870f2f9ace33c564317d59b70bc19d3e/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:485776daa8447da5da39681af455aa3b2c2586ddcf4af8772495e7c532c7e5ab", size = 296782137, upload-time = "2025-11-18T05:49:34.248Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815, upload-time = "2024-07-28T19:59:01.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643, upload-time = "2024-07-28T19:58:59.335Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, +] + +[[package]] +name = "psycopg" +version = "3.2.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/77/c72d10262b872617e509a0c60445afcc4ce2cd5cd6bc1c97700246d69c85/psycopg-3.2.12.tar.gz", hash = "sha256:85c08d6f6e2a897b16280e0ff6406bef29b1327c045db06d21f364d7cd5da90b", size = 160642, upload-time = "2025-10-26T00:46:03.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/28/8c4f90e415411dc9c78d6ba10b549baa324659907c13f64bfe3779d4066c/psycopg-3.2.12-py3-none-any.whl", hash = "sha256:8a1611a2d4c16ae37eada46438be9029a35bb959bb50b3d0e1e93c0f3d54c9ee", size = 206765, upload-time = "2025-10-26T00:10:42.173Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] +pool = [ + { name = "psycopg-pool" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.12" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/4d/980fdd0f75914c8b1f229a6e5a9c422b53e809166b96a7d0e1287b369796/psycopg_binary-3.2.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16db2549a31ccd4887bef05570d95036813ce25fd9810b523ba1c16b0f6cfd90", size = 4037686, upload-time = "2025-10-26T00:16:22.041Z" }, + { url = "https://files.pythonhosted.org/packages/51/76/6b6ccd3fd31c67bec8608225407322f26a2a633c05b35c56b7c0638dcc67/psycopg_binary-3.2.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b9a99ded7d19b24d3b6fa632b58e52bbdecde7e1f866c3b23d0c27b092af4e3", size = 4098526, upload-time = "2025-10-26T00:16:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/91/d8/be5242efed4f57f74a27eb47cb3a01bebb04e43ca57e903fcbda23361e72/psycopg_binary-3.2.12-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:385c7b5cfffac115f413b8e32c941c85ea0960e0b94a6ef43bb260f774c54893", size = 4646680, upload-time = "2025-10-26T00:17:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/20/c1/96e42d39c0e75c4059f80e8fc9b286e2b6d9652f30b42698101d4be201cf/psycopg_binary-3.2.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9c674887d1e0d4384c06c822bc7fcfede4952742e232ec1e76b5a6ae39a3ddd4", size = 4749345, upload-time = "2025-10-26T00:18:16.61Z" }, + { url = "https://files.pythonhosted.org/packages/78/00/0ee41e18bdb05b43a27ebf8a952343319554cd9bde7931f633343b5abbad/psycopg_binary-3.2.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72fd979e410ba7805462817ef8ed6f37dd75f9f4ae109bdb8503e013ccecb80b", size = 4432535, upload-time = "2025-10-26T00:18:53.823Z" }, + { url = "https://files.pythonhosted.org/packages/c5/77/580cc455ba909d9e3082b80bb1952f67c5b9692a56ecaf71816ce0e9aa69/psycopg_binary-3.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82fa5134517af44e28a30c38f34384773a0422ffd545fd298433ea9f2cc5a9", size = 3888888, upload-time = "2025-10-26T00:19:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/cb/29/0d0d2aa4238fd57ddbd2f517c58cefb26d408d3e989cbca9ad43f4c48433/psycopg_binary-3.2.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:100fdfee763d701f6da694bde711e264aca4c2bc84fb81e1669fb491ce11d219", size = 3571385, upload-time = "2025-10-26T00:19:56.844Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7d/eb11cd86339122c19c1039cb5ee5f87f88d6015dff564b1ed23d0c4a90e7/psycopg_binary-3.2.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:802bd01fb18a0acb0dea491f69a9a2da6034f33329a62876ab5b558a1fb66b45", size = 3614219, upload-time = "2025-10-26T00:20:27.135Z" }, + { url = "https://files.pythonhosted.org/packages/65/02/dff51dc1f88d9854405013e2cabbf7060c2b3993cb82d6e8ad21396081af/psycopg_binary-3.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:f33c9e12ed05e579b7fb3c8fdb10a165f41459394b8eb113e7c377b2bd027f61", size = 2919778, upload-time = "2025-10-26T00:20:51.974Z" }, + { url = "https://files.pythonhosted.org/packages/db/4a/b2779f74fdb0d661febe802fb3b770546a99f0a513ef108e8f9ed36b87cb/psycopg_binary-3.2.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ea9751310b840186379c949ede5a5129b31439acdb929f3003a8685372117ed8", size = 4019926, upload-time = "2025-10-26T00:21:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/d5/af/df6c2beb44de456c4f025a61dfe611cf5b3eb3d3fa671144ce19ac7f1139/psycopg_binary-3.2.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9fdf3a0c24822401c60c93640da69b3dfd4d9f29c3a8d797244fe22bfe592823", size = 4092107, upload-time = "2025-10-26T00:22:00.043Z" }, + { url = "https://files.pythonhosted.org/packages/f6/3b/b16260c93a0a435000fd175f1abb8d12af5542bd9d35d17dd2b7f347dbd5/psycopg_binary-3.2.12-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49582c3b6d578bdaab2932b59f70b1bd93351ed4d594b2c97cea1611633c9de1", size = 4626849, upload-time = "2025-10-26T00:22:38.606Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/2c8d1c534777176e3e4832105f0b2f70c0ff3d63def0f1fda46833cc2dc1/psycopg_binary-3.2.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5b6e505618cb376a7a7d6af86833a8f289833fe4cc97541d7100745081dc31bd", size = 4719811, upload-time = "2025-10-26T00:23:18.23Z" }, + { url = "https://files.pythonhosted.org/packages/34/44/005ab6a42698489310f52f287b78c26560aeedb091ba12f034acdff4549b/psycopg_binary-3.2.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6a898717ab560db393355c6ecf39b8c534f252afc3131480db1251e061090d3a", size = 4410950, upload-time = "2025-10-26T00:23:55.532Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ba/c59303ed65659cd62da2b3f4ad2b8635ae10eb85e7645d063025277c953d/psycopg_binary-3.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd632f7038c76b0921f6d5621f5ba9ecabfad3042fa40e5875db11771d2a5de", size = 3861578, upload-time = "2025-10-26T00:24:28.482Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/d36f03b11959978b2c2522c87369fa8d75c1fa9b311805b39ce7678455ae/psycopg_binary-3.2.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3e9c9e64fb7cda688e9488402611c0be2c81083664117edcc709d15f37faa30f", size = 3534948, upload-time = "2025-10-26T00:24:58.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/e0e5fc0d5f2d2650f85540cebd0d047e14b0933b99f713749b2ebc031047/psycopg_binary-3.2.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c1e38b1eda54910628f68448598139a9818973755abf77950057372c1fe89a6", size = 3583525, upload-time = "2025-10-26T00:25:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/13/27/e2b1afb9819835f85f1575f07fdfc871dd8b4ea7ed8244bfe86a2f6d6566/psycopg_binary-3.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:77690f0bf08356ca00fc357f50a5980c7a25f076c2c1f37d9d775a278234fefd", size = 2910254, upload-time = "2025-10-26T00:25:53.335Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0b/9d480aba4a4864832c29e6fc94ddd34d9927c276448eb3b56ffe24ed064c/psycopg_binary-3.2.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:442f20153415f374ae5753ca618637611a41a3c58c56d16ce55f845d76a3cf7b", size = 4017829, upload-time = "2025-10-26T00:26:27.031Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/0d294b30349bde24a46741a1f27a10e8ab81e9f4118d27c2fe592acfb42a/psycopg_binary-3.2.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79de3cc5adbf51677009a8fda35ac9e9e3686d5595ab4b0c43ec7099ece6aeb5", size = 4089835, upload-time = "2025-10-26T00:27:01.392Z" }, + { url = "https://files.pythonhosted.org/packages/82/d4/ff82e318e5a55d6951b278d3af7b4c7c1b19344e3a3722b6613f156a38ea/psycopg_binary-3.2.12-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:095ccda59042a1239ac2fefe693a336cb5cecf8944a8d9e98b07f07e94e2b78d", size = 4625474, upload-time = "2025-10-26T00:27:40.34Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/2c9df6475a5ab6d614d516f4497c568d84f7d6c21d0e11444468c9786c9f/psycopg_binary-3.2.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:efab679a2c7d1bf7d0ec0e1ecb47fe764945eff75bb4321f2e699b30a12db9b3", size = 4720350, upload-time = "2025-10-26T00:28:20.104Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/7aec81b0c41985dc006e2d5822486ad4b7c2a1a97a5a05e37dc2adaf1512/psycopg_binary-3.2.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d369e79ad9647fc8217cbb51bbbf11f9a1ffca450be31d005340157ffe8e91b3", size = 4411621, upload-time = "2025-10-26T00:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/fc/15/d3cb41b8fa9d5f14320ab250545fbb66f9ddb481e448e618902672a806c0/psycopg_binary-3.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eedc410f82007038030650aa58f620f9fe0009b9d6b04c3dc71cbd3bae5b2675", size = 3863081, upload-time = "2025-10-26T00:29:31.235Z" }, + { url = "https://files.pythonhosted.org/packages/69/8a/72837664e63e3cd3aa145cedcf29e5c21257579739aba78ab7eb668f7d9c/psycopg_binary-3.2.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bae4be7f6781bf6c9576eedcd5e1bb74468126fa6de991e47cdb1a8ea3a42a", size = 3537428, upload-time = "2025-10-26T00:30:01.465Z" }, + { url = "https://files.pythonhosted.org/packages/cc/7e/1b78ae38e7d69e6d7fb1e2dcce101493f5fa429480bac3a68b876c9b1635/psycopg_binary-3.2.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8ffe75fe6be902dadd439adf4228c98138a992088e073ede6dd34e7235f4e03e", size = 3585981, upload-time = "2025-10-26T00:30:31.635Z" }, + { url = "https://files.pythonhosted.org/packages/a3/f8/245b4868b2dac46c3fb6383b425754ae55df1910c826d305ed414da03777/psycopg_binary-3.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:2598d0e4f2f258da13df0560187b3f1dfc9b8688c46b9d90176360ae5212c3fc", size = 2912929, upload-time = "2025-10-26T00:30:56.413Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5b/76fbb40b981b73b285a00dccafc38cf67b7a9b3f7d4f2025dda7b896e7ef/psycopg_binary-3.2.12-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dc68094e00a5a7e8c20de1d3a0d5e404a27f522e18f8eb62bbbc9f865c3c81ef", size = 4016868, upload-time = "2025-10-26T00:31:29.974Z" }, + { url = "https://files.pythonhosted.org/packages/0e/08/8841ae3e2d1a3228e79eaaf5b7f991d15f0a231bb5031a114305b19724b1/psycopg_binary-3.2.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2d55009eeddbef54c711093c986daaf361d2c4210aaa1ee905075a3b97a62441", size = 4090508, upload-time = "2025-10-26T00:32:04.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/de/a41f62230cf4095ae4547eceada218cf28c17e7f94376913c1c8dde9546f/psycopg_binary-3.2.12-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:66a031f22e4418016990446d3e38143826f03ad811b9f78f58e2afbc1d343f7a", size = 4629788, upload-time = "2025-10-26T00:32:43.28Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/529d92134eae44475f781a86d58cdf3edd0953e17c69762abf387a9f2636/psycopg_binary-3.2.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:58ed30d33c25d7dc8d2f06285e88493147c2a660cc94713e4b563a99efb80a1f", size = 4724124, upload-time = "2025-10-26T00:33:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f5/97344e87065f7c9713ce213a2cff7732936ec3af6622e4b2a88715a953f2/psycopg_binary-3.2.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e0b5ccd03ca4749b8f66f38608ccbcb415cbd130d02de5eda80d042b83bee90e", size = 4411340, upload-time = "2025-10-26T00:34:00.759Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c2/34bce068f6bfb4c2e7bb1187bb64a3f3be254702b158c4ad05eacc0055cf/psycopg_binary-3.2.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:909de94de7dd4d6086098a5755562207114c9638ec42c52d84c8a440c45fe084", size = 3867815, upload-time = "2025-10-26T00:34:33.181Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c647e01ab162e6bfa52380e23e486215e9d28ffd31e9cf3cb1e9ca59008b/psycopg_binary-3.2.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:7130effd0517881f3a852eff98729d51034128f0737f64f0d1c7ea8343d77bd7", size = 3541756, upload-time = "2025-10-26T00:35:08.622Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d0/795bdaa8c946a7b7126bf7ca8d4371eaaa613093e3ec341a0e50f52cbee2/psycopg_binary-3.2.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89b3c5201ca616d69ca0c3c0003ca18f7170a679c445c7e386ebfb4f29aa738e", size = 3587950, upload-time = "2025-10-26T00:35:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/10c3e95827a3ca8af332dfc471befec86e15a14dc83cee893c49a4910dad/psycopg_binary-3.2.12-cp314-cp314-win_amd64.whl", hash = "sha256:48a8e29f3e38fcf8d393b8fe460d83e39c107ad7e5e61cd3858a7569e0554a39", size = 3005787, upload-time = "2025-10-26T00:36:06.783Z" }, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/8f/3ec52b17087c2ed5fa32b64fd4814dde964c9aa4bd49d0d30fc24725ca6d/psycopg_pool-3.2.7.tar.gz", hash = "sha256:a77d531bfca238e49e5fb5832d65b98e69f2c62bfda3d2d4d833696bdc9ca54b", size = 29765, upload-time = "2025-10-26T00:46:10.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/59/74e752f605c6f0e351d4cf1c54fb9a1616dc800db4572b95bbfbb1a6225f/psycopg_pool-3.2.7-py3-none-any.whl", hash = "sha256:4b47bb59d887ef5da522eb63746b9f70e2faf967d34aac4f56ffc65e9606728f", size = 38232, upload-time = "2025-10-26T00:46:00.496Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pyproj" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bd/f205552cd1713b08f93b09e39a3ec99edef0b3ebbbca67b486fdf1abe2de/pyproj-3.7.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2514d61f24c4e0bb9913e2c51487ecdaeca5f8748d8313c933693416ca41d4d5", size = 6227022, upload-time = "2025-08-14T12:03:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/75/4c/9a937e659b8b418ab573c6d340d27e68716928953273e0837e7922fcac34/pyproj-3.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8693ca3892d82e70de077701ee76dd13d7bca4ae1c9d1e739d72004df015923a", size = 4625810, upload-time = "2025-08-14T12:03:53.808Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/a9f41e814dc4d1dc54e95b2ccaf0b3ebe3eb18b1740df05fe334724c3d89/pyproj-3.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5e26484d80fea56273ed1555abaea161e9661d81a6c07815d54b8e883d4ceb25", size = 9638694, upload-time = "2025-08-14T12:03:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ab/9bdb4a6216b712a1f9aab1c0fcbee5d3726f34a366f29c3e8c08a78d6b70/pyproj-3.7.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:281cb92847814e8018010c48b4069ff858a30236638631c1a91dd7bfa68f8a8a", size = 9493977, upload-time = "2025-08-14T12:03:57.937Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/2db75b1b6190f1137b1c4e8ef6a22e1c338e46320f6329bfac819143e063/pyproj-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c8577f0b7bb09118ec2e57e3babdc977127dd66326d6c5d755c76b063e6d9dc", size = 10841151, upload-time = "2025-08-14T12:04:00.271Z" }, + { url = "https://files.pythonhosted.org/packages/89/f7/989643394ba23a286e9b7b3f09981496172f9e0d4512457ffea7dc47ffc7/pyproj-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a23f59904fac3a5e7364b3aa44d288234af267ca041adb2c2b14a903cd5d3ac5", size = 10751585, upload-time = "2025-08-14T12:04:02.228Z" }, + { url = "https://files.pythonhosted.org/packages/53/6d/ad928fe975a6c14a093c92e6a319ca18f479f3336bb353a740bdba335681/pyproj-3.7.2-cp311-cp311-win32.whl", hash = "sha256:f2af4ed34b2cf3e031a2d85b067a3ecbd38df073c567e04b52fa7a0202afde8a", size = 5908533, upload-time = "2025-08-14T12:04:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/b95584605cec9ed50b7ebaf7975d1c4ddeec5a86b7a20554ed8b60042bd7/pyproj-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b7cb633565129677b2a183c4d807c727d1c736fcb0568a12299383056e67433", size = 6320742, upload-time = "2025-08-14T12:04:06.357Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/536e8f93bca808175c2d0a5ac9fdf69b960d8ab6b14f25030dccb07464d7/pyproj-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:38b08d85e3a38e455625b80e9eb9f78027c8e2649a21dec4df1f9c3525460c71", size = 6245772, upload-time = "2025-08-14T12:04:08.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832, upload-time = "2025-08-14T12:04:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650, upload-time = "2025-08-14T12:04:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087, upload-time = "2025-08-14T12:04:13.964Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797, upload-time = "2025-08-14T12:04:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036, upload-time = "2025-08-14T12:04:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952, upload-time = "2025-08-14T12:04:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872, upload-time = "2025-08-14T12:04:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176, upload-time = "2025-08-14T12:04:24.736Z" }, + { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452, upload-time = "2025-08-14T12:04:27.287Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/8f/f125feec0b958e8d22c8f0b492b30b1991d9499a4315dfde466cf4289edc/redis-7.0.1.tar.gz", hash = "sha256:c949df947dca995dc68fdf5a7863950bf6df24f8d6022394585acc98e81624f1", size = 4755322, upload-time = "2025-10-27T14:34:00.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/97/9f22a33c475cda519f20aba6babb340fb2f2254a02fb947816960d1e669a/redis-7.0.1-py3-none-any.whl", hash = "sha256:4977af3c7d67f8f0eb8b6fec0dafc9605db9343142f634041fb0235f67c0588a", size = 339938, upload-time = "2025-10-27T14:33:58.553Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/ef/37ed4b213d64b48422df92560af7300e10fe30b5d665dd79932baebee0c6/scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92", size = 36619956, upload-time = "2025-09-11T17:39:20.5Z" }, + { url = "https://files.pythonhosted.org/packages/85/ab/5c2eba89b9416961a982346a4d6a647d78c91ec96ab94ed522b3b6baf444/scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e", size = 28931117, upload-time = "2025-09-11T17:39:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/eed51ab64d227fe60229a2d57fb60ca5898cfa50ba27d4f573e9e5f0b430/scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173", size = 20921997, upload-time = "2025-09-11T17:39:34.892Z" }, + { url = "https://files.pythonhosted.org/packages/be/7c/33ea3e23bbadde96726edba6bf9111fb1969d14d9d477ffa202c67bec9da/scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d", size = 23523374, upload-time = "2025-09-11T17:39:40.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/7399dc96e1e3f9a05e258c98d716196a34f528eef2ec55aad651ed136d03/scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2", size = 33583702, upload-time = "2025-09-11T17:39:49.011Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/a5c75095089b96ea72c1bd37a4497c24b581ec73db4ef58ebee142ad2d14/scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9", size = 35883427, upload-time = "2025-09-11T17:39:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/ab/66/e25705ca3d2b87b97fe0a278a24b7f477b4023a926847935a1a71488a6a6/scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3", size = 36212940, upload-time = "2025-09-11T17:40:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fd/0bb911585e12f3abdd603d721d83fc1c7492835e1401a0e6d498d7822b4b/scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88", size = 38865092, upload-time = "2025-09-11T17:40:15.143Z" }, + { url = "https://files.pythonhosted.org/packages/d6/73/c449a7d56ba6e6f874183759f8483cde21f900a8be117d67ffbb670c2958/scipy-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa", size = 38687626, upload-time = "2025-09-11T17:40:24.041Z" }, + { url = "https://files.pythonhosted.org/packages/68/72/02f37316adf95307f5d9e579023c6899f89ff3a051fa079dbd6faafc48e5/scipy-1.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c", size = 25503506, upload-time = "2025-09-11T17:40:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d", size = 36646259, upload-time = "2025-09-11T17:40:39.329Z" }, + { url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371", size = 28888976, upload-time = "2025-09-11T17:40:46.82Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0", size = 20879905, upload-time = "2025-09-11T17:40:52.545Z" }, + { url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232", size = 23553066, upload-time = "2025-09-11T17:40:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1", size = 33336407, upload-time = "2025-09-11T17:41:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f", size = 35673281, upload-time = "2025-09-11T17:41:15.055Z" }, + { url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef", size = 36004222, upload-time = "2025-09-11T17:41:23.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1", size = 38664586, upload-time = "2025-09-11T17:41:31.021Z" }, + { url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e", size = 38550641, upload-time = "2025-09-11T17:41:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851", size = 25456070, upload-time = "2025-09-11T17:41:41.3Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" }, + { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" }, + { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9f/b62587029980378304ba5a8563d376c96f40b1e133daacee76efdcae32de/scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff", size = 39360061, upload-time = "2025-09-11T17:45:09.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/7a2f1609921352c7fbee0815811b5050582f67f19983096c4769867ca45f/scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d", size = 26126914, upload-time = "2025-09-11T17:45:14.73Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b4/5c18a766e8353015439f3780f5fc473f36f9762edc1a2e45da3ff5a31b21/scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9", size = 39457455, upload-time = "2025-09-11T17:44:58.899Z" }, + { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "xgboost" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine != 'aarch64' and sys_platform == 'linux'" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/64/42310363ecd814de5930981672d20da3d35271721ad2d2b4970b4092825b/xgboost-3.1.2.tar.gz", hash = "sha256:0f94496db277f5c227755e1f3ec775c59bafae38f58c94aa97c5198027c93df5", size = 1237438, upload-time = "2025-11-20T18:33:29.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/1e/efdd603db8cb37422b01d925f9cce1baaac46508661c73f6aafd5b9d7c51/xgboost-3.1.2-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:b44f6ee43a28b998e289ab05285bd65a65d7999c78cf60b215e523d23dc94c5d", size = 2377854, upload-time = "2025-11-20T18:06:21.217Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c6/ed928cb106f56ab73b3f4edb5287c1352251eb9225b5932d3dd5e2803f60/xgboost-3.1.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:09690f7430504fcd3b3e62bf826bb1282bb49873b68b07120d2696ab5638df41", size = 2211078, upload-time = "2025-11-20T18:06:47.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/2f/5418f4b1deaf0886caf81c5e056299228ac2fc09b965a2dfe5e4496331c8/xgboost-3.1.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f9b83f39340e5852bbf3e918318e7feb7a2a700ac7e8603f9bc3a06787f0d86b", size = 4953319, upload-time = "2025-11-20T18:28:29.851Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/c60fcc137fa685533bb31e721de3ecc88959d393830d59d0204c5cbd2c85/xgboost-3.1.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:24879ac75c0ee21acae0101f791bc43303f072a86d70fdfc89dae10a0008767f", size = 115885060, upload-time = "2025-11-20T18:32:00.773Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/41847e45ff075f3636c95d1000e0b75189aed4f1ae18c36812575bb42b4b/xgboost-3.1.2-py3-none-win_amd64.whl", hash = "sha256:e627c50003269b4562aa611ed348dff8cb770e11a9f784b3888a43139a0f5073", size = 71979118, upload-time = "2025-11-20T18:27:55.23Z" }, +]