diff --git a/.gitignore b/.gitignore index 505a3b1..1e73087 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ wheels/ # Virtual environments .venv + +.env diff --git a/Dockerfile-schedule b/Dockerfile-schedule new file mode 100644 index 0000000..9bf67f4 --- /dev/null +++ b/Dockerfile-schedule @@ -0,0 +1,41 @@ +# Use Python slim image as base +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Copy requirements.txt first to leverage Docker cache +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# System packages +RUN apt update && apt install -y tzdata + +# Timezone +ENV TZ=Europe/Rome +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Copy application files +COPY schedule.py . + +# Copy entrypoint script and make it executable +COPY entrypoint-schedule.sh . +RUN chmod +x entrypoint-schedule.sh + +# Set environment variables +ENV PYTHONUNBUFFERED=1 + +# Required environment variables for schedule.py +# These can be overridden at runtime using -e or --env-file +# ENDPOINT_URL: The API endpoint to fetch plexts from +ENV ENDPOINT_URL="" +# MONGO_URI: MongoDB connection string +ENV MONGO_URI="" +# DB_NAME: MongoDB database name +ENV COLLECTION_NAME="" + +# Run the schedule script using the entrypoint script +# Using JSON array form for proper signal handling +CMD ["./entrypoint-schedule.sh"] diff --git a/Dockerfile-web b/Dockerfile-web new file mode 100644 index 0000000..e16d63b --- /dev/null +++ b/Dockerfile-web @@ -0,0 +1,39 @@ +# Use Python slim image as base +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Copy requirements.txt first to leverage Docker cache +COPY requirements.txt . + +# System packages +RUN apt update && apt install -y wget tzdata + +# Timezone +ENV TZ=Europe/Rome +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application files +COPY app.py . +COPY ingress.py . +COPY models.py . + +# Copy entrypoint script and make it executable +COPY entrypoint-web.sh . +RUN chmod +x entrypoint-web.sh + +# Expose port (default 5000, can be overridden via PORT env var) +EXPOSE 5000 + +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV PORT=5000 + +# Run gunicorn with the Flask app using the entrypoint script +# The script handles PORT environment variable expansion +# Using JSON array form for proper signal handling +CMD ["./entrypoint-web.sh"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..1e4f27f --- /dev/null +++ b/app.py @@ -0,0 +1,223 @@ +import os +import logging +from datetime import datetime +from zoneinfo import ZoneInfo +from flask import Flask, request, jsonify +from ingress import IngressAPI +from models import EventType, Plext + +# Timezone configuration +TIMEZONE = ZoneInfo("Europe/Rome") + +# Configure logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +app = Flask(__name__) + + +def parse_timestamp(value: str) -> int: + """ + Parse timestamp from either milliseconds (int) or ISO 8601 string. + + Args: + value: Either integer milliseconds or ISO 8601 string + + Returns: + Timestamp in milliseconds since epoch + + Raises: + ValueError: If format is invalid + """ + # Try parsing as integer (milliseconds) + try: + return int(value) + except ValueError: + pass + + # Try parsing as ISO 8601 datetime + try: + # Handle 'Z' suffix (UTC) + iso_value = value.replace("Z", "+00:00") + + # Handle timezone offset without colon (e.g., +0100 -> +01:00) + # Match pattern like +0100 or -0100 at the end of the string + import re + + match = re.search(r"([+-])(\d{2})(\d{2})$", iso_value) + if match: + sign, hours, minutes = match.groups() + iso_value = re.sub( + r"([+-])(\d{2})(\d{2})$", f"{sign}{hours}:{minutes}", iso_value + ) + + dt = datetime.fromisoformat(iso_value) + return int(dt.timestamp() * 1000) + except ValueError: + raise ValueError( + f"Invalid timestamp format: {value}. " + "Use milliseconds (e.g., 1736659200000) or " + "ISO 8601 format (e.g., '2026-01-12T10:00:00Z' or '2026-01-12T10:00:00+01:00')" + ) + + +def plext_to_dict(plext: Plext) -> dict: + """ + Convert a Plext object to a dictionary for JSON serialization. + + Args: + plext: Plext object to convert + + Returns: + Dictionary representation of the plext + """ + coords = plext.get_event_coordinates() + # Convert timestamp from milliseconds to datetime in Europe/Rome timezone + dt = datetime.fromtimestamp(plext.timestamp / 1000.0, tz=TIMEZONE) + return { + "id": plext.id, + "timestamp": plext.timestamp, + "timestamp_formatted": dt.strftime("%Y-%m-%d %H:%M:%S"), + "text": plext.text, + "team": plext.team, + "plext_type": plext.plext_type, + "categories": plext.categories, + "event_type": plext.get_event_type().name, + "player_name": plext.get_player_name(), + "portal_name": plext.get_portal_name(), + "coordinates": {"lat": coords[0], "lng": coords[1]} if coords else None, + "markup": [ + { + "type": m.type, + "plain": m.plain, + "team": m.team, + "name": m.name, + "address": m.address, + "latE6": m.latE6, + "lngE6": m.lngE6, + } + for m in plext.markup + ], + } + + +@app.route("/plexts", methods=["GET"]) +def get_plexts(): + """ + Get plexts from the Ingress API. + + Query Parameters: + event_types: List of event types to filter by (comma-separated) + player_name: Player name to filter by + min_lat: Minimum latitude (default: 45470259) + min_lng: Minimum longitude (default: 12244155) + max_lat: Maximum latitude (default: 45480370) + max_lng: Maximum longitude (default: 12298207) + min_timestamp: Minimum timestamp (milliseconds or ISO 8601 format, default: -1) + max_timestamp: Maximum timestamp (milliseconds or ISO 8601 format, default: -1) + + Returns: + JSON response with list of plexts + """ + try: + # Parse query parameters + event_types_param = request.args.get("event_types") + event_types = None + if event_types_param: + event_types_list = [et.strip() for et in event_types_param.split(",")] + try: + event_types = [EventType[et] for et in event_types_list] + except KeyError as e: + return jsonify({"error": f"Invalid event type: {e}"}), 400 + + player_name = request.args.get("player_name") + + min_lat = int(request.args.get("min_lat", os.getenv("MIN_LAT"))) + min_lng = int(request.args.get("min_lng", os.getenv("MIN_LNG"))) + max_lat = int(request.args.get("max_lat", os.getenv("MAX_LAT"))) + max_lng = int(request.args.get("max_lng", os.getenv("MAX_LNG"))) + + min_timestamp_param = request.args.get("min_timestamp", "-1") + max_timestamp_param = request.args.get("max_timestamp", "-1") + + min_timestamp = parse_timestamp(min_timestamp_param) + max_timestamp = parse_timestamp(max_timestamp_param) + + # Initialize IngressAPI client + cookie = os.getenv("INGRESS_COOKIE") + + client = IngressAPI( + version=os.getenv("V"), + cookie=cookie, + ) + + # Fetch plexts + plexts = client.get_plexts( + min_lat_e6=min_lat, + min_lng_e6=min_lng, + max_lat_e6=max_lat, + max_lng_e6=max_lng, + min_timestamp_ms=min_timestamp, + max_timestamp_ms=max_timestamp, + event_types=event_types, + player_name=player_name, + ) + + # Sort plexts by timestamp (ascending - oldest first) + sorted_plexts = sorted(plexts, key=lambda p: p.timestamp) + + # Convert to JSON-serializable format + result = [plext_to_dict(p) for p in sorted_plexts] + + return jsonify( + { + "count": len(result), + "plexts": result, + } + ) + + except ValueError as e: + logger.warning(f"Validation error: {e}") + return jsonify({"error": str(e)}), 400 + except Exception as e: + logger.exception("Error processing request") + return jsonify({"error": f"An error occurred: {e}"}), 500 + + +@app.route("/", methods=["GET"]) +def index(): + """Root endpoint providing API information. + + Returns: + JSON response containing API name, version, and available endpoints + with their parameters and descriptions. + """ + return jsonify( + { + "name": "Ingress Intel API", + "version": "1.0.0", + "endpoints": { + "/plexts": { + "method": "GET", + "description": "Get plexts from the Ingress API", + "parameters": { + "event_types": "List of event types to filter by (comma-separated)", + "player_name": "Player name to filter by", + "min_lat": "Minimum latitude (default: 45470259)", + "min_lng": "Minimum longitude (default: 12244155)", + "max_lat": "Maximum latitude (default: 45480370)", + "max_lng": "Maximum longitude (default: 12298207)", + "min_timestamp": "Minimum timestamp (milliseconds or ISO 8601 format, default: -1)", + "max_timestamp": "Maximum timestamp (milliseconds or ISO 8601 format, default: -1)", + }, + "event_types": [e.name for e in EventType], + } + }, + } + ) + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=os.getenv("PORT", 5000)) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8d9bc4e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,83 @@ +services: + # MongoDB database service + mongodb: + image: mongo:7.0 + container_name: ingress-mongodb + restart: unless-stopped + environment: + MONGO_INITDB_ROOT_USERNAME: ingress_root + MONGO_INITDB_ROOT_PASSWORD: Qu3enai5 + MONGO_INITDB_DATABASE: ingress + ports: + - "27017" + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + networks: + - ingress-network + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet + interval: 10s + timeout: 5s + retries: 5 + + # Web application service + web: + image: mrosati84/ingress-web:1.0 + container_name: ingress-web + restart: unless-stopped + environment: + PYTHONUNBUFFERED: 1 + PORT: 7000 + MONGO_URI: mongodb://ingress_root:Qu3enai5@mongodb:27017/ + DB_NAME: ingress + COLLECTION_NAME: ingress + TZ: "Europe/Rome" + INGRESS_COOKIE: "csrftoken=6D4gJaFhxXDgYd3RcPNNHOs1VKMjuyIw34FYWmICB1cZyeK5tnvPwhP3gYkjbTa4; sessionid=.eJyrViotTi3yTFGyUkozNTQxT0tKNDE2SzY2SU1U0gHLueYmZuYApUstDI2MTSwtzY1NHNJBYnrJ-blANcWpxcWZ-XlhqUUgCqjQSKkWAGviGpE:1vfiwE:Yibl7yDDPVrD5jQo3OsfCEPGCH0; ingress.intelmap.zoom=10; ingress.intelmap.lat=45.953536660296486; ingress.intelmap.lng=12.827911376953125" + V: "412c0ac7e784d6df783fc507bca30e23b3c58c55" + ports: + - "7000" + depends_on: + mongodb: + condition: service_healthy + networks: + - ingress-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:7000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Schedule service + schedule: + image: mrosati84/ingress-schedule:1.0 + container_name: ingress-schedule + restart: unless-stopped + environment: + PYTHONUNBUFFERED: 1 + ENDPOINT_URL: http://web:7000/plexts + MONGO_URI: mongodb://root:root@mongodb:27017/ + DB_NAME: ingress + COLLECTION_NAME: ingress + TZ: "Europe/Rome" + + depends_on: + mongodb: + condition: service_healthy + web: + condition: service_healthy + networks: + - ingress-network + +# Named volumes for data persistence +volumes: + mongodb_data: + driver: local + mongodb_config: + driver: local + +# Custom network for all services +networks: + ingress-network: + driver: bridge diff --git a/entrypoint-schedule.sh b/entrypoint-schedule.sh new file mode 100644 index 0000000..3d0554f --- /dev/null +++ b/entrypoint-schedule.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -e + +# Validate required environment variables +if [ -z "$ENDPOINT_URL" ]; then + echo "Error: ENDPOINT_URL environment variable is required but not set" + exit 1 +fi + +if [ -z "$MONGO_URI" ]; then + echo "Error: MONGO_URI environment variable is required but not set" + exit 1 +fi + +if [ -z "$DB_NAME" ]; then + echo "Error: DB_NAME environment variable is required but not set" + exit 1 +fi + +if [ -z "$COLLECTION_NAME" ]; then + echo "Error: COLLECTION_NAME environment variable is required but not set" + exit 1 +fi + +# Display configuration (without sensitive data) +echo "Starting schedule script with configuration:" +echo " ENDPOINT_URL: ${ENDPOINT_URL}" +echo " MONGO_URI: [REDACTED]" +echo " DB_NAME: ${DB_NAME}" +echo " COLLECTION_NAME: ${COLLECTION_NAME}" +echo "" + +# Run the schedule script +# Using exec to replace the shell process with the Python process +# This ensures proper signal handling (SIGTERM, SIGINT) and exit code propagation +exec python schedule.py diff --git a/entrypoint-web.sh b/entrypoint-web.sh new file mode 100644 index 0000000..82e9628 --- /dev/null +++ b/entrypoint-web.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -e + +# Use PORT environment variable or default to 5000 +PORT=${PORT:-5000} + +# Run gunicorn with the specified port and comprehensive logging +# --access-logfile -: Send access logs to stdout +# --error-logfile -: Send error logs to stdout +# --log-level info: Set logging level to info +# --capture-output: Capture stdout/stderr from workers +# --timeout 120: Increase timeout for long-running requests +exec gunicorn -w 4 -b "0.0.0.0:${PORT}" \ + --access-logfile - \ + --error-logfile - \ + --log-level info \ + --capture-output \ + --timeout 120 \ + app:app diff --git a/ingress.py b/ingress.py index 9992819..db825aa 100644 --- a/ingress.py +++ b/ingress.py @@ -1,12 +1,42 @@ +import logging +import time from typing import List, Optional import requests from models import Plext, EventType +# Configure logging +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger = logging.getLogger(__name__) + class IngressAPI: + """Client for interacting with the Ingress Intel API. + + Attributes: + BASE_URL: The base URL for the Ingress API. + version: The API version string. + headers: HTTP headers for API requests. + """ + BASE_URL = "https://intel.ingress.com/r" def __init__(self, version: str, cookie: str): + """Initialize the IngressAPI client. + + Args: + version: The API version string to use for requests. + cookie: The authentication cookie string for the Ingress API. + Should include csrftoken and sessionid. + """ + logger.info("=" * 80) + logger.info("Initializing IngressAPI client") + logger.info(f"API Version: {version}") + logger.debug(f"Cookie length: {len(cookie)} characters") + self.version = version self.headers = { "accept": "application/json, text/javascript, */*; q=0.01", @@ -27,11 +57,22 @@ class IngressAPI: } # Extract CSRF token from cookie and add to headers + csrf_token = None for item in cookie.split(";"): if "csrftoken" in item: - self.headers["x-csrftoken"] = item.split("=")[1].strip() + csrf_token = item.split("=")[1].strip() + self.headers["x-csrftoken"] = csrf_token + logger.debug(f"CSRF token extracted: {csrf_token[:10]}... (truncated)") break + if not csrf_token: + logger.warning("No CSRF token found in cookie!") + + logger.info(f"Headers configured with {len(self.headers)} fields") + logger.debug(f"Header keys: {list(self.headers.keys())}") + logger.info("IngressAPI client initialization complete") + logger.info("=" * 80) + def get_plexts( self, min_lat_e6: int, @@ -44,9 +85,42 @@ class IngressAPI: event_types: Optional[List[EventType]] = None, player_name: Optional[str] = None, ) -> List[Plext]: + """Fetch plexts from the Ingress API. + + Args: + min_lat_e6: Minimum latitude in microdegrees (E6 format). + min_lng_e6: Minimum longitude in microdegrees (E6 format). + max_lat_e6: Maximum latitude in microdegrees (E6 format). + max_lng_e6: Maximum longitude in microdegrees (E6 format). + min_timestamp_ms: Minimum timestamp in milliseconds since epoch. + Use -1 for no minimum. + max_timestamp_ms: Maximum timestamp in milliseconds since epoch. + Use -1 for no maximum. + tab: The tab to fetch from (default: "all"). + event_types: Optional list of event types to filter by. + player_name: Optional player name to filter by. + + Returns: + A list of Plext objects matching the specified criteria. + + Raises: + requests.HTTPError: If the API request fails. + requests.exceptions.JSONDecodeError: If the response cannot be + decoded as JSON. """ - Fetches plexts from the Ingress API. - """ + logger.info("-" * 80) + logger.info("get_plexts method called") + logger.info(f"Parameters:") + logger.info(f" - min_lat_e6: {min_lat_e6}") + logger.info(f" - min_lng_e6: {min_lng_e6}") + logger.info(f" - max_lat_e6: {max_lat_e6}") + logger.info(f" - max_lng_e6: {max_lng_e6}") + logger.info(f" - min_timestamp_ms: {min_timestamp_ms}") + logger.info(f" - max_timestamp_ms: {max_timestamp_ms}") + logger.info(f" - tab: {tab}") + logger.info(f" - event_types: {event_types}") + logger.info(f" - player_name: {player_name}") + payload = { "minLatE6": min_lat_e6, "minLngE6": min_lng_e6, @@ -54,27 +128,147 @@ class IngressAPI: "maxLngE6": max_lng_e6, "minTimestampMs": min_timestamp_ms, "maxTimestampMs": max_timestamp_ms, + "ascendingTimestampOrder": True, "tab": tab, "v": self.version, } - response = requests.post( - f"{self.BASE_URL}/getPlexts", json=payload, headers=self.headers + url = f"{self.BASE_URL}/getPlexts" + logger.info(f"Preparing HTTP POST request to: {url}") + logger.debug(f"Request payload: {payload}") + logger.debug( + f"Request headers (excluding cookie): { {k: v for k, v in self.headers.items() if k != 'cookie'} }" ) - response.raise_for_status() + + start_time = time.time() try: - data = response.json() - except requests.exceptions.JSONDecodeError: - print(f"Error decoding JSON: {response.text}") + logger.info("Sending HTTP request...") + response = requests.post(url, json=payload, headers=self.headers) + elapsed_time = time.time() - start_time + + logger.info(f"HTTP Response received") + logger.info(f" - Status Code: {response.status_code}") + logger.info(f" - Status Text: {response.reason}") + logger.info(f" - Response Time: {elapsed_time:.3f} seconds") + logger.info(f" - Response Size: {len(response.content)} bytes") + logger.debug(f" - Response Headers: {dict(response.headers)}") + + # Log response content for debugging (truncated if too large) + response_text = response.text + if len(response_text) > 500: + logger.debug( + f" - Response Content (first 500 chars): {response_text[:500]}..." + ) + else: + logger.debug(f" - Response Content: {response_text}") + + # Check for non-200 status codes + if response.status_code != 200: + logger.error(f"Non-200 status code received: {response.status_code}") + logger.error(f"Response body: {response.text}") + + # Raise exception for HTTP errors + response.raise_for_status() + logger.info("HTTP request successful (status code 200)") + + except requests.exceptions.HTTPError as e: + logger.error(f"HTTP Error occurred!") + logger.error(f" - Exception Type: {type(e).__name__}") + logger.error(f" - Exception Message: {str(e)}") + logger.error(f" - Response Status Code: {response.status_code}") + logger.error(f" - Response Text: {response.text}") + logger.error(f" - Request URL: {url}") + logger.error(f" - Request Payload: {payload}") + logger.exception("Full exception traceback:") raise - plexts = [Plext.from_json(item) for item in data["result"]] + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection Error occurred!") + logger.error(f" - Exception Type: {type(e).__name__}") + logger.error(f" - Exception Message: {str(e)}") + logger.error(f" - Request URL: {url}") + logger.exception("Full exception traceback:") + raise + except requests.exceptions.Timeout as e: + logger.error(f"Timeout Error occurred!") + logger.error(f" - Exception Type: {type(e).__name__}") + logger.error(f" - Exception Message: {str(e)}") + logger.error(f" - Request URL: {url}") + logger.exception("Full exception traceback:") + raise + + except requests.exceptions.RequestException as e: + logger.error(f"Request Exception occurred!") + logger.error(f" - Exception Type: {type(e).__name__}") + logger.error(f" - Exception Message: {str(e)}") + logger.error(f" - Request URL: {url}") + logger.exception("Full exception traceback:") + raise + + try: + logger.info("Parsing JSON response...") + data = response.json() + logger.info("JSON parsing successful") + logger.debug(f"JSON keys: {list(data.keys())}") + + if "result" in data: + result_count = len(data["result"]) + logger.info(f"Found {result_count} plexts in response") + else: + logger.warning("No 'result' key found in JSON response") + logger.warning(f"Available keys: {list(data.keys())}") + + except requests.exceptions.JSONDecodeError as e: + logger.error(f"JSON Decode Error occurred!") + logger.error(f" - Exception Type: {type(e).__name__}") + logger.error(f" - Exception Message: {str(e)}") + logger.error(f" - Response Text: {response.text}") + logger.error(f" - Response Status Code: {response.status_code}") + logger.exception("Full exception traceback:") + raise + + except Exception as e: + logger.error(f"Unexpected error during JSON parsing!") + logger.error(f" - Exception Type: {type(e).__name__}") + logger.error(f" - Exception Message: {str(e)}") + logger.exception("Full exception traceback:") + raise + + try: + logger.info("Creating Plext objects from JSON data...") + plexts = [Plext.from_json(item) for item in data["result"]] + logger.info(f"Successfully created {len(plexts)} Plext objects") + except Exception as e: + logger.error(f"Error creating Plext objects!") + logger.error(f" - Exception Type: {type(e).__name__}") + logger.error(f" - Exception Message: {str(e)}") + logger.exception("Full exception traceback:") + raise + + # Apply event type filter if event_types: + logger.info(f"Filtering by event types: {event_types}") + initial_count = len(plexts) plexts = [p for p in plexts if p.get_event_type() in event_types] + filtered_count = len(plexts) + logger.info( + f"Event type filter: {initial_count} -> {filtered_count} plexts" + ) + logger.debug(f"Filtered out {initial_count - filtered_count} plexts") + # Apply player name filter if player_name: + logger.info(f"Filtering by player name: {player_name}") + initial_count = len(plexts) plexts = [p for p in plexts if p.get_player_name() == player_name] + filtered_count = len(plexts) + logger.info( + f"Player name filter: {initial_count} -> {filtered_count} plexts" + ) + logger.debug(f"Filtered out {initial_count - filtered_count} plexts") + logger.info(f"Returning {len(plexts)} plexts") + logger.info("-" * 80) return plexts diff --git a/main.py b/main.py index ab9ccf8..3471d18 100644 --- a/main.py +++ b/main.py @@ -2,10 +2,14 @@ import os import argparse from typing import List from datetime import datetime +from dotenv import load_dotenv from ingress import IngressAPI from models import EventType, Plext +load_dotenv() + + def parse_timestamp(value: str) -> int: """ Parse timestamp from either milliseconds (int) or ISO 8601 string. @@ -40,6 +44,14 @@ def parse_timestamp(value: str) -> int: def print_plexts(plexts: List[Plext]): + """Print plexts to stdout in a human-readable format. + + Sorts plexts by timestamp (ascending - oldest first) and prints each + plext with its timestamp, event type, text, and coordinates if available. + + Args: + plexts: List of Plext objects to print. + """ # Sort plexts by timestamp (ascending - oldest first) sorted_plexts = sorted(plexts, key=lambda p: p.timestamp) @@ -57,6 +69,11 @@ def print_plexts(plexts: List[Plext]): def main(): + """Main entry point for the Ingress Intel Report CLI. + + Parses command-line arguments, initializes the IngressAPI client, + fetches plexts based on the specified filters, and prints them. + """ parser = argparse.ArgumentParser(description="Ingress Intel Report") parser.add_argument( "--event-types", @@ -94,15 +111,12 @@ def main(): # It's recommended to set the INGRESS_COOKIE environment variable # instead of hardcoding the cookie in the script. - cookie = os.getenv( - "INGRESS_COOKIE", - "csrftoken=3yUkOxJ2lkc49AStOe8JDpmP1vWI5WTXpV5hlMKTRB4sisPGLdv1IXMBvKfYAZmQ; sessionid=.eJyrViotTi3yTFGyUrJINTA2SbG0MDdPSjJPMjVU0gHLueYmZuYApYvyixNLMvVyE0tKUvMd0kGiesn5uUBVxanFxZn5eWGpRSAKqNRIqRYA3zUdAQ:1veJTk:haQGju5WSpAUlMygA1AF26nvx1I; ingress.intelmap.shflt=viz; _ncc=1; ingress.intelmap.zoom=16; ingress.intelmap.lng=12.271176494006568; ingress.intelmap.lat=45.47531344367309", - ) + cookie = os.getenv("INGRESS_COOKIE") # This is the version from the curl command in json_doc.md # Replace with a valid one if it expires client = IngressAPI( - version="e6d07d367cf3d1d959dd9627c9ae3827352409d1", + version=os.getenv("V"), cookie=cookie, ) diff --git a/models.py b/models.py index a6566ca..949f7f5 100644 --- a/models.py +++ b/models.py @@ -5,6 +5,20 @@ import re class EventType(Enum): + """Enumeration of possible Ingress event types. + + Attributes: + RESONATOR_DEPLOYED: A resonator was deployed on a portal. + RESONATOR_DESTROYED: A resonator was destroyed on a portal. + PORTAL_CAPTURED: A portal was captured by a faction. + PORTAL_NEUTRALIZED: A portal was neutralized. + PORTAL_UNDER_ATTACK: A portal is under attack. + LINK_CREATED: A link was created between portals. + LINK_DESTROYED: A link was destroyed. + CONTROL_FIELD_CREATED: A control field was created. + UNKNOWN: Unknown event type. + """ + RESONATOR_DEPLOYED = "RESONATOR_DEPLOYED" RESONATOR_DESTROYED = "RESONATOR_DESTROYED" PORTAL_CAPTURED = "PORTAL_CAPTURED" @@ -30,6 +44,18 @@ EVENT_TYPE_KEYWORDS = { @dataclass class Markup: + """Represents markup data within a plext message. + + Attributes: + type: The type of markup (e.g., "PLAYER", "PORTAL"). + plain: Plain text representation of the markup. + team: Team affiliation (e.g., "RESISTANCE", "ENLIGHTENED"). + name: Name associated with the markup (e.g., player name, portal name). + address: Address of the location (for portals). + latE6: Latitude in microdegrees (E6 format). + lngE6: Longitude in microdegrees (E6 format). + """ + type: str plain: str team: str = "" @@ -41,6 +67,18 @@ class Markup: @dataclass class Plext: + """Represents a plext (message) from the Ingress Intel API. + + Attributes: + id: Unique identifier for the plext. + timestamp: Timestamp in milliseconds since epoch. + text: The text content of the plext. + team: Team affiliation (e.g., "RESISTANCE", "ENLIGHTENED"). + plext_type: Type of plext (e.g., "SYSTEM_BROADCAST", "PLAYER_GENERATED"). + categories: Category flags for the plext. + markup: List of Markup objects containing structured data. + """ + id: str timestamp: int text: str @@ -51,6 +89,15 @@ class Plext: @classmethod def from_json(cls, data: List[Any]) -> "Plext": + """Create a Plext instance from raw JSON data. + + Args: + data: Raw JSON data from the Ingress API. Expected format is a list + where index 2 contains a dict with a "plext" key. + + Returns: + A new Plext instance populated with data from the JSON. + """ plext_data = data[2]["plext"] markup_data = plext_data["markup"] @@ -81,6 +128,12 @@ class Plext: ) def get_event_type(self) -> EventType: + """Determine the event type based on the plext text content. + + Returns: + The EventType that matches the plext text, or EventType.UNKNOWN if + no match is found. + """ for event_type, keywords in EVENT_TYPE_KEYWORDS.items(): if all(keyword in self.text for keyword in keywords): # A special case for "captured", to avoid matching "destroyed" @@ -90,6 +143,14 @@ class Plext: return EventType.UNKNOWN def get_player_name(self) -> str: + """Extract the player name from the plext. + + First attempts to find the player name in the markup. If not found, + attempts to extract it from the text using regex. + + Returns: + The player name if found, otherwise an empty string. + """ for m in self.markup: if m.type == "PLAYER": return m.plain @@ -101,11 +162,23 @@ class Plext: return "" def get_portal_name(self) -> str: + """Extract the portal name from the plext. + + First attempts to find the portal name in the markup. If not found, + attempts to extract it from the text using regex patterns for various + event types. + + Returns: + The portal name if found, otherwise an empty string. + """ for m in self.markup: if m.type == "PORTAL": return m.name # If portal name is not in markup, try to extract from text - match = re.search(r"(?:deployed|destroyed|captured|linked from|created a Control Field @) (.+?) \(", self.text) + match = re.search( + r"(?:deployed|destroyed|captured|linked from|created a Control Field @) (.+?) \(", + self.text, + ) if not match: match = re.search(r"Your Portal (.+?) is under attack by", self.text) if not match: @@ -115,6 +188,14 @@ class Plext: return "" def get_event_coordinates(self) -> Optional[tuple[int, int]]: + """Extract the coordinates of the portal associated with this event. + + Searches the markup for a PORTAL type entry and returns its coordinates. + + Returns: + A tuple of (latitude, longitude) in microdegrees (E6 format) if found, + otherwise None. + """ for m in self.markup: if m.type == "PORTAL": return m.latE6, m.lngE6 diff --git a/pyproject.toml b/pyproject.toml index 1d29778..a63c11e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,12 @@ version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" -dependencies = ["requests>=2.31.0",] +dependencies = [ + "requests>=2.31.0", + "flask>=3.1.2", + "gunicorn>=23.0.0", + "python-dotenv>=1.2.1", + "pymongo>=4.10.0", + "apscheduler>=3.10.0", + "ipython>=9.9.0", +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..91edd98 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +apscheduler==3.11.2 +blinker==1.9.0 +certifi==2026.1.4 +charset-normalizer==3.4.4 +click==8.3.1 +dnspython==2.8.0 +flask==3.1.2 +gunicorn==23.0.0 +idna==3.11 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==3.0.3 +packaging==25.0 +pymongo==4.16.0 +python-dotenv==1.2.1 +requests==2.32.5 +tzlocal==5.3.1 +urllib3==2.6.3 +werkzeug==3.1.5 diff --git a/schedule.py b/schedule.py new file mode 100644 index 0000000..efda47c --- /dev/null +++ b/schedule.py @@ -0,0 +1,150 @@ +import os +import requests +from datetime import datetime, timedelta, UTC +from zoneinfo import ZoneInfo +from pymongo import MongoClient +from dotenv import load_dotenv +from pymongo.errors import PyMongoError +from apscheduler.schedulers.blocking import BlockingScheduler + +load_dotenv() + +# Configuration +ENDPOINT_URL = os.getenv("ENDPOINT_URL") +MONGO_URI = os.getenv("MONGO_URI") +DB_NAME = os.getenv("DB_NAME") +COLLECTION_NAME = os.getenv("COLLECTION_NAME") +TIMEZONE = ZoneInfo("Europe/Rome") + + +def get_time_range(): + """ + Calculate the time range for the current execution. + Returns a tuple of (min_timestamp, max_timestamp) in ISO 8601 format with Europe/Rome timezone. + """ + now = datetime.now(TIMEZONE) + one_minute_ago = now - timedelta(minutes=1) + + # Format with colon in timezone offset (e.g., +01:00 instead of +0100) + def format_with_colon(dt): + base = dt.strftime("%Y-%m-%dT%H:%M:%S") + # Get timezone offset and format with colon + offset = dt.strftime("%z") + if offset: + sign = offset[0] + hours = offset[1:3] + minutes = offset[3:5] + return f"{base}{sign}{hours}:{minutes}" + return base + + min_timestamp = format_with_colon(one_minute_ago) + max_timestamp = format_with_colon(now) + + return min_timestamp, max_timestamp + + +def fetch_plexts(min_timestamp, max_timestamp): + """ + Fetch plexts from the endpoint. + + Args: + min_timestamp: Start timestamp in ISO 8601 format + max_timestamp: End timestamp in ISO 8601 format + + Returns: + List of plexts or None if error occurs + """ + try: + params = {"min_timestamp": min_timestamp, "max_timestamp": max_timestamp} + response = requests.get(ENDPOINT_URL, params=params, timeout=30) + response.raise_for_status() + data = response.json() + return data.get("plexts", []) + except requests.RequestException as e: + print(f"Error fetching plexts: {e}") + return None + + +def save_to_mongodb(plexts): + """ + Save plexts to MongoDB. + + Args: + plexts: List of plext dictionaries to save + + Returns: + True if successful, False otherwise + """ + if not plexts: + print("No plexts to save") + return True + + try: + client = MongoClient(MONGO_URI) + db = client[DB_NAME] + collection = db[COLLECTION_NAME] + + # Insert all plexts + result = collection.insert_many(plexts) + print(f"Inserted {len(result.inserted_ids)} plexts to MongoDB") + return True + except PyMongoError as e: + print(f"Error saving to MongoDB: {e}") + return False + finally: + client.close() + + +def job(): + """ + Main job function that runs every minute. + """ + print( + f"\n[{datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S %Z')}] Starting job..." + ) + + # Get time range + min_timestamp, max_timestamp = get_time_range() + print(f"Time range: {min_timestamp} to {max_timestamp}") + + # Fetch plexts + plexts = fetch_plexts(min_timestamp, max_timestamp) + + if plexts is None: + print("Failed to fetch plexts") + return + + print(f"Fetched {len(plexts)} plexts") + + # Save to MongoDB + if save_to_mongodb(plexts): + print("Job completed successfully") + else: + print("Job completed with errors") + + +def main(): + """ + Main function to schedule and run the job. + """ + print("Starting Plex monitoring scheduler...") + print("Press Ctrl+C to stop") + + # Create scheduler + scheduler = BlockingScheduler() + + # Schedule job to run every minute + scheduler.add_job(job, "interval", minutes=1) + + # Run the job immediately on startup + job() + + # Start the scheduler + try: + scheduler.start() + except (KeyboardInterrupt, SystemExit): + print("\nScheduler stopped by user") + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock index b253ac0..9adb88a 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,36 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "apscheduler" +version = "3.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -52,6 +82,83 @@ wheels = [ { 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 = "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 = "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 = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[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 = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -66,11 +173,274 @@ name = "ingress" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "apscheduler" }, + { name = "flask" }, + { name = "gunicorn" }, + { name = "ipython" }, + { name = "pymongo" }, + { name = "python-dotenv" }, { name = "requests" }, ] [package.metadata] -requires-dist = [{ name = "requests", specifier = ">=2.31.0" }] +requires-dist = [ + { name = "apscheduler", specifier = ">=3.10.0" }, + { name = "flask", specifier = ">=3.1.2" }, + { name = "gunicorn", specifier = ">=23.0.0" }, + { name = "ipython", specifier = ">=9.9.0" }, + { name = "pymongo", specifier = ">=4.10.0" }, + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "requests", specifier = ">=2.31.0" }, +] + +[[package]] +name = "ipython" +version = "9.9.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" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl", hash = "sha256:b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b", size = 621431, upload-time = "2026-01-05T12:36:44.669Z" }, +] + +[[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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[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 = "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 = "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 = "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 = "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 = "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 = "pymongo" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/9c/a4895c4b785fc9865a84a56e14b5bd21ca75aadc3dab79c14187cdca189b/pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c", size = 2495323, upload-time = "2026-01-07T18:05:48.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/84/148d8b5da8260f4679d6665196ae04ab14ffdf06f5fe670b0ab11942951f/pymongo-4.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d15f060bc6d0964a8bb70aba8f0cb6d11ae99715438f640cff11bbcf172eb0e8", size = 972009, upload-time = "2026-01-07T18:04:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/9f3a8daf583d0adaaa033a3e3e58194d2282737dc164014ff33c7a081103/pymongo-4.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a19ea46a0fe71248965305a020bc076a163311aefbaa1d83e47d06fa30ac747", size = 971784, upload-time = "2026-01-07T18:04:39.669Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f2/b6c24361fcde24946198573c0176406bfd5f7b8538335f3d939487055322/pymongo-4.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:311d4549d6bf1f8c61d025965aebb5ba29d1481dc6471693ab91610aaffbc0eb", size = 1947174, upload-time = "2026-01-07T18:04:41.368Z" }, + { url = "https://files.pythonhosted.org/packages/47/1a/8634192f98cf740b3d174e1018dd0350018607d5bd8ac35a666dc49c732b/pymongo-4.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46ffb728d92dd5b09fc034ed91acf5595657c7ca17d4cf3751322cd554153c17", size = 1991727, upload-time = "2026-01-07T18:04:42.965Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2f/0c47ac84572b28e23028a23a3798a1f725e1c23b0cf1c1424678d16aff42/pymongo-4.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:acda193f440dd88c2023cb00aa8bd7b93a9df59978306d14d87a8b12fe426b05", size = 2082497, upload-time = "2026-01-07T18:04:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ba/57/9f46ef9c862b2f0cf5ce798f3541c201c574128d31ded407ba4b3918d7b6/pymongo-4.16.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d9fdb386cf958e6ef6ff537d6149be7edb76c3268cd6833e6c36aa447e4443f", size = 2064947, upload-time = "2026-01-07T18:04:46.228Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/5421c0998f38e32288100a07f6cb2f5f9f352522157c901910cb2927e211/pymongo-4.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91899dd7fb9a8c50f09c3c1cf0cb73bfbe2737f511f641f19b9650deb61c00ca", size = 1980478, upload-time = "2026-01-07T18:04:48.017Z" }, + { url = "https://files.pythonhosted.org/packages/92/93/bfc448d025e12313a937d6e1e0101b50cc9751636b4b170e600fe3203063/pymongo-4.16.0-cp313-cp313-win32.whl", hash = "sha256:2cd60cd1e05de7f01927f8e25ca26b3ea2c09de8723241e5d3bcfdc70eaff76b", size = 934672, upload-time = "2026-01-07T18:04:49.538Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/12710a5e01218d50c3dd165fd72c5ed2699285f77348a3b1a119a191d826/pymongo-4.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ead8a0050c53eaa55935895d6919d393d0328ec24b2b9115bdbe881aa222673", size = 959237, upload-time = "2026-01-07T18:04:51.382Z" }, + { url = "https://files.pythonhosted.org/packages/0c/56/d288bcd1d05bc17ec69df1d0b1d67bc710c7c5dbef86033a5a4d2e2b08e6/pymongo-4.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:dbbc5b254c36c37d10abb50e899bc3939bbb7ab1e7c659614409af99bd3e7675", size = 940909, upload-time = "2026-01-07T18:04:52.904Z" }, + { url = "https://files.pythonhosted.org/packages/30/9e/4d343f8d0512002fce17915a89477b9f916bda1205729e042d8f23acf194/pymongo-4.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a254d49a9ffe9d7f888e3c677eed3729b14ce85abb08cd74732cead6ccc3c66", size = 1026634, upload-time = "2026-01-07T18:04:54.359Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e3/341f88c5535df40c0450fda915f582757bb7d988cdfc92990a5e27c4c324/pymongo-4.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a1bf44e13cf2d44d2ea2e928a8140d5d667304abe1a61c4d55b4906f389fbe64", size = 1026252, upload-time = "2026-01-07T18:04:56.642Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/9471b22eb98f0a2ca0b8e09393de048502111b2b5b14ab1bd9e39708aab5/pymongo-4.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1c5f1f818b669875d191323a48912d3fcd2e4906410e8297bb09ac50c4d5ccc", size = 2207399, upload-time = "2026-01-07T18:04:58.255Z" }, + { url = "https://files.pythonhosted.org/packages/87/ac/47c4d50b25a02f21764f140295a2efaa583ee7f17992a5e5fa542b3a690f/pymongo-4.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77cfd37a43a53b02b7bd930457c7994c924ad8bbe8dff91817904bcbf291b371", size = 2260595, upload-time = "2026-01-07T18:04:59.788Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1b/0ce1ce9dd036417646b2fe6f63b58127acff3cf96eeb630c34ec9cd675ff/pymongo-4.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:36ef2fee50eee669587d742fb456e349634b4fcf8926208766078b089054b24b", size = 2366958, upload-time = "2026-01-07T18:05:01.942Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3c/a5a17c0d413aa9d6c17bc35c2b472e9e79cda8068ba8e93433b5f43028e9/pymongo-4.16.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55f8d5a6fe2fa0b823674db2293f92d74cd5f970bc0360f409a1fc21003862d3", size = 2346081, upload-time = "2026-01-07T18:05:03.576Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/f815533d1a88fb8a3b6c6e895bb085ffdae68ccb1e6ed7102202a307f8e2/pymongo-4.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9caacac0dd105e2555521002e2d17afc08665187017b466b5753e84c016628e6", size = 2246053, upload-time = "2026-01-07T18:05:05.459Z" }, + { url = "https://files.pythonhosted.org/packages/c6/88/4be3ec78828dc64b212c123114bd6ae8db5b7676085a7b43cc75d0131bd2/pymongo-4.16.0-cp314-cp314-win32.whl", hash = "sha256:c789236366525c3ee3cd6e4e450a9ff629a7d1f4d88b8e18a0aea0615fd7ecf8", size = 989461, upload-time = "2026-01-07T18:05:07.018Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/ab8d5af76421b34db483c9c8ebc3a2199fb80ae63dc7e18f4cf1df46306a/pymongo-4.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b0714d7764efb29bf9d3c51c964aed7c4c7237b341f9346f15ceaf8321fdb35", size = 1017803, upload-time = "2026-01-07T18:05:08.499Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/98d68020728ac6423cf02d17cfd8226bf6cce5690b163d30d3f705e8297e/pymongo-4.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:12762e7cc0f8374a8cae3b9f9ed8dabb5d438c7b33329232dd9b7de783454033", size = 997184, upload-time = "2026-01-07T18:05:09.944Z" }, + { url = "https://files.pythonhosted.org/packages/50/00/dc3a271daf06401825b9c1f4f76f018182c7738281ea54b9762aea0560c1/pymongo-4.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1c01e8a7cd0ea66baf64a118005535ab5bf9f9eb63a1b50ac3935dccf9a54abe", size = 1083303, upload-time = "2026-01-07T18:05:11.702Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/b5375ee21d12eababe46215011ebc63801c0d2c5ffdf203849d0d79f9852/pymongo-4.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4c4872299ebe315a79f7f922051061634a64fda95b6b17677ba57ef00b2ba2a4", size = 1083233, upload-time = "2026-01-07T18:05:13.182Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e3/52efa3ca900622c7dcb56c5e70f15c906816d98905c22d2ee1f84d9a7b60/pymongo-4.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78037d02389745e247fe5ab0bcad5d1ab30726eaac3ad79219c7d6bbb07eec53", size = 2527438, upload-time = "2026-01-07T18:05:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/cb/96/43b1be151c734e7766c725444bcbfa1de6b60cc66bfb406203746839dd25/pymongo-4.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c126fb72be2518395cc0465d4bae03125119136462e1945aea19840e45d89cfc", size = 2600399, upload-time = "2026-01-07T18:05:16.794Z" }, + { url = "https://files.pythonhosted.org/packages/e7/62/fa64a5045dfe3a1cd9217232c848256e7bc0136cffb7da4735c5e0d30e40/pymongo-4.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3867dc225d9423c245a51eaac2cfcd53dde8e0a8d8090bb6aed6e31bd6c2d4f", size = 2720960, upload-time = "2026-01-07T18:05:18.498Z" }, + { url = "https://files.pythonhosted.org/packages/54/7b/01577eb97e605502821273a5bc16ce0fb0be5c978fe03acdbff471471202/pymongo-4.16.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f25001a955073b80510c0c3db0e043dbbc36904fd69e511c74e3d8640b8a5111", size = 2699344, upload-time = "2026-01-07T18:05:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/55/68/6ef6372d516f703479c3b6cbbc45a5afd307173b1cbaccd724e23919bb1a/pymongo-4.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d9885aad05f82fd7ea0c9ca505d60939746b39263fa273d0125170da8f59098", size = 2577133, upload-time = "2026-01-07T18:05:22.052Z" }, + { url = "https://files.pythonhosted.org/packages/15/c7/b5337093bb01da852f945802328665f85f8109dbe91d81ea2afe5ff059b9/pymongo-4.16.0-cp314-cp314t-win32.whl", hash = "sha256:948152b30eddeae8355495f9943a3bf66b708295c0b9b6f467de1c620f215487", size = 1040560, upload-time = "2026-01-07T18:05:23.888Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/5b448cd1b103f3889d5713dda37304c81020ff88e38a826e8a75ddff4610/pymongo-4.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f6e42c1bc985d9beee884780ae6048790eb4cd565c46251932906bdb1630034a", size = 1075081, upload-time = "2026-01-07T18:05:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/ddc794cdc8500f6f28c119c624252fb6dfb19481c6d7ed150f13cf468a6d/pymongo-4.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6b2a20edb5452ac8daa395890eeb076c570790dfce6b7a44d788af74c2f8cf96", size = 1047725, upload-time = "2026-01-07T18:05:28.47Z" }, +] + +[[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 = "requests" @@ -87,6 +457,50 @@ 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 = "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 = "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 = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -95,3 +509,24 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6 wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] + +[[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 = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +]