diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b27f476 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Eloverblik API Configuration +ELOVERBLIK_API_TOKEN=your_refresh_token_here + +# Database Configuration +DB_HOST=localhost +DB_NAME=energy_consumption +DB_USER=energy_user +DB_PASSWORD=your_password_here +DB_PORT=5432 diff --git a/.gitea/workflows/daily-health-check.yml b/.gitea/workflows/daily-health-check.yml new file mode 100644 index 0000000..ff5ad5d --- /dev/null +++ b/.gitea/workflows/daily-health-check.yml @@ -0,0 +1,51 @@ +name: Daily Health Check + +on: + schedule: + # Run daily at 6:00 AM UTC + - cron: "0 6 * * *" + workflow_dispatch: # Allow manual triggering + +jobs: + health-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv sync + + - name: Run health check + env: + ELOVERBLIK_API_TOKEN: ${{ secrets.ELOVERBLIK_API_TOKEN }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_PORT: ${{ secrets.DB_PORT }} + run: | + uv run energy-ingester health-check --verbose + + - name: Report status + run: | + echo "Health check completed successfully!" + + - name: Notify on failure + if: failure() + run: | + echo "Health check failed! System may be down." + # Add notification logic here (email, Slack, etc.) + exit 1 diff --git a/.gitea/workflows/database-setup.yml b/.gitea/workflows/database-setup.yml new file mode 100644 index 0000000..4ee726c --- /dev/null +++ b/.gitea/workflows/database-setup.yml @@ -0,0 +1,177 @@ +name: Database Setup + +on: + workflow_dispatch: + inputs: + action: + description: "Database action to perform" + required: true + default: "setup" + type: choice + options: + - setup + - migrate + - reset + confirm: + description: 'Type "yes" to confirm destructive operations' + required: false + default: "no" + type: string + +jobs: + database-setup: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv sync + + - name: Validate inputs + run: | + if [[ "${{ github.event.inputs.action }}" == "reset" && "${{ github.event.inputs.confirm }}" != "yes" ]]; then + echo "❌ Reset operation requires confirmation. Please type 'yes' in the confirm field." + exit 1 + fi + + - name: Database Setup + if: github.event.inputs.action == 'setup' + env: + ELOVERBLIK_API_TOKEN: ${{ secrets.ELOVERBLIK_API_TOKEN }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_PORT: ${{ secrets.DB_PORT }} + run: | + echo "🏗️ Setting up database tables..." + uv run python -c " + from energy_consumption_ingester import DatabaseStorage + from dotenv import load_dotenv + import sys + + load_dotenv() + db = DatabaseStorage.from_env() + + if db.connect(): + print('✅ Database connection successful') + if db.create_tables(): + print('✅ Database tables created successfully') + else: + print('❌ Failed to create tables') + sys.exit(1) + else: + print('❌ Failed to connect to database') + sys.exit(1) + + db.close() + print('🎉 Database setup completed!') + " + + - name: Database Migration + if: github.event.inputs.action == 'migrate' + env: + ELOVERBLIK_API_TOKEN: ${{ secrets.ELOVERBLIK_API_TOKEN }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_PORT: ${{ secrets.DB_PORT }} + run: | + echo "🔄 Running database migrations..." + uv run python -c " + from energy_consumption_ingester import DatabaseStorage + from dotenv import load_dotenv + import sys + + load_dotenv() + db = DatabaseStorage.from_env() + + if db.connect(): + print('✅ Database connection successful') + if db.create_tables(): + print('✅ Database migrations completed') + else: + print('❌ Migration failed') + sys.exit(1) + else: + print('❌ Failed to connect to database') + sys.exit(1) + + db.close() + print('🎉 Migration completed!') + " + + - name: Database Reset (DESTRUCTIVE) + if: github.event.inputs.action == 'reset' && github.event.inputs.confirm == 'yes' + env: + ELOVERBLIK_API_TOKEN: ${{ secrets.ELOVERBLIK_API_TOKEN }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_PORT: ${{ secrets.DB_PORT }} + run: | + echo "🚨 RESETTING DATABASE - THIS WILL DELETE ALL DATA!" + uv run python -c " + from energy_consumption_ingester import DatabaseStorage + from dotenv import load_dotenv + import sys + + load_dotenv() + db = DatabaseStorage.from_env() + + if db.connect(): + print('✅ Database connection successful') + + # Drop tables + cursor = db.connection.cursor() + cursor.execute('DROP TABLE IF EXISTS consumption_readings CASCADE;') + cursor.execute('DROP TABLE IF EXISTS metering_points CASCADE;') + cursor.execute('DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;') + db.connection.commit() + print('🗑️ Existing tables dropped') + + # Recreate tables + if db.create_tables(): + print('✅ Database tables recreated') + else: + print('❌ Failed to recreate tables') + sys.exit(1) + else: + print('❌ Failed to connect to database') + sys.exit(1) + + db.close() + print('🎉 Database reset completed!') + " + + - name: Test Connection + env: + ELOVERBLIK_API_TOKEN: ${{ secrets.ELOVERBLIK_API_TOKEN }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_PORT: ${{ secrets.DB_PORT }} + run: | + echo "🔍 Testing system health..." + uv run energy-ingester health-check --verbose + + - name: Summary + run: | + echo "✅ Database operation '${{ github.event.inputs.action }}' completed successfully!" + echo "💡 You can now run the hourly sync or manual sync operations." diff --git a/.gitea/workflows/hourly-sync.yml b/.gitea/workflows/hourly-sync.yml new file mode 100644 index 0000000..0db732e --- /dev/null +++ b/.gitea/workflows/hourly-sync.yml @@ -0,0 +1,47 @@ +name: Hourly Energy Data Sync + +on: + schedule: + # Run every hour at minute 5 (to avoid peak times) + - cron: "5 * * * *" + workflow_dispatch: # Allow manual triggering + +jobs: + sync-energy-data: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv sync + + - name: Run incremental sync + env: + ELOVERBLIK_API_TOKEN: ${{ secrets.ELOVERBLIK_API_TOKEN }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_PORT: ${{ secrets.DB_PORT }} + run: | + uv run energy-ingester sync-incremental --verbose + + - name: Notify on failure + if: failure() + run: | + echo "Energy data sync failed! Check logs and database connectivity." + # Add notification logic here (email, Slack, etc.) + exit 1 diff --git a/.gitea/workflows/manual-full-sync.yml b/.gitea/workflows/manual-full-sync.yml new file mode 100644 index 0000000..3b3b1bb --- /dev/null +++ b/.gitea/workflows/manual-full-sync.yml @@ -0,0 +1,53 @@ +name: Manual Full Sync + +on: + workflow_dispatch: + inputs: + days: + description: "Number of days to sync" + required: true + default: "30" + type: string + +jobs: + full-sync: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + uv sync + + - name: Run full sync + env: + ELOVERBLIK_API_TOKEN: ${{ secrets.ELOVERBLIK_API_TOKEN }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + DB_PORT: ${{ secrets.DB_PORT }} + run: | + uv run energy-ingester sync --days ${{ github.event.inputs.days }} --verbose + + - name: Summary + run: | + echo "Full sync completed for ${{ github.event.inputs.days }} days!" + + - name: Notify on failure + if: failure() + run: | + echo "Full sync failed!" + exit 1 diff --git a/.gitea/workflows/python-code-quality.yml b/.gitea/workflows/python-code-quality.yml new file mode 100644 index 0000000..b75fee6 --- /dev/null +++ b/.gitea/workflows/python-code-quality.yml @@ -0,0 +1,13 @@ +name: Python Code Quality + +on: + push: + branches: + - main + pull_request: + +jobs: + python-code-quality: + uses: brian/CI-templates/.gitea/workflows/python/code-quality.yml@v1.0.0 + with: + python-version: ${{ vars.PYTHON_VERSION }} diff --git a/.gitea/workflows/setup-db.yml b/.gitea/workflows/setup-db.yml deleted file mode 100644 index e7df7fe..0000000 --- a/.gitea/workflows/setup-db.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Database Setup Automation - -on: - # Trigger the workflow manually from the Gitea UI - workflow_dispatch: - -jobs: - create_db_and_user: - runs-on: ubuntu-latest - - container: - image: node:20-alpine - - steps: - - name: Install PostgreSQL Client - run: | - # Install psql client on Alpine Linux - apk add --no-cache postgresql-client - - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Configure and Create PostgreSQL Resources - # Set environment variables from the Gitea Secrets - env: - POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} - POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }} - POSTGRES_ADMIN_USER: ${{ secrets.POSTGRES_ADMIN_USER }} - POSTGRES_ADMIN_PASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }} - NEW_DB_NAME: ${{ secrets.NEW_DB_NAME }} - NEW_DB_USER: ${{ secrets.NEW_DB_USER }} - NEW_DB_PASSWORD: ${{ secrets.NEW_DB_PASSWORD }} - - run: | - echo "Running database creation script..." - - # Add executable permission to the script - chmod +x ./scripts/create_db_user.sh - - # Export the admin password so psql can use it automatically - export PGPASSWORD=$POSTGRES_ADMIN_PASSWORD - - # Execute the script - ./scripts/create_db_user.sh - - # Unset the password variable for security - unset PGPASSWORD diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b7e0522 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,40 @@ +repos: + # General repository hygiene hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: debug-statements + - id: name-tests-test + - id: check-merge-conflict + + # Python linting and formatting with Ruff + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.8 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + # Static type checking with mypy + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + + # Python syntax modernization with pyupgrade + - repo: https://github.com/asottile/pyupgrade + rev: v3.20.0 + hooks: + - id: pyupgrade + args: ["--py312-plus"] + + # Formatting for Markdown, JSON, and YAML with Prettier + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + files: "\\.(md|json|yaml|yml)$" diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/POC.ipynb b/POC.ipynb new file mode 100644 index 0000000..e51c2a9 --- /dev/null +++ b/POC.ipynb @@ -0,0 +1,893 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a2da9908", + "metadata": {}, + "source": [ + "# Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e95933f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Token request status: 200\n", + "Token response: {\"result\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOiJDdXN0b21lckFQSV9EYXRhQWNjZXNzIiwidG9rZW5pZCI6ImE2NzY1ODRmLWY1MzktNDI0Yy04MmZhLWM5MjNhMjNhOTVlYSIsIndlYkFwcCI6IkN1c3RvbWVyQXBwIiwidmVyc2lvbiI6IjIiLCJpZGVudGl0eVRva2VuIjoiWUpoaXlqMXRjMUZiTnBtWmNaM0tjdGUrbzNzQ1FxR3B1dWtEcVh4QzVveEhKMisvcTAycTdpWGlZZURTRm9jUjVmYVBZMHVCeGpiWnYrOE52SHUycnpFU2pFRDNUajBQbmM5QzBZY1FXb0hzTDRvVHliOWRTSysrVFR5NTdDWTR4bit1dTdYN2lYdTNyV1pLUTUzTkh5T2NRSjB3Y3ZtWURHUjVDT3lITW14WDZhMjFEUHdBOWFsRk53RlFXbGkxT0NuTnp3QUdQaXJZWnZLcTYxNW52aFBjczZaN0FWUlh5MnRqMTFPbWpuR1NQVFJYU2pvdjdpRkhZcmhTdTRiTFpDQXhDd0FHWHl4a0tPWENRRDZFUVdTUU92d0ZpMHdnSUZwVHBIQjJKaHlEVFRvM2FYU1RaSnh2MEpFZktvalNLWXUxSE54enpWcTE3NmVWei9UVW5JZTdUeTc0LzZNekFqZmplcWpFUTk3aXcvbnRwTFNzdGkwakZqdjB6V1hKRXRKMVNQYURmbG5ZdG9DcEtnZmdCVDc4RTNKcXVIZSszQjJLanUwL0xlUU4rMXpHU2cxeEhuUFZOdlcxanRabjhWbjhIaVVmNUhUK0tPZ3ZDNitqaWJQRWRwNXJvaWtSNkFjRzZHcS8xOThkL2d6bEo4MzJMVWVnR1BiTlBHVVBwZ2VQTmFtUlRSWXpnSk9GdmwzV0NGb1NXMUZhYU5UalBJL1F4Wjh4L05VVzlJZXFXbG1rb1VrMVhIS3lWSzBjdW1KWXlaelJBNHJKdGJ0QXVaaGdMUCtpUmtSUzlFMkcwOWhDQnhzOHJKWjRycWVvZmVITC9iQkQwYXIzd25tREFpUks3L1ovNmkvbXFJMEZHa3NUcXdYZzdBTTY0SldGQkNUcm1ZMk9hSlIwRHpSZmd3ekN5Y0NzRHRRUW1RNTZwNWVPRzJyRnlUUHh5YjEwS3p4a3FTbTZVNDExcEhlVllFbnR4cVloYUg4and2RWJxVFJBNEZ5VUZtQVVxQmhIVDFWNWpsRkFZL0U3bjUwejAxSE5WL21KNzVBcGNqektsaUx4YVBuQXdYUG43NmVVbHc4NUt4anJueEh5d3V6a00wVFhDSVRIZ0NrWmdYbkdxVEYwZE1TL3VhRE9Tbjl5NTY5SE9vb1FxM3VNZ0prV2RFaUdSMkUvcVErbFVNS05kVlNEL0F6Y0tLZkdhdkZUTko2WVhZOVozKzZKN2lpMWFDbGxoWFAwQ1VRRzhaOVpaMEMrU3JlN3FNSzVvaEpwY2FDOUdoZ1hVNklqZ0xZd2JTM0RGOUN3NGt3cmxHRVJSNURVZVNXREg2aFp5SVdEWWc2d1k4bHMxNGVlVjQ4bW5aTVVCSVE4cDZvNzhGSzFLUWc2V3BQZ3NqMGVOcXdBVW1HemNQcUs5UHJsdDc5VmZvK2pQSDlkUW1IaUJIbjFDcjROWWJpUEI4bllNN09JdFJpb3lHVmpyTi9lVjR1SGloc1c1ZzUxMG9pMjVxNzU0Y2VPbW10ck0ra0VzRGtoVEd1UzNxZml5ZUhqRDV0dk1leXU3eGs0c1gyeDdhVkdDeGV0eGlDalNmWkZ0R2JiWmY3VWhKWTAzNzBZbDYvZFRNeDJ4VFNlUzhqOUgwZUZJT1RBL3N3ZVNXUjdDMlc2MitWOC9SNERZT3lOdjJzWmRib2RIN3hKdk0yUnNxcHFRMUtqS1VsclpEUmRTMFk4ZDZTOGJuTTBGUC9tRXlDZkF1MmthRnE5d0hQcGZUdzJ2QVFIeTdMNnR5ZjNoSUNtOHQ1eGh4NEJwV1ZUUkVtQUVXZk5KQ3c2QXZlUFExVHY5TjVmQ1N6NXo5RlU2VmhxcUlQL1hWM2l3dUFQc3dVc2hpaG90ZTBPR2YrZlpyMEZHY04vSTJXYzZneDdIT3RYVitUWEdieGdvY2lianFWRTFyQTRweTJsYnpSQlo3dXVRc29OcW1YQlVkckF6U0V3MkFEZGg3cE1KWGpBRXF0cHZKOXluWUlqWWRpZXdocllEazRRNCtEekl5V01NRk5YOGVyakFER21PYnJvazB6aUtGd1Fwb3hCL2I2SU9GYzVwWjFvUm0zY0dNUjU2M3BPdmw0aTNIalhMbmxvbWkxQ29Hb1JPL203MGp2RXRMdXBvOGRRSGpFZXlEUzBmeGl1b0hHRWV1Uk12SGpjcVUranBadjloUjEwMXh1QVVGRjNaUFJ6NnhQb2JsZVR0N0xnSjZWMm9KZmJ4QmpPWEdLTEthdGNiUEkzMS84MWtSWlIyN0w5eVZ4NkRLays1ZjV4Zk5JdUxWd0pzeDNBRVBob2o4SERPVFRKSERMUlZpa1hOdGE4OTNZOWVaZ2xpRzdyMXZCaXRTZ2dwQTRlbDhjMHlIdGJDaG9Vb3N6SmsyMHZJWGphem1zRG5kTU5DVXluRGlWbEN4VDBROWRPMlc4Rlp5NERxRmxNN1BQVVZWZzRhMkRIUzhianZQR3dWZ0R6WDkrVzA3VEMrVVlpZ3k2Sk93YnkwMThqd2loQjlLb3d6M0Vqc1djTjF1RGRsYXZPbHNWbHFBU05tWUF0MmpnbVF1YnQzTUlrdFhObGNOMWl0MnA5NFJ3UE9qNmhjYm1OM3ZRaXNvVExyd1llK1RlciIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWVpZGVudGlmaWVyIjoiTUlEOmUyZjE2NDE4LTlmZDItNGFkZi1hNDA2LWEwMDQ5ZTZjYzU4YSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2dpdmVubmFtZSI6IkJyaWFuIEJqYXJrZSBKZW5zZW4iLCJsb2dpblR5cGUiOiJLZXlDYXJkIiwiYjNmIjoiRjRSQ1VHSjdoTUUyMlpUVzRnTGZha2crL0VXL2Y4MmZVVTFoNVFjMk1DWT0iLCJwaWQiOiJQSUQ6OTIwOC0yMDAyLTItNjAwNTA4Mzg2NzY4IiwidXNlcklkIjoiMTA0MzQ1OSIsImV4cCI6MTc2MTY0NzgxNywiaXNzIjoiRW5lcmdpbmV0IiwianRpIjoiYTY3NjU4NGYtZjUzOS00MjRjLTgyZmEtYzkyM2EyM2E5NWVhIiwidG9rZW5OYW1lIjoiZ2l0ZWEtYWNjZXNzLXRva2VuIiwiYXVkIjoiRW5lcmdpbmV0In0.zmb-Oy1Jn6u16dAodmdd3OApFbUVcTjetM5Z8x_evxQ\"}\n", + "Successfully obtained access token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5c...\n" + ] + } + ], + "source": [ + "import os\n", + "from dotenv import load_dotenv\n", + "import http.client\n", + "import json\n", + "from pydantic import AnyUrl\n", + "\n", + "load_dotenv()\n", + "ELOVERBLIK_API_TOKEN = os.getenv(\"ELOVERBLIK_API_TOKEN\")\n", + "\n", + "\n", + "server_url = AnyUrl(\"https://api.eloverblik.dk\")\n", + "\n", + "# Prepare JWT authentication\n", + "\n", + "conn = http.client.HTTPSConnection(server_url.host)\n", + "\n", + "headers = {\n", + " \"Authorization\": f\"Bearer {ELOVERBLIK_API_TOKEN}\",\n", + " \"Content-Type\": \"application/json\",\n", + " \"api-version\": \"1.0\",\n", + "}\n", + "\n", + "conn.request(\"GET\", \"/customerapi/api/token\", headers=headers)\n", + "\n", + "res = conn.getresponse()\n", + "print(f\"Token request status: {res.status}\")\n", + "\n", + "if res.status == 200:\n", + " token_response = res.read().decode(\"utf-8\")\n", + " print(f\"Token response: {token_response[:50]}...\")\n", + "\n", + " # Parse the response to get the access token\n", + " try:\n", + " token_data = json.loads(token_response)\n", + " access_token = token_data[\"result\"]\n", + " print(f\"Successfully obtained access token: {access_token[:50]}...\")\n", + " except Exception as e:\n", + " print(f\"Error parsing token response: {e}\")\n", + " access_token = None\n", + "else:\n", + " error_data = res.read().decode(\"utf-8\")\n", + " print(f\"Error getting token: {error_data}\")\n", + " access_token = None" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "0a68b6bd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Testing access token with metering points API...\n", + "Metering points API status: 200\n", + "Response: {\"result\":[{\"streetCode\":\"2724\",\"streetName\":\"Vejl...\n", + "Metering points API status: 200\n", + "Response: {\"result\":[{\"streetCode\":\"2724\",\"streetName\":\"Vejl...\n" + ] + } + ], + "source": [ + "if not access_token:\n", + " print(\"No access token available to test with\")\n", + "else:\n", + " print(\"Testing access token with metering points API...\")\n", + "\n", + " # Create new connection for API calls\n", + " conn = http.client.HTTPSConnection(server_url.host)\n", + "\n", + " # Use the access token for API calls\n", + " api_headers = {\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " \"Content-Type\": \"application/json\",\n", + " \"api-version\": \"1.0\",\n", + " }\n", + "\n", + " # Get metering points\n", + " from urllib.parse import urlencode\n", + "\n", + " params = {\n", + " \"includeAll\": True,\n", + " }\n", + "\n", + " conn.request(\n", + " \"GET\",\n", + " \"/customerapi/api/meteringpoints/meteringpoints?\" + urlencode(params),\n", + " headers=api_headers,\n", + " )\n", + "\n", + " res = conn.getresponse()\n", + " print(f\"Metering points API status: {res.status}\")\n", + "\n", + " if res.status != 200:\n", + " error_data = res.read().decode(\"utf-8\")\n", + " print(f\"Error: {error_data}\")\n", + " else:\n", + " data = res.read().decode(\"utf-8\")\n", + " print(f\"Response: {data[:50] + '...' if len(data) > 50 else data}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "686fe407", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using metering point ID: 571313124600282119\n", + "Requesting consumption data from 2025-10-20 to 2025-10-27\n", + "Consumption data API status: 200\n", + "Success! Consumption data retrieved:\n", + "{\n", + " \"result\": [\n", + " {\n", + " \"MyEnergyData_MarketDocument\": {\n", + " \"mRID\": \"0HNGJHUMDF3GS:000001A6\",\n", + " \"createdDateTime\": \"2025-10-27T11:02:04Z\",\n", + " \"sender_MarketParticipant.name\": \"\",\n", + " \"sender_MarketParticipant.mRID\": {\n", + " \"codingScheme\": null,\n", + " \"name\": null\n", + " },\n", + " \"period.timeInterval\": {\n", + " \"start\": \"2025-10-19T22:00:00Z\",\n", + " \"end\": \"2025-10-26T23:00:00Z\"\n", + " },\n", + " \"TimeSeries\": [\n", + " {\n", + " \"mRID\": \"571313124600282119\",\n", + " \"businessType\": \"A04\",\n", + " \"curveType\": \"A01\",\n", + " \"measurement_Unit.name\": \"KWH\",\n", + " \"MarketEvaluationPoint\": {\n", + " \"mRID\": {\n", + " \"codingScheme\": \"A10\",\n", + " \"name\": \"571313124600282119\"\n", + " }\n", + " },\n", + " \"Period\": [\n", + " {\n", + " \"resolution\": \"PT1H\",\n", + " \"timeInterval\": {\n", + " \"start\": \"2025-10-19T22:00:00Z\",\n", + " \"end\": \"2025-10-2...\n", + "Consumption data API status: 200\n", + "Success! Consumption data retrieved:\n", + "{\n", + " \"result\": [\n", + " {\n", + " \"MyEnergyData_MarketDocument\": {\n", + " \"mRID\": \"0HNGJHUMDF3GS:000001A6\",\n", + " \"createdDateTime\": \"2025-10-27T11:02:04Z\",\n", + " \"sender_MarketParticipant.name\": \"\",\n", + " \"sender_MarketParticipant.mRID\": {\n", + " \"codingScheme\": null,\n", + " \"name\": null\n", + " },\n", + " \"period.timeInterval\": {\n", + " \"start\": \"2025-10-19T22:00:00Z\",\n", + " \"end\": \"2025-10-26T23:00:00Z\"\n", + " },\n", + " \"TimeSeries\": [\n", + " {\n", + " \"mRID\": \"571313124600282119\",\n", + " \"businessType\": \"A04\",\n", + " \"curveType\": \"A01\",\n", + " \"measurement_Unit.name\": \"KWH\",\n", + " \"MarketEvaluationPoint\": {\n", + " \"mRID\": {\n", + " \"codingScheme\": \"A10\",\n", + " \"name\": \"571313124600282119\"\n", + " }\n", + " },\n", + " \"Period\": [\n", + " {\n", + " \"resolution\": \"PT1H\",\n", + " \"timeInterval\": {\n", + " \"start\": \"2025-10-19T22:00:00Z\",\n", + " \"end\": \"2025-10-2...\n" + ] + } + ], + "source": [ + "# Get consumption data for a specific metering point ID\n", + "\n", + "if not access_token:\n", + " print(\"No access token available\")\n", + "else:\n", + " # First, let's get the metering points data properly and extract the metering point ID\n", + " conn = http.client.HTTPSConnection(server_url.host)\n", + "\n", + " api_headers = {\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " \"Content-Type\": \"application/json\",\n", + " \"api-version\": \"1.0\",\n", + " }\n", + "\n", + " # Get metering points\n", + " from urllib.parse import urlencode\n", + "\n", + " params = {\"includeAll\": True}\n", + "\n", + " conn.request(\n", + " \"GET\",\n", + " \"/customerapi/api/meteringpoints/meteringpoints?\" + urlencode(params),\n", + " headers=api_headers,\n", + " )\n", + " res = conn.getresponse()\n", + "\n", + " if res.status == 200:\n", + " metering_data = res.read().decode(\"utf-8\")\n", + " metering_points = json.loads(metering_data)\n", + "\n", + " if metering_points[\"result\"]:\n", + " # Get the first metering point ID\n", + " metering_point_id = metering_points[\"result\"][0][\"meteringPointId\"]\n", + " print(f\"Using metering point ID: {metering_point_id}\")\n", + "\n", + " # Now get consumption data for this metering point\n", + " # Let's get data from the last 7 days\n", + " from datetime import datetime, timedelta\n", + "\n", + " end_date = datetime.now()\n", + " start_date = end_date - timedelta(days=7)\n", + "\n", + " # Format dates as required by the API (YYYY-MM-DD)\n", + " date_from = start_date.strftime(\"%Y-%m-%d\")\n", + " date_to = end_date.strftime(\"%Y-%m-%d\")\n", + "\n", + " print(f\"Requesting consumption data from {date_from} to {date_to}\")\n", + "\n", + " # Prepare the request body for consumption data\n", + " consumption_request = {\n", + " \"meteringPoints\": {\"meteringPoint\": [metering_point_id]}\n", + " }\n", + "\n", + " # Create new connection for consumption data request\n", + " conn = http.client.HTTPSConnection(server_url.host)\n", + "\n", + " # Make the consumption data request\n", + " conn.request(\n", + " \"POST\",\n", + " f\"/customerapi/api/meterdata/gettimeseries/{date_from}/{date_to}/Hour\",\n", + " body=json.dumps(consumption_request),\n", + " headers=api_headers,\n", + " )\n", + "\n", + " res = conn.getresponse()\n", + " print(f\"Consumption data API status: {res.status}\")\n", + "\n", + " if res.status == 200:\n", + " consumption_data = res.read().decode(\"utf-8\")\n", + " consumption_json = json.loads(consumption_data)\n", + " print(\"Success! Consumption data retrieved:\")\n", + " print(\n", + " json.dumps(consumption_json, indent=2)[:1000] + \"...\"\n", + " if len(json.dumps(consumption_json, indent=2)) > 1000\n", + " else json.dumps(consumption_json, indent=2)\n", + " )\n", + " else:\n", + " error_data = res.read().decode(\"utf-8\")\n", + " print(f\"Error getting consumption data: {error_data}\")\n", + " else:\n", + " print(\"No metering points found\")\n", + " else:\n", + " error_data = res.read().decode(\"utf-8\")\n", + " print(f\"Error getting metering points: {error_data}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "97359b7b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metering Point ID: 571313124600282119\n", + "Unit: KWH\n", + "Data Period: 2025-10-19T22:00:00Z to 2025-10-26T23:00:00Z\n", + "\n", + "============================================================\n", + "CONSUMPTION DATA:\n", + "============================================================\n", + "\n", + "Period starting: 2025-10-19T22:00:00Z\n", + "Resolution: PT1H\n", + "Number of hourly readings: 24\n", + " Hour 1: 0.53 kWh (Quality: A04)\n", + " Hour 2: 0.55 kWh (Quality: A04)\n", + " Hour 3: 0.51 kWh (Quality: A04)\n", + " Hour 4: 0.5 kWh (Quality: A04)\n", + " Hour 5: 0.6 kWh (Quality: A04)\n", + " Hour 6: 0.52 kWh (Quality: A04)\n", + " Hour 7: 0.68 kWh (Quality: A04)\n", + " Hour 8: 0.69 kWh (Quality: A04)\n", + " Hour 9: 0.62 kWh (Quality: A04)\n", + " Hour 10: 0.62 kWh (Quality: A04)\n", + " ... and 14 more hourly readings\n", + "\n", + "Period starting: 2025-10-20T22:00:00Z\n", + "Resolution: PT1H\n", + "Number of hourly readings: 24\n", + " Hour 1: 0.58 kWh (Quality: A04)\n", + " Hour 2: 0.56 kWh (Quality: A04)\n", + " Hour 3: 0.57 kWh (Quality: A04)\n", + " Hour 4: 0.61 kWh (Quality: A04)\n", + " Hour 5: 0.64 kWh (Quality: A04)\n", + " Hour 6: 0.58 kWh (Quality: A04)\n", + " Hour 7: 0.62 kWh (Quality: A04)\n", + " Hour 8: 0.53 kWh (Quality: A04)\n", + " Hour 9: 0.58 kWh (Quality: A04)\n", + " Hour 10: 0.53 kWh (Quality: A04)\n", + " ... and 14 more hourly readings\n", + "\n", + "Period starting: 2025-10-21T22:00:00Z\n", + "Resolution: PT1H\n", + "Number of hourly readings: 24\n", + " Hour 1: 0.62 kWh (Quality: A04)\n", + " Hour 2: 0.62 kWh (Quality: A04)\n", + " Hour 3: 0.5 kWh (Quality: A04)\n", + " Hour 4: 0.59 kWh (Quality: A04)\n", + " Hour 5: 0.57 kWh (Quality: A04)\n", + " Hour 6: 0.58 kWh (Quality: A04)\n", + " Hour 7: 0.55 kWh (Quality: A04)\n", + " Hour 8: 0.69 kWh (Quality: A04)\n", + " Hour 9: 0.77 kWh (Quality: A04)\n", + " Hour 10: 1.0 kWh (Quality: A04)\n", + " ... and 14 more hourly readings\n", + "\n", + "Period starting: 2025-10-22T22:00:00Z\n", + "Resolution: PT1H\n", + "Number of hourly readings: 24\n", + " Hour 1: 0.59 kWh (Quality: A04)\n", + " Hour 2: 0.68 kWh (Quality: A04)\n", + " Hour 3: 0.6 kWh (Quality: A04)\n", + " Hour 4: 0.6 kWh (Quality: A04)\n", + " Hour 5: 0.57 kWh (Quality: A04)\n", + " Hour 6: 0.57 kWh (Quality: A04)\n", + " Hour 7: 0.57 kWh (Quality: A04)\n", + " Hour 8: 0.57 kWh (Quality: A04)\n", + " Hour 9: 0.59 kWh (Quality: A04)\n", + " Hour 10: 0.58 kWh (Quality: A04)\n", + " ... and 14 more hourly readings\n", + "\n", + "Period starting: 2025-10-23T22:00:00Z\n", + "Resolution: PT1H\n", + "Number of hourly readings: 24\n", + " Hour 1: 0.58 kWh (Quality: A04)\n", + " Hour 2: 0.72 kWh (Quality: A04)\n", + " Hour 3: 0.61 kWh (Quality: A04)\n", + " Hour 4: 0.6 kWh (Quality: A04)\n", + " Hour 5: 0.54 kWh (Quality: A04)\n", + " Hour 6: 0.53 kWh (Quality: A04)\n", + " Hour 7: 0.49 kWh (Quality: A04)\n", + " Hour 8: 0.8 kWh (Quality: A04)\n", + " Hour 9: 0.62 kWh (Quality: A04)\n", + " Hour 10: 1.21 kWh (Quality: A04)\n", + " ... and 14 more hourly readings\n", + "\n", + "Period starting: 2025-10-24T22:00:00Z\n", + "Resolution: PT1H\n", + "Number of hourly readings: 24\n", + " Hour 1: 0.89 kWh (Quality: A04)\n", + " Hour 2: 1.15 kWh (Quality: A04)\n", + " Hour 3: 0.58 kWh (Quality: A04)\n", + " Hour 4: 0.58 kWh (Quality: A04)\n", + " Hour 5: 0.62 kWh (Quality: A04)\n", + " Hour 6: 0.61 kWh (Quality: A04)\n", + " Hour 7: 0.58 kWh (Quality: A04)\n", + " Hour 8: 0.56 kWh (Quality: A04)\n", + " Hour 9: 0.53 kWh (Quality: A04)\n", + " Hour 10: 0.61 kWh (Quality: A04)\n", + " ... and 14 more hourly readings\n", + "\n", + "Period starting: 2025-10-25T22:00:00Z\n", + "Resolution: PT1H\n", + "Number of hourly readings: 25\n", + " Hour 1: 0.63 kWh (Quality: A04)\n", + " Hour 2: 0.51 kWh (Quality: A04)\n", + " Hour 3: 0.55 kWh (Quality: A04)\n", + " Hour 4: 0.56 kWh (Quality: A04)\n", + " Hour 5: 0.56 kWh (Quality: A04)\n", + " Hour 6: 0.5 kWh (Quality: A04)\n", + " Hour 7: 0.55 kWh (Quality: A04)\n", + " Hour 8: 0.56 kWh (Quality: A04)\n", + " Hour 9: 0.55 kWh (Quality: A04)\n", + " Hour 10: 0.54 kWh (Quality: A04)\n", + " ... and 15 more hourly readings\n", + "\n", + "============================================================\n", + "SUMMARY:\n", + "Total consumption over 169 hours: 138.31 kWh\n", + "Average hourly consumption: 0.818 kWh\n", + "============================================================\n" + ] + } + ], + "source": [ + "# Parse and display the consumption data in a more readable format\n", + "\n", + "if \"consumption_json\" in locals() and consumption_json:\n", + " try:\n", + " # Extract the time series data\n", + " market_document = consumption_json[\"result\"][0][\"MyEnergyData_MarketDocument\"]\n", + " time_series = market_document[\"TimeSeries\"][0]\n", + " periods = time_series[\"Period\"]\n", + "\n", + " print(f\"Metering Point ID: {time_series['mRID']}\")\n", + " print(f\"Unit: {time_series['measurement_Unit.name']}\")\n", + " print(\n", + " f\"Data Period: {market_document['period.timeInterval']['start']} to {market_document['period.timeInterval']['end']}\"\n", + " )\n", + " print(\"\\n\" + \"=\" * 60)\n", + " print(\"CONSUMPTION DATA:\")\n", + " print(\"=\" * 60)\n", + "\n", + " total_consumption = 0\n", + " data_points = 0\n", + "\n", + " for period in periods:\n", + " period_start = period[\"timeInterval\"][\"start\"]\n", + " print(f\"\\nPeriod starting: {period_start}\")\n", + " print(f\"Resolution: {period['resolution']}\")\n", + "\n", + " if \"Point\" in period:\n", + " points = period[\"Point\"]\n", + " print(f\"Number of hourly readings: {len(points)}\")\n", + "\n", + " for point in points[:10]: # Show first 10 points\n", + " position = point[\"position\"]\n", + " quantity = float(point[\"out_Quantity.quantity\"])\n", + " quality = point[\"out_Quantity.quality\"]\n", + "\n", + " print(f\" Hour {position}: {quantity} kWh (Quality: {quality})\")\n", + " total_consumption += quantity\n", + " data_points += 1\n", + "\n", + " if len(points) > 10:\n", + " print(f\" ... and {len(points) - 10} more hourly readings\")\n", + " # Add remaining consumption to total\n", + " for point in points[10:]:\n", + " total_consumption += float(point[\"out_Quantity.quantity\"])\n", + " data_points += 1\n", + "\n", + " print(\"\\n\" + \"=\" * 60)\n", + " print(\"SUMMARY:\")\n", + " print(\n", + " f\"Total consumption over {data_points} hours: {total_consumption:.2f} kWh\"\n", + " )\n", + " print(f\"Average hourly consumption: {total_consumption / data_points:.3f} kWh\")\n", + " print(\"=\" * 60)\n", + "\n", + " except Exception as e:\n", + " print(f\"Error parsing consumption data: {e}\")\n", + " print(\"Raw data structure:\")\n", + " print(json.dumps(consumption_json, indent=2)[:500] + \"...\")\n", + "else:\n", + " print(\"No consumption data available to parse\")" + ] + }, + { + "cell_type": "markdown", + "id": "2bf461e8", + "metadata": {}, + "source": [ + "# PostgreSQL Database Design\n", + "\n", + "For storing energy consumption data efficiently, we'll design a normalized database schema with the following tables:\n", + "\n", + "## Tables Structure:\n", + "\n", + "### 1. `metering_points` - Store meter information\n", + "- `id` (Primary Key)\n", + "- `metering_point_id` (Unique identifier from API)\n", + "- `street_name`, `building_number`, `city` etc.\n", + "- `meter_number`\n", + "- `consumer_name`\n", + "- `created_at`, `updated_at`\n", + "\n", + "### 2. `consumption_readings` - Store hourly consumption data\n", + "- `id` (Primary Key)\n", + "- `metering_point_id` (Foreign Key)\n", + "- `timestamp` (UTC timestamp for the reading)\n", + "- `consumption_kwh` (Energy consumption in kWh)\n", + "- `quality` (Data quality indicator)\n", + "- `period_resolution` (e.g., 'PT1H' for hourly)\n", + "- `created_at`\n", + "\n", + "### Benefits:\n", + "- **Normalized**: Avoids data duplication\n", + "- **Indexed**: Fast queries on timestamp and metering_point_id\n", + "- **Scalable**: Can handle multiple meters and large time series data\n", + "- **Flexible**: Easy to add new meters or extend with additional fields" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "969c7953", + "metadata": {}, + "outputs": [], + "source": [ + "# SQL Schema for PostgreSQL Database\n", + "\n", + "create_tables_sql = \"\"\"\n", + "-- Create metering_points table\n", + "CREATE TABLE IF NOT EXISTS metering_points (\n", + " id SERIAL PRIMARY KEY,\n", + " metering_point_id VARCHAR(50) UNIQUE NOT NULL,\n", + " street_code VARCHAR(10),\n", + " street_name VARCHAR(255),\n", + " building_number VARCHAR(20),\n", + " floor_id VARCHAR(10),\n", + " room_id VARCHAR(10),\n", + " city_subdivision_name VARCHAR(255),\n", + " municipality_code VARCHAR(10),\n", + " location_description TEXT,\n", + " settlement_method VARCHAR(10),\n", + " meter_reading_occurrence VARCHAR(20),\n", + " first_consumer_party_name VARCHAR(255),\n", + " second_consumer_party_name VARCHAR(255),\n", + " meter_number VARCHAR(50),\n", + " consumer_start_date TIMESTAMP WITH TIME ZONE,\n", + " type_of_mp VARCHAR(10),\n", + " balance_supplier_name VARCHAR(255),\n", + " postcode VARCHAR(10),\n", + " created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n", + " updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n", + ");\n", + "\n", + "-- Create consumption_readings table\n", + "CREATE TABLE IF NOT EXISTS consumption_readings (\n", + " id SERIAL PRIMARY KEY,\n", + " metering_point_id VARCHAR(50) REFERENCES metering_points(metering_point_id),\n", + " timestamp TIMESTAMP WITH TIME ZONE NOT NULL,\n", + " consumption_kwh DECIMAL(10, 3) NOT NULL,\n", + " quality VARCHAR(10),\n", + " period_resolution VARCHAR(10) DEFAULT 'PT1H',\n", + " created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n", + " UNIQUE(metering_point_id, timestamp)\n", + ");\n", + "\n", + "-- Create indexes for performance\n", + "CREATE INDEX IF NOT EXISTS idx_consumption_metering_point ON consumption_readings(metering_point_id);\n", + "CREATE INDEX IF NOT EXISTS idx_consumption_timestamp ON consumption_readings(timestamp);\n", + "CREATE INDEX IF NOT EXISTS idx_consumption_metering_point_timestamp ON consumption_readings(metering_point_id, timestamp);\n", + "\n", + "-- Create a function to update the updated_at timestamp\n", + "CREATE OR REPLACE FUNCTION update_updated_at_column()\n", + "RETURNS TRIGGER AS $$\n", + "BEGIN\n", + " NEW.updated_at = CURRENT_TIMESTAMP;\n", + " RETURN NEW;\n", + "END;\n", + "$$ language 'plpgsql';\n", + "\n", + "-- Create trigger for metering_points\n", + "CREATE TRIGGER update_metering_points_updated_at \n", + " BEFORE UPDATE ON metering_points \n", + " FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n", + "\"\"\"\n", + "\n", + "print(\"PostgreSQL Schema:\")\n", + "print(create_tables_sql)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8159a41", + "metadata": {}, + "outputs": [], + "source": [ + "# Python code to store data in PostgreSQL using psycopg2\n", + "\n", + "import psycopg2\n", + "from datetime import datetime\n", + "\n", + "# Database connection configuration\n", + "DB_CONFIG = {\n", + " \"host\": \"localhost\",\n", + " \"database\": \"energy_consumption\",\n", + " \"user\": \"your_username\",\n", + " \"password\": \"your_password\",\n", + " \"port\": 5432,\n", + "}\n", + "\n", + "\n", + "class EnergyDataStorage:\n", + " def __init__(self, db_config):\n", + " self.db_config = db_config\n", + " self.connection = None\n", + "\n", + " def connect(self):\n", + " \"\"\"Establish database connection\"\"\"\n", + " try:\n", + " self.connection = psycopg2.connect(**self.db_config)\n", + " print(\"Database connection established\")\n", + " return True\n", + " except Exception as e:\n", + " print(f\"Error connecting to database: {e}\")\n", + " return False\n", + "\n", + " def create_tables(self):\n", + " \"\"\"Create database tables if they don't exist\"\"\"\n", + " if not self.connection:\n", + " print(\"No database connection\")\n", + " return False\n", + "\n", + " try:\n", + " with self.connection.cursor() as cursor:\n", + " cursor.execute(create_tables_sql)\n", + " self.connection.commit()\n", + " print(\"Tables created successfully\")\n", + " return True\n", + " except Exception as e:\n", + " print(f\"Error creating tables: {e}\")\n", + " self.connection.rollback()\n", + " return False\n", + "\n", + " def store_metering_point(self, metering_point_data):\n", + " \"\"\"Store or update metering point information\"\"\"\n", + " if not self.connection:\n", + " print(\"No database connection\")\n", + " return False\n", + "\n", + " try:\n", + " with self.connection.cursor() as cursor:\n", + " # Use UPSERT (INSERT ... ON CONFLICT)\n", + " upsert_sql = \"\"\"\n", + " INSERT INTO metering_points (\n", + " metering_point_id, street_code, street_name, building_number,\n", + " floor_id, room_id, city_subdivision_name, municipality_code,\n", + " location_description, settlement_method, meter_reading_occurrence,\n", + " first_consumer_party_name, second_consumer_party_name,\n", + " meter_number, consumer_start_date, type_of_mp,\n", + " balance_supplier_name, postcode\n", + " ) VALUES (\n", + " %(meteringPointId)s, %(streetCode)s, %(streetName)s, %(buildingNumber)s,\n", + " %(floorId)s, %(roomId)s, %(citySubDivisionName)s, %(municipalityCode)s,\n", + " %(locationDescription)s, %(settlementMethod)s, %(meterReadingOccurrence)s,\n", + " %(firstConsumerPartyName)s, %(secondConsumerPartyName)s,\n", + " %(meterNumber)s, %(consumerStartDate)s, %(typeOfMP)s,\n", + " %(balanceSupplierName)s, %(postcode)s\n", + " )\n", + " ON CONFLICT (metering_point_id) \n", + " DO UPDATE SET\n", + " street_code = EXCLUDED.street_code,\n", + " street_name = EXCLUDED.street_name,\n", + " building_number = EXCLUDED.building_number,\n", + " updated_at = CURRENT_TIMESTAMP;\n", + " \"\"\"\n", + "\n", + " cursor.execute(upsert_sql, metering_point_data)\n", + " self.connection.commit()\n", + " print(\n", + " f\"Metering point {metering_point_data['meteringPointId']} stored successfully\"\n", + " )\n", + " return True\n", + "\n", + " except Exception as e:\n", + " print(f\"Error storing metering point: {e}\")\n", + " self.connection.rollback()\n", + " return False\n", + "\n", + " def store_consumption_readings(self, metering_point_id, consumption_data):\n", + " \"\"\"Store consumption readings with conflict handling\"\"\"\n", + " if not self.connection:\n", + " print(\"No database connection\")\n", + " return False\n", + "\n", + " try:\n", + " with self.connection.cursor() as cursor:\n", + " # Prepare batch insert with conflict handling\n", + " insert_sql = \"\"\"\n", + " INSERT INTO consumption_readings (\n", + " metering_point_id, timestamp, consumption_kwh, quality, period_resolution\n", + " ) VALUES (\n", + " %s, %s, %s, %s, %s\n", + " )\n", + " ON CONFLICT (metering_point_id, timestamp) \n", + " DO UPDATE SET\n", + " consumption_kwh = EXCLUDED.consumption_kwh,\n", + " quality = EXCLUDED.quality;\n", + " \"\"\"\n", + "\n", + " readings_data = []\n", + " for reading in consumption_data:\n", + " readings_data.append(\n", + " (\n", + " metering_point_id,\n", + " reading[\"timestamp\"],\n", + " reading[\"consumption_kwh\"],\n", + " reading[\"quality\"],\n", + " reading.get(\"period_resolution\", \"PT1H\"),\n", + " )\n", + " )\n", + "\n", + " cursor.executemany(insert_sql, readings_data)\n", + " self.connection.commit()\n", + " print(f\"Stored {len(readings_data)} consumption readings\")\n", + " return True\n", + "\n", + " except Exception as e:\n", + " print(f\"Error storing consumption readings: {e}\")\n", + " self.connection.rollback()\n", + " return False\n", + "\n", + " def close(self):\n", + " \"\"\"Close database connection\"\"\"\n", + " if self.connection:\n", + " self.connection.close()\n", + " print(\"Database connection closed\")\n", + "\n", + "\n", + "# Example usage:\n", + "print(\"EnergyDataStorage class defined. Use it like this:\")\n", + "print(\"\"\"\n", + "# Initialize storage\n", + "storage = EnergyDataStorage(DB_CONFIG)\n", + "storage.connect()\n", + "storage.create_tables()\n", + "\n", + "# Store metering point data\n", + "storage.store_metering_point(metering_point_data)\n", + "\n", + "# Store consumption readings\n", + "storage.store_consumption_readings(metering_point_id, readings_list)\n", + "\n", + "storage.close()\n", + "\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d00bdc8f", + "metadata": {}, + "outputs": [], + "source": [ + "# Function to transform API data for database storage\n", + "\n", + "\n", + "def transform_consumption_data_for_db(consumption_json):\n", + " \"\"\"\n", + " Transform the Eloverblik API response into database-ready format\n", + " \"\"\"\n", + " if not consumption_json or \"result\" not in consumption_json:\n", + " return None, []\n", + "\n", + " try:\n", + " # Extract metering point data\n", + " market_document = consumption_json[\"result\"][0][\"MyEnergyData_MarketDocument\"]\n", + " time_series = market_document[\"TimeSeries\"][0]\n", + " metering_point_id = time_series[\"mRID\"]\n", + "\n", + " # For demonstration, we'll create a basic metering point record\n", + " # In practice, you'd get this from the metering points API call\n", + " metering_point_data = {\n", + " \"meteringPointId\": metering_point_id,\n", + " \"streetCode\": None,\n", + " \"streetName\": None,\n", + " \"buildingNumber\": None,\n", + " \"floorId\": None,\n", + " \"roomId\": None,\n", + " \"citySubDivisionName\": None,\n", + " \"municipalityCode\": None,\n", + " \"locationDescription\": None,\n", + " \"settlementMethod\": None,\n", + " \"meterReadingOccurrence\": None,\n", + " \"firstConsumerPartyName\": None,\n", + " \"secondConsumerPartyName\": None,\n", + " \"meterNumber\": None,\n", + " \"consumerStartDate\": None,\n", + " \"typeOfMP\": None,\n", + " \"balanceSupplierName\": None,\n", + " \"postcode\": None,\n", + " }\n", + "\n", + " # Extract consumption readings\n", + " consumption_readings = []\n", + " periods = time_series[\"Period\"]\n", + "\n", + " for period in periods:\n", + " period_start = datetime.fromisoformat(\n", + " period[\"timeInterval\"][\"start\"].replace(\"Z\", \"+00:00\")\n", + " )\n", + " resolution = period[\"resolution\"]\n", + "\n", + " if \"Point\" in period:\n", + " for point in period[\"Point\"]:\n", + " # Calculate the actual timestamp for this point\n", + " position = int(point[\"position\"])\n", + " # Position is 1-based, so subtract 1 to get hours offset\n", + " hours_offset = position - 1\n", + "\n", + " point_timestamp = period_start + timedelta(hours=hours_offset)\n", + "\n", + " reading = {\n", + " \"timestamp\": point_timestamp,\n", + " \"consumption_kwh\": float(point[\"out_Quantity.quantity\"]),\n", + " \"quality\": point[\"out_Quantity.quality\"],\n", + " \"period_resolution\": resolution,\n", + " }\n", + " consumption_readings.append(reading)\n", + "\n", + " return metering_point_data, consumption_readings\n", + "\n", + " except Exception as e:\n", + " print(f\"Error transforming data: {e}\")\n", + " return None, []\n", + "\n", + "\n", + "# Test the transformation with our existing data\n", + "if \"consumption_json\" in locals() and consumption_json:\n", + " metering_point, readings = transform_consumption_data_for_db(consumption_json)\n", + "\n", + " if metering_point and readings:\n", + " print(\n", + " f\"Transformed data for metering point: {metering_point['meteringPointId']}\"\n", + " )\n", + " print(f\"Number of readings: {len(readings)}\")\n", + " print(\"\\nSample readings:\")\n", + " for i, reading in enumerate(readings[:3]):\n", + " print(\n", + " f\" {i + 1}. {reading['timestamp']}: {reading['consumption_kwh']} kWh (Quality: {reading['quality']})\"\n", + " )\n", + " print(\" ...\")\n", + " else:\n", + " print(\"Failed to transform data\")\n", + "else:\n", + " print(\"No consumption data available to transform\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "energy-consumption-ingester", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/README.md b/README.md index 9b4e380..514187f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,380 @@ -# energy-consumption-ingester +# Energy Consumption Ingester -Scheduled pipeline that ingests energy consumption data. \ No newline at end of file +A Python package for ingesting energy consumption data from the Eloverblik API and storing it in PostgreSQL. + +## Project Structure + +``` +energy-consumption-ingester/ +├── src/ +│ └── energy_consumption_ingester/ +│ ├── __init__.py # Package initialization +│ ├── eloverblik.py # Eloverblik API client +│ ├── database.py # PostgreSQL storage +│ ├── utils.py # Data transformation utilities +│ ├── cli.py # Command line interface +│ └── config.py # Configuration and examples +├── scripts/ +│ └── create_db_user.sh # Database setup script +├── example.py # Usage example +├── main.py # Original script +├── POC.ipynb # Jupyter notebook for development +├── pyproject.toml # Package configuration +├── README.md # This file +└── .env # Environment variables (create this) +``` + +## Installation + +### Prerequisites + +1. **PostgreSQL** - Make sure you have PostgreSQL installed and running +2. **Python 3.12+** - Required Python version +3. **uv** - Package manager + +### Setup + +1. **Clone and enter the repository:** + + ```bash + cd energy-consumption-ingester + ``` + +2. **Install dependencies using uv:** + + ```bash + uv sync + ``` + +3. **Create environment file:** + + ```bash + cp .env.example .env + # Edit .env with your credentials + ``` + +4. **Set up PostgreSQL database:** + + ```bash + # Create database + createdb energy_consumption + + # Create user (optional - run the script) + chmod +x scripts/create_db_user.sh + ./scripts/create_db_user.sh + ``` + +## Configuration + +Create a `.env` file in the project root: + +```env +# Eloverblik API Configuration +ELOVERBLIK_API_TOKEN=your_refresh_token_here + +# Database Configuration +DB_HOST=localhost +DB_NAME=energy_consumption +DB_USER=energy_user +DB_PASSWORD=your_password_here +DB_PORT=5432 +``` + +## Usage + +### Command Line Interface + +The package provides a CLI for common operations: + +```bash +# Incremental sync (recommended for CI/CD hourly jobs) +uv run energy-ingester sync-incremental + +# Full sync for initial setup or backfill +uv run energy-ingester sync --days 30 + +# Health check +uv run energy-ingester health-check + +# Database setup (initial setup) +uv run energy-ingester setup-database + +# Database reset (DESTRUCTIVE - deletes all data) +uv run energy-ingester setup-database --reset + +# Manual sync with custom parameters +uv run energy-ingester sync-incremental --hours-back 48 --verbose +``` + +### CI/CD Integration + +For **Gitea CI** (recommended for hourly sync): + +```bash +# This is the command your CI should run every hour +uv run energy-ingester sync-incremental --verbose +``` + +The incremental sync is optimized for frequent runs: + +- Only syncs recent data (last 25 hours by default) +- Detects and fills gaps automatically +- Handles overlapping data gracefully +- Fast execution suitable for hourly scheduling + +### Python API + +```python +from energy_consumption_ingester import ( + EloverblikClient, + DatabaseStorage, + transform_metering_point_data, + transform_consumption_data +) + +# Initialize clients from environment variables +client = EloverblikClient.from_env() +db = DatabaseStorage.from_env() + +# Connect and setup database +db.connect() +db.create_tables() + +# Get metering points +metering_points = client.get_metering_points() + +# Get consumption data +consumption_data = client.get_consumption_data_last_days( + metering_points[0]['meteringPointId'], + days=7 +) + +# Transform and store data +mp_data = transform_metering_point_data(metering_points[0]) +db.store_metering_point(mp_data) + +metering_point_id, readings = transform_consumption_data(consumption_data) +db.store_consumption_readings(metering_point_id, readings) + +db.close() +``` + +### Example Script + +Run the included example: + +```bash +uv run python example.py +``` + +## Development + +### Install development dependencies: + +```bash +uv sync --group dev +``` + +### Code formatting and linting: + +```bash +# Format code +uv run black src/ example.py + +# Lint code +uv run flake8 src/ example.py + +# Type checking +uv run mypy src/ +``` + +### Testing: + +```bash +uv run pytest +``` + +## CI/CD Setup for Gitea + +### Automated Workflows + +The project includes pre-configured Gitea workflows in `.gitea/workflows/`: + +1. **`hourly-sync.yml`** - Runs every hour to sync energy data +2. **`daily-health-check.yml`** - Daily system health monitoring +3. **`manual-full-sync.yml`** - Manual trigger for full data backfill +4. **`database-setup.yml`** - Manual database setup and management + +### Required Secrets + +Configure these secrets in your Gitea repository settings: + +``` +ELOVERBLIK_API_TOKEN # Your Eloverblik refresh token +DB_HOST # Database host (e.g., your-db-server.com) +DB_NAME # Database name (e.g., energy_consumption) +DB_USER # Database username +DB_PASSWORD # Database password +DB_PORT # Database port (usually 5432) +``` + +### Workflow Schedule + +- **Hourly Sync**: Runs at 5 minutes past every hour (`5 * * * *`) +- **Health Check**: Runs daily at 6:00 AM UTC (`0 6 * * *`) +- **Manual Sync**: Can be triggered manually with custom day range + +### Commands for CI + +```bash +# Recommended for hourly CI jobs (fast, incremental) +uv run energy-ingester sync-incremental --verbose + +# For initial setup or recovery (slower, comprehensive) +uv run energy-ingester sync --days 30 --verbose + +# For monitoring and alerting +uv run energy-ingester health-check --verbose + +# Database setup (run once initially or after schema changes) +uv run energy-ingester setup-database --verbose +``` + +### Database Setup Workflow + +The `database-setup.yml` workflow provides safe database management: + +- **Setup**: Create tables and schema (safe, idempotent) +- **Migrate**: Re-run table creation (for schema updates) +- **Reset**: Drop all tables and recreate (DESTRUCTIVE - requires confirmation) + +To run the database setup in Gitea CI: + +1. Go to Actions → Database Setup +2. Choose action: `setup`, `migrate`, or `reset` +3. For reset operations, type `yes` in the confirm field +4. Run workflow + +### Docker Deployment + +For containerized deployment: + +```bash +# Build and run with Docker Compose +docker-compose up -d + +# Or build manually +docker build -t energy-ingester . +docker run -e ELOVERBLIK_API_TOKEN=your_token energy-ingester +``` + +### Monitoring and Alerts + +The workflows are designed to: + +- Exit with error codes on failure (for CI/CD detection) +- Log detailed information for debugging +- Support notification integration (extend the workflows) + +To add Slack/email notifications, modify the workflow files in `.gitea/workflows/`. + +## Modules + +### 1. `eloverblik.py` - API Client + +**Purpose:** Handles all interactions with the Eloverblik API + +**Key Features:** + +- Automatic token refresh (refresh token → access token) +- Metering points discovery +- Consumption data retrieval with flexible date ranges +- Error handling and retry logic +- Environment variable configuration + +**Main Methods:** + +- `get_access_token()` - Exchange refresh token for access token +- `get_metering_points()` - Retrieve all metering points +- `get_consumption_data()` - Get consumption data for date range +- `get_consumption_data_last_days()` - Convenience method for recent data + +### 2. `database.py` - PostgreSQL Storage + +**Purpose:** Handles all database operations for storing energy data + +**Key Features:** + +- Normalized database schema design +- UPSERT operations (handles duplicates gracefully) +- Proper indexing for performance +- Transaction management +- Connection pooling ready + +**Main Methods:** + +- `create_tables()` - Set up database schema +- `store_metering_point()` - Store/update metering point info +- `store_consumption_readings()` - Batch insert consumption data +- `get_latest_reading_timestamp()` - Find last stored reading + +**Database Schema:** + +- `metering_points` - Meter metadata (location, consumer info, etc.) +- `consumption_readings` - Time series consumption data + +### 3. `utils.py` - Data Transformation + +**Purpose:** Transform API responses to database-ready format + +**Key Features:** + +- API data normalization +- Timestamp parsing and timezone handling +- Data validation +- Statistics calculation + +## Architecture Benefits + +### 1. **Separation of Concerns** + +- **API layer** (`eloverblik.py`) - Pure API interactions +- **Storage layer** (`database.py`) - Pure database operations +- **Transform layer** (`utils.py`) - Data processing logic + +### 2. **Modular Design** + +- Each module can be used independently +- Easy to test individual components +- Simple to extend or replace components + +### 3. **Environment-Based Configuration** + +- No hardcoded credentials +- Easy deployment across environments +- Secure credential management + +### 4. **uv Package Management** + +- Fast dependency resolution +- Proper Python packaging +- Development vs production dependencies +- Script entry points + +### 5. **Database Design** + +- Normalized schema prevents data duplication +- Proper indexing for time-series queries +- UPSERT operations handle data refresh scenarios +- Extensible for multiple meters/customers + +## Best Practices Implemented + +1. **Type Hints** - Full type annotation for better IDE support and validation +2. **Error Handling** - Comprehensive exception handling with logging +3. **Configuration Management** - Environment variables with validation +4. **Documentation** - Comprehensive docstrings and README +5. **CLI Interface** - User-friendly command line tool +6. **Packaging** - Proper Python package structure with `pyproject.toml` + +This structure gives you a professional, maintainable, and scalable foundation for your energy consumption ingestion project! diff --git a/example.py b/example.py new file mode 100644 index 0000000..5fcab69 --- /dev/null +++ b/example.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Example script demonstrating the energy consumption ingester.""" + +import logging +from typing import Any + +# Optional import for development +try: + from dotenv import load_dotenv + + _load_dotenv: Any | None = load_dotenv +except ImportError: + _load_dotenv = None + +from energy_consumption_ingester import ( + EloverblikClient, + DatabaseStorage, + transform_metering_point_data, + transform_consumption_data, +) + + +def main() -> None: + """Example usage of the energy consumption ingester.""" + # Load environment variables from .env file (if available) + if _load_dotenv is not None: + _load_dotenv() + + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + try: + # Initialize the Eloverblik client + logger.info("Initializing Eloverblik client...") + client = EloverblikClient.from_env() + + # Initialize database storage + logger.info("Initializing database connection...") + db = DatabaseStorage.from_env() + + # Connect to database and create tables + if not db.connect(): + logger.error("Failed to connect to database") + return + + if not db.create_tables(): + logger.error("Failed to create database tables") + return + + # Get metering points + logger.info("Fetching metering points...") + metering_points = client.get_metering_points() + + if not metering_points: + logger.error("No metering points found") + return + + logger.info(f"Found {len(metering_points)} metering points") + + # Process the first metering point + mp_data = metering_points[0] + metering_point_id = mp_data["meteringPointId"] + + logger.info(f"Processing metering point: {metering_point_id}") + + # Transform and store metering point data + transformed_mp = transform_metering_point_data(mp_data) + if db.store_metering_point(transformed_mp): + logger.info("Metering point stored successfully") + + # Get consumption data for the last 7 days + logger.info("Fetching consumption data...") + consumption_data = client.get_consumption_data_last_days( + metering_point_id, days=7 + ) + + if consumption_data: + # Transform consumption data + _, readings = transform_consumption_data(consumption_data) + + if readings: + # Store consumption readings + if db.store_consumption_readings(metering_point_id, readings): + logger.info(f"Stored {len(readings)} consumption readings") + + # Show some statistics + total_consumption = sum(r["consumption_kwh"] for r in readings) + avg_consumption = total_consumption / len(readings) + + logger.info(f"Total consumption: {total_consumption:.2f} kWh") + logger.info( + f"Average hourly consumption: {avg_consumption:.3f} kWh" + ) + else: + logger.error("Failed to store consumption readings") + else: + logger.warning("No consumption readings to store") + else: + logger.warning("No consumption data retrieved") + + except Exception as e: + logger.error(f"Error: {e}") + finally: + # Close database connection + if "db" in locals(): + db.close() + logger.info("Database connection closed") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..266cc2d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "energy-consumption-ingester" +version = "0.1.0" +description = "Energy consumption data ingester for Eloverblik API with PostgreSQL storage" +readme = "README.md" +requires-python = ">=3.12" +authors = [ + {name = "Brian Bjarke Jensen", email = "your-email@example.com"}, +] +keywords = ["energy", "consumption", "eloverblik", "postgresql", "data"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "pydantic>=2.12.3", + "psycopg2-binary>=2.9.0", +] + +[dependency-groups] +dev = [ + "ipykernel>=7.0.1", + "python-dotenv>=1.0.0", + "pytest>=7.0.0", + "black>=23.0.0", + "flake8>=6.0.0", + "mypy>=1.0.0", + "ruff>=0.14.2", + "pyupgrade>=3.21.0", + "types-psycopg2>=2.9.21.20251012", + "pre-commit>=4.3.0", +] + +[project.scripts] +energy-ingester = "energy_consumption_ingester.cli:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/energy_consumption_ingester"] + +[tool.black] +line-length = 88 +target-version = ['py312'] + +[tool.mypy] +python_version = "3.12" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true diff --git a/src/energy_consumption_ingester/__init__.py b/src/energy_consumption_ingester/__init__.py new file mode 100644 index 0000000..87d8848 --- /dev/null +++ b/src/energy_consumption_ingester/__init__.py @@ -0,0 +1,21 @@ +"""Energy consumption data ingester for Eloverblik API.""" + +__version__ = "0.1.0" + +from .eloverblik import EloverblikClient +from .database import DatabaseStorage +from .scheduler import ScheduledSync +from .utils import ( + transform_metering_point_data, + transform_consumption_data, + calculate_consumption_stats, +) + +__all__ = [ + "EloverblikClient", + "DatabaseStorage", + "ScheduledSync", + "transform_metering_point_data", + "transform_consumption_data", + "calculate_consumption_stats", +] diff --git a/src/energy_consumption_ingester/__main__.py b/src/energy_consumption_ingester/__main__.py new file mode 100644 index 0000000..a144f1a --- /dev/null +++ b/src/energy_consumption_ingester/__main__.py @@ -0,0 +1,6 @@ +"""CLI module entry point for python -m execution.""" + +from .cli import main + +if __name__ == "__main__": + main() diff --git a/src/energy_consumption_ingester/cli.py b/src/energy_consumption_ingester/cli.py new file mode 100644 index 0000000..72b3c47 --- /dev/null +++ b/src/energy_consumption_ingester/cli.py @@ -0,0 +1,374 @@ +"""Command line interface for energy consumption ingester.""" + +import argparse +import logging +import os +import sys +from typing import Any + +# Optional import for development +try: + from dotenv import load_dotenv + + _load_dotenv: Any | None = load_dotenv +except ImportError: + _load_dotenv = None + +from .eloverblik import EloverblikClient +from .database import DatabaseStorage +from .utils import transform_metering_point_data, transform_consumption_data +from .scheduler import ScheduledSync + + +def load_environment() -> None: + """Load environment variables from .env file if available.""" + if _load_dotenv: + _load_dotenv() + + +def setup_logging(verbose: bool = False) -> None: + """Set up logging configuration.""" + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig( + level=level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + + +def sync_incremental(hours_back: int = 25, verbose: bool = False) -> int: + """Sync consumption data incrementally for the last specified hours.""" + setup_logging(verbose) + logger = logging.getLogger(__name__) + load_environment() + + # Get required environment variables + api_token = os.getenv("ELOVERBLIK_API_TOKEN") + if not api_token: + logger.error("ELOVERBLIK_API_TOKEN environment variable is required") + return 1 + + try: + # Initialize scheduled sync + sync = ScheduledSync.from_env() + + # Connect to database + if not sync.db.connect(): + logger.error("Failed to connect to database") + return 1 + + if not sync.db.create_tables(): + logger.error("Failed to create database tables") + return 1 + + # Perform incremental sync + logger.info("Starting incremental synchronization") + if sync.sync_incremental(hours_back=hours_back): + logger.info("Incremental sync completed successfully") + return 0 + else: + logger.error("Incremental sync failed") + return 1 + + except Exception as e: + logger.error(f"Error during incremental sync: {e}") + return 1 + finally: + if "sync" in locals(): + sync.db.close() + + +def setup_database(reset: bool = False, verbose: bool = False) -> int: + """Setup the database schema.""" + setup_logging(verbose) + logger = logging.getLogger(__name__) + load_environment() + + db_params: dict[str, Any] = { + "host": os.getenv("DB_HOST", "localhost"), + "port": int(os.getenv("DB_PORT", "5432")), + "database": os.getenv("DB_NAME", "energy_consumption"), + "user": os.getenv("DB_USER"), + "password": os.getenv("DB_PASSWORD"), + } + + if not db_params["user"] or not db_params["password"]: + logger.error("DB_USER and DB_PASSWORD environment variables are required") + return 1 + + try: + db = DatabaseStorage(db_params) + + # Connect to database + if not db.connect(): + logger.error("Failed to connect to database") + return 1 + + # Drop tables if reset is requested + if reset or os.getenv("RESET_DB", "").lower() in ("true", "1", "yes"): + logger.info("Resetting database (dropping existing tables)") + if db.connection is not None: + cursor = db.connection.cursor() + cursor.execute("DROP TABLE IF EXISTS consumption_readings CASCADE;") + cursor.execute("DROP TABLE IF EXISTS metering_points CASCADE;") + cursor.execute( + "DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;" + ) + db.connection.commit() + logger.info("Existing tables dropped") + + # Create database schema + if db.create_tables(): + logger.info("Database schema created successfully") + return 0 + else: + logger.error("Failed to create database schema") + return 1 + except Exception as e: + logger.error(f"Error setting up database: {e}") + return 1 + finally: + if "db" in locals(): + db.close() + + +def health_check(verbose: bool = False) -> int: + """Perform a health check of the system.""" + setup_logging(verbose) + logger = logging.getLogger(__name__) + load_environment() + + # Get required environment variables + api_token = os.getenv("ELOVERBLIK_API_TOKEN") + if not api_token: + logger.error("ELOVERBLIK_API_TOKEN environment variable is required") + return 1 + + db_params: dict[str, Any] = { + "host": os.getenv("DB_HOST", "localhost"), + "port": int(os.getenv("DB_PORT", "5432")), + "database": os.getenv("DB_NAME", "energy_consumption"), + "user": os.getenv("DB_USER"), + "password": os.getenv("DB_PASSWORD"), + } + + if not db_params["user"] or not db_params["password"]: + logger.error("DB_USER and DB_PASSWORD environment variables are required") + return 1 + + try: + from .scheduler import ScheduledSync + + logger.info("Starting health check") + + # Initialize clients + eloverblik = EloverblikClient(api_token) + db = DatabaseStorage(db_params) + + # Create ScheduledSync with proper instances + sync = ScheduledSync(eloverblik, db) + + if sync.health_check(): + logger.info("Health check passed") + return 0 + else: + logger.error("Health check failed") + return 1 + except Exception as e: + logger.error(f"Error during health check: {e}") + return 1 + + +def sync_full_backfill(verbose: bool = False) -> int: + """Perform a full backfill synchronization.""" + setup_logging(verbose) + logger = logging.getLogger(__name__) + load_environment() + + # Get required environment variables + api_token = os.getenv("ELOVERBLIK_API_TOKEN") + if not api_token: + logger.error("ELOVERBLIK_API_TOKEN environment variable is required") + return 1 + + try: + # Initialize scheduled sync + sync = ScheduledSync.from_env() + + # Connect to database + if not sync.db.connect(): + logger.error("Failed to connect to database") + return 1 + + if not sync.db.create_tables(): + logger.error("Failed to create database tables") + return 1 + + # Perform full backfill sync + logger.info("Starting full backfill synchronization") + if sync.sync_full_backfill(): + logger.info("Full backfill sync completed successfully") + return 0 + else: + logger.error("Full backfill sync failed") + return 1 + + except Exception as e: + logger.error(f"Error during full backfill sync: {e}") + return 1 + finally: + if "sync" in locals(): + sync.db.close() + + +def sync_data(days: int = 7, verbose: bool = False) -> None: + """ + Sync energy consumption data from Eloverblik to PostgreSQL. + + Args: + days: Number of days to sync (from today backwards) + verbose: Enable verbose logging + """ + setup_logging(verbose) + logger = logging.getLogger(__name__) + + # Load environment variables + load_environment() + + try: + # Initialize clients + eloverblik = EloverblikClient.from_env() + db = DatabaseStorage.from_env() + + # Connect to database and create tables + if not db.connect(): + logger.error("Failed to connect to database") + sys.exit(1) + + if not db.create_tables(): + logger.error("Failed to create database tables") + sys.exit(1) + + # Get metering points + logger.info("Fetching metering points...") + metering_points = eloverblik.get_metering_points() + + if not metering_points: + logger.error("No metering points found") + sys.exit(1) + + logger.info(f"Found {len(metering_points)} metering points") + + # Process each metering point + for mp_data in metering_points: + metering_point_id = mp_data["meteringPointId"] + logger.info(f"Processing metering point: {metering_point_id}") + + # Store metering point data + transformed_mp = transform_metering_point_data(mp_data) + if not db.store_metering_point(transformed_mp): + logger.error(f"Failed to store metering point {metering_point_id}") + continue + + # Get consumption data + logger.info(f"Fetching {days} days of consumption data...") + consumption_data = eloverblik.get_consumption_data_last_days( + metering_point_id, days + ) + + if not consumption_data: + logger.warning(f"No consumption data for {metering_point_id}") + continue + + # Transform and store consumption data + _, readings = transform_consumption_data(consumption_data) + + if readings: + if db.store_consumption_readings(metering_point_id, readings): + logger.info( + f"Stored {len(readings)} readings for {metering_point_id}" + ) + else: + logger.error(f"Failed to store readings for {metering_point_id}") + else: + logger.warning(f"No readings to store for {metering_point_id}") + + logger.info("Data synchronization completed successfully") + + except Exception as e: + logger.error(f"Error during data sync: {e}") + sys.exit(1) + finally: + if "db" in locals(): + db.close() + + +def main() -> None: + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + description="Energy consumption data ingester for Eloverblik API" + ) + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Sync command (legacy - full sync) + sync_parser = subparsers.add_parser( + "sync", help="Full sync from Eloverblik to PostgreSQL" + ) + sync_parser.add_argument( + "--days", type=int, default=7, help="Number of days to sync (default: 7)" + ) + sync_parser.add_argument( + "--verbose", action="store_true", help="Enable verbose logging" + ) + + # Incremental sync command (optimized for CI) + incr_parser = subparsers.add_parser( + "sync-incremental", + help="Incremental sync for CI/CD pipelines (recommended for hourly jobs)", + ) + incr_parser.add_argument( + "--hours-back", + type=int, + default=25, + help="Hours to look back for gaps (default: 25)", + ) + incr_parser.add_argument( + "--verbose", action="store_true", help="Enable verbose logging" + ) + + # Health check command + health_parser = subparsers.add_parser( + "health-check", help="Check system health (API connectivity, database, etc.)" + ) + health_parser.add_argument( + "--verbose", action="store_true", help="Enable verbose logging" + ) + + # Database setup command + db_parser = subparsers.add_parser( + "setup-database", help="Set up database tables and schema" + ) + db_parser.add_argument( + "--reset", + action="store_true", + help="Reset database (DESTRUCTIVE - drops all tables)", + ) + db_parser.add_argument( + "--verbose", action="store_true", help="Enable verbose logging" + ) + + args = parser.parse_args() + + if args.command == "sync": + sync_data(days=args.days, verbose=args.verbose) + elif args.command == "sync-incremental": + sync_incremental(hours_back=args.hours_back, verbose=args.verbose) + elif args.command == "health-check": + health_check(verbose=args.verbose) + elif args.command == "setup-database": + setup_database(reset=args.reset, verbose=args.verbose) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/src/energy_consumption_ingester/config.py b/src/energy_consumption_ingester/config.py new file mode 100644 index 0000000..f2f7fb2 --- /dev/null +++ b/src/energy_consumption_ingester/config.py @@ -0,0 +1,48 @@ +"""Test configuration and example usage.""" + +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Example configuration +EXAMPLE_ENV_FILE = """ +# Eloverblik API Configuration +ELOVERBLIK_API_TOKEN=your_refresh_token_here + +# Database Configuration +DB_HOST=localhost +DB_NAME=energy_consumption +DB_USER=energy_user +DB_PASSWORD=your_password_here +DB_PORT=5432 +""" + +# Example usage +EXAMPLE_USAGE = """ +# Basic usage example +from energy_consumption_ingester import EloverblikClient, DatabaseStorage + +# Initialize clients +client = EloverblikClient.from_env() +db = DatabaseStorage.from_env() + +# Connect and sync data +db.connect() +db.create_tables() + +# Get metering points +metering_points = client.get_metering_points() + +# Get consumption data for last 7 days +consumption_data = client.get_consumption_data_last_days( + metering_points[0]['meteringPointId'], + days=7 +) +""" + +if __name__ == "__main__": + print("Example .env file content:") + print(EXAMPLE_ENV_FILE) + print("\nExample usage:") + print(EXAMPLE_USAGE) diff --git a/src/energy_consumption_ingester/database.py b/src/energy_consumption_ingester/database.py new file mode 100644 index 0000000..6c751f2 --- /dev/null +++ b/src/energy_consumption_ingester/database.py @@ -0,0 +1,310 @@ +"""Database storage module for energy consumption data.""" + +import logging +from datetime import datetime +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + import psycopg2 + from psycopg2.extras import RealDictCursor +else: + try: + import psycopg2 + from psycopg2.extras import RealDictCursor + except ImportError: + psycopg2 = None # type: ignore + RealDictCursor = None # type: ignore + + +class DatabaseStorage: + """PostgreSQL database storage for energy consumption data.""" + + def __init__(self, db_config: dict[str, Any]): + """ + Initialize database storage. + + Args: + db_config: Database connection configuration dict + """ + if psycopg2 is None: + raise ImportError( + "psycopg2 not installed. Install with: uv add psycopg2-binary" + ) + + self.db_config = db_config + self.connection = None + self.logger = logging.getLogger(__name__) + + def connect(self) -> bool: + """ + Establish database connection. + + Returns: + True if successful, False otherwise + """ + try: + self.connection = psycopg2.connect(**self.db_config) + self.logger.info("Database connection established") + return True + except Exception as e: + self.logger.error(f"Error connecting to database: {e}") + return False + + def create_tables(self) -> bool: + """ + Create database tables if they don't exist. + + Returns: + True if successful, False otherwise + """ + if not self.connection: + self.logger.error("No database connection") + return False + + create_tables_sql = """ + -- Create metering_points table + CREATE TABLE IF NOT EXISTS metering_points ( + id SERIAL PRIMARY KEY, + metering_point_id VARCHAR(50) UNIQUE NOT NULL, + street_code VARCHAR(10), + street_name VARCHAR(255), + building_number VARCHAR(20), + floor_id VARCHAR(10), + room_id VARCHAR(10), + city_subdivision_name VARCHAR(255), + municipality_code VARCHAR(10), + location_description TEXT, + settlement_method VARCHAR(10), + meter_reading_occurrence VARCHAR(20), + first_consumer_party_name VARCHAR(255), + second_consumer_party_name VARCHAR(255), + meter_number VARCHAR(50), + consumer_start_date TIMESTAMP WITH TIME ZONE, + type_of_mp VARCHAR(10), + balance_supplier_name VARCHAR(255), + postcode VARCHAR(10), + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ); + + -- Create consumption_readings table + CREATE TABLE IF NOT EXISTS consumption_readings ( + id SERIAL PRIMARY KEY, + metering_point_id VARCHAR(50) REFERENCES metering_points(metering_point_id), + timestamp TIMESTAMP WITH TIME ZONE NOT NULL, + consumption_kwh DECIMAL(10, 3) NOT NULL, + quality VARCHAR(10), + period_resolution VARCHAR(10) DEFAULT 'PT1H', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(metering_point_id, timestamp) + ); + + -- Create indexes for performance + CREATE INDEX IF NOT EXISTS idx_consumption_metering_point + ON consumption_readings(metering_point_id); + CREATE INDEX IF NOT EXISTS idx_consumption_timestamp + ON consumption_readings(timestamp); + CREATE INDEX IF NOT EXISTS idx_consumption_metering_point_timestamp + ON consumption_readings(metering_point_id, timestamp); + + -- Create a function to update the updated_at timestamp + CREATE OR REPLACE FUNCTION update_updated_at_column() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; + END; + $$ language 'plpgsql'; + + -- Create trigger for metering_points + CREATE TRIGGER update_metering_points_updated_at + BEFORE UPDATE ON metering_points + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + """ + + try: + with self.connection.cursor() as cursor: + cursor.execute(create_tables_sql) + self.connection.commit() + self.logger.info("Tables created successfully") + return True + except Exception as e: + self.logger.error(f"Error creating tables: {e}") + self.connection.rollback() + return False + + def store_metering_point(self, metering_point_data: dict[str, Any]) -> bool: + """ + Store or update metering point information. + + Args: + metering_point_data: Dictionary containing metering point data + + Returns: + True if successful, False otherwise + """ + if not self.connection: + self.logger.error("No database connection") + return False + + try: + with self.connection.cursor() as cursor: + upsert_sql = """ + INSERT INTO metering_points ( + metering_point_id, street_code, street_name, building_number, + floor_id, room_id, city_subdivision_name, municipality_code, + location_description, settlement_method, meter_reading_occurrence, + first_consumer_party_name, second_consumer_party_name, + meter_number, consumer_start_date, type_of_mp, + balance_supplier_name, postcode + ) VALUES ( + %(meteringPointId)s, %(streetCode)s, %(streetName)s, %(buildingNumber)s, + %(floorId)s, %(roomId)s, %(citySubDivisionName)s, %(municipalityCode)s, + %(locationDescription)s, %(settlementMethod)s, %(meterReadingOccurrence)s, + %(firstConsumerPartyName)s, %(secondConsumerPartyName)s, + %(meterNumber)s, %(consumerStartDate)s, %(typeOfMP)s, + %(balanceSupplierName)s, %(postcode)s + ) + ON CONFLICT (metering_point_id) + DO UPDATE SET + street_code = EXCLUDED.street_code, + street_name = EXCLUDED.street_name, + building_number = EXCLUDED.building_number, + updated_at = CURRENT_TIMESTAMP; + """ + + cursor.execute(upsert_sql, metering_point_data) + self.connection.commit() + self.logger.info( + f"Metering point {metering_point_data['meteringPointId']} stored" + ) + return True + + except Exception as e: + self.logger.error(f"Error storing metering point: {e}") + self.connection.rollback() + return False + + def store_consumption_readings( + self, metering_point_id: str, consumption_data: list[dict[str, Any]] + ) -> bool: + """ + Store consumption readings with conflict handling. + + Args: + metering_point_id: The metering point identifier + consumption_data: List of consumption reading dictionaries + + Returns: + True if successful, False otherwise + """ + if not self.connection: + self.logger.error("No database connection") + return False + + try: + with self.connection.cursor() as cursor: + insert_sql = """ + INSERT INTO consumption_readings ( + metering_point_id, timestamp, consumption_kwh, quality, period_resolution + ) VALUES ( + %s, %s, %s, %s, %s + ) + ON CONFLICT (metering_point_id, timestamp) + DO UPDATE SET + consumption_kwh = EXCLUDED.consumption_kwh, + quality = EXCLUDED.quality; + """ + + readings_data = [] + for reading in consumption_data: + readings_data.append( + ( + metering_point_id, + reading["timestamp"], + reading["consumption_kwh"], + reading["quality"], + reading.get("period_resolution", "PT1H"), + ) + ) + + cursor.executemany(insert_sql, readings_data) + self.connection.commit() + self.logger.info(f"Stored {len(readings_data)} consumption readings") + return True + + except Exception as e: + self.logger.error(f"Error storing consumption readings: {e}") + self.connection.rollback() + return False + + def get_latest_reading_timestamp(self, metering_point_id: str) -> datetime | None: + """ + Get the timestamp of the latest reading for a metering point. + + Args: + metering_point_id: The metering point identifier + + Returns: + Latest timestamp or None if no readings exist + """ + if not self.connection: + self.logger.error("No database connection") + return None + + try: + with self.connection.cursor() as cursor: + cursor.execute( + """ + SELECT MAX(timestamp) + FROM consumption_readings + WHERE metering_point_id = %s + """, + (metering_point_id,), + ) + result = cursor.fetchone() + return result[0] if result[0] else None + + except Exception as e: + self.logger.error(f"Error getting latest reading timestamp: {e}") + return None + + def close(self) -> None: + """Close database connection.""" + if self.connection: + self.connection.close() + self.logger.info("Database connection closed") + + @classmethod + def from_env(cls) -> "DatabaseStorage": + """ + Create DatabaseStorage from environment variables. + + Expected environment variables: + - DB_HOST + - DB_NAME + - DB_USER + - DB_PASSWORD + - DB_PORT (optional, defaults to 5432) + + Returns: + DatabaseStorage instance + """ + import os + + db_config = { + "host": os.getenv("DB_HOST", "localhost"), + "database": os.getenv("DB_NAME"), + "user": os.getenv("DB_USER"), + "password": os.getenv("DB_PASSWORD"), + "port": int(os.getenv("DB_PORT", 5432)), + } + + # Check for required environment variables + required_vars = ["DB_NAME", "DB_USER", "DB_PASSWORD"] + missing_vars = [var for var in required_vars if not os.getenv(var)] + + if missing_vars: + raise ValueError(f"Missing required environment variables: {missing_vars}") + + return cls(db_config) diff --git a/src/energy_consumption_ingester/eloverblik.py b/src/energy_consumption_ingester/eloverblik.py new file mode 100644 index 0000000..b64bac2 --- /dev/null +++ b/src/energy_consumption_ingester/eloverblik.py @@ -0,0 +1,220 @@ +"""Eloverblik API client for energy consumption data.""" + +import http.client +import json +import os +from datetime import datetime, timedelta +from typing import Any +from urllib.parse import urlencode + +from pydantic import AnyUrl + + +class EloverblikClient: + """Client for interacting with the Eloverblik API.""" + + def __init__( + self, refresh_token: str, server_url: str = "https://api.eloverblik.dk" + ): + """ + Initialize the Eloverblik client. + + Args: + refresh_token: The refresh token for authentication + server_url: The base URL of the Eloverblik API + """ + self.refresh_token = refresh_token + self.server_url = AnyUrl(server_url) + self.access_token: str | None = None + + # Ensure host is available + if not self.server_url.host: + raise ValueError(f"Invalid server URL: {server_url}") + self.host = self.server_url.host + + def get_access_token(self) -> bool: + """ + Exchange refresh token for access token. + + Returns: + True if successful, False otherwise + """ + conn = http.client.HTTPSConnection(self.host) + + headers = { + "Authorization": f"Bearer {self.refresh_token}", + "Content-Type": "application/json", + } + + try: + conn.request("GET", "/customerapi/api/token", headers=headers) + res = conn.getresponse() + + if res.status == 200: + token_response = res.read().decode("utf-8") + token_data = json.loads(token_response) + self.access_token = token_data["result"] + return True + else: + error_data = res.read().decode("utf-8") + print(f"Error getting access token: {error_data}") + return False + + except Exception as e: + print(f"Exception getting access token: {e}") + return False + finally: + conn.close() + + def get_metering_points( + self, include_all: bool = True + ) -> list[dict[str, Any]] | None: + """ + Get all metering points for the authenticated user. + + Args: + include_all: Whether to include all metering points + + Returns: + List of metering point data or None if error + """ + if not self.access_token: + if not self.get_access_token(): + return None + + conn = http.client.HTTPSConnection(self.host) + + headers = { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json", + "api-version": "1.0", + } + + params = {"includeAll": include_all} + + try: + conn.request( + "GET", + "/customerapi/api/meteringpoints/meteringpoints?" + urlencode(params), + headers=headers, + ) + res = conn.getresponse() + + if res.status == 200: + data = res.read().decode("utf-8") + metering_data = json.loads(data) + result = metering_data.get("result", []) + return result if isinstance(result, list) else [] + else: + error_data = res.read().decode("utf-8") + print(f"Error getting metering points: {error_data}") + return None + + except Exception as e: + print(f"Exception getting metering points: {e}") + return None + finally: + conn.close() + + def get_consumption_data( + self, + metering_point_id: str, + date_from: str, + date_to: str, + aggregation: str = "Hour", + ) -> dict[str, Any] | None: + """ + Get consumption data for a specific metering point. + + Args: + metering_point_id: The metering point identifier + date_from: Start date in YYYY-MM-DD format + date_to: End date in YYYY-MM-DD format + aggregation: Data aggregation level (Hour, Day, Month) + + Returns: + Consumption data or None if error + """ + if not self.access_token: + if not self.get_access_token(): + return None + + conn = http.client.HTTPSConnection(self.host) + + headers = { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json", + "api-version": "1.0", + } + + request_body = {"meteringPoints": {"meteringPoint": [metering_point_id]}} + + try: + conn.request( + "POST", + f"/customerapi/api/meterdata/gettimeseries/{date_from}/{date_to}/{aggregation}", + body=json.dumps(request_body), + headers=headers, + ) + res = conn.getresponse() + + if res.status == 200: + data = res.read().decode("utf-8") + result = json.loads(data) + return result if isinstance(result, dict) else {} + else: + error_data = res.read().decode("utf-8") + print(f"Error getting consumption data: {error_data}") + return None + + except Exception as e: + print(f"Exception getting consumption data: {e}") + return None + finally: + conn.close() + + def get_consumption_data_last_days( + self, metering_point_id: str, days: int = 7, aggregation: str = "Hour" + ) -> dict[str, Any] | None: + """ + Get consumption data for the last N days. + + Args: + metering_point_id: The metering point identifier + days: Number of days to look back + aggregation: Data aggregation level (Hour, Day, Month) + + Returns: + Consumption data or None if error + """ + end_date = datetime.now() + start_date = end_date - timedelta(days=days) + + date_from = start_date.strftime("%Y-%m-%d") + date_to = end_date.strftime("%Y-%m-%d") + + return self.get_consumption_data( + metering_point_id, date_from, date_to, aggregation + ) + + @classmethod + def from_env( + cls, token_env_var: str = "ELOVERBLIK_API_TOKEN" + ) -> "EloverblikClient": + """ + Create client from environment variable. + + Args: + token_env_var: Name of environment variable containing the refresh token + + Returns: + EloverblikClient instance + + Raises: + ValueError: If token environment variable is not set + """ + token = os.getenv(token_env_var) + if not token: + raise ValueError(f"Environment variable {token_env_var} is not set") + + return cls(refresh_token=token) diff --git a/src/energy_consumption_ingester/scheduler.py b/src/energy_consumption_ingester/scheduler.py new file mode 100644 index 0000000..68bb93d --- /dev/null +++ b/src/energy_consumption_ingester/scheduler.py @@ -0,0 +1,235 @@ +"""Scheduled synchronization functionality for CI/CD pipelines.""" + +import logging +from datetime import datetime, timedelta + +from .eloverblik import EloverblikClient +from .database import DatabaseStorage +from .utils import transform_metering_point_data, transform_consumption_data + + +class ScheduledSync: + """Handles scheduled synchronization of energy consumption data.""" + + def __init__(self, client: EloverblikClient, db: DatabaseStorage): + """ + Initialize scheduled sync. + + Args: + client: Eloverblik API client + db: Database storage instance + """ + self.client = client + self.db = db + self.logger = logging.getLogger(__name__) + + def sync_incremental(self, hours_back: int = 25) -> bool: + """ + Perform incremental sync - only sync recent data. + + Args: + hours_back: Number of hours to look back (default 25 to handle + timezone issues and ensure no gaps) + + Returns: + True if successful, False otherwise + """ + try: + self.logger.info(f"Starting incremental sync for last {hours_back} hours") + + # Get metering points + metering_points = self.client.get_metering_points() + if not metering_points: + self.logger.error("No metering points found") + return False + + success_count = 0 + total_readings = 0 + + for mp_data in metering_points: + metering_point_id = mp_data["meteringPointId"] + + try: + # Store/update metering point data + transformed_mp = transform_metering_point_data(mp_data) + self.db.store_metering_point(transformed_mp) + + # Get latest reading timestamp from database + latest_timestamp = self.db.get_latest_reading_timestamp( + metering_point_id + ) + + # Calculate date range for sync + end_date = datetime.now() + + if latest_timestamp: + # Sync from last reading with some overlap + start_date = latest_timestamp - timedelta(hours=1) + self.logger.info( + f"Syncing {metering_point_id} from {start_date} (last reading: {latest_timestamp})" + ) + else: + # No previous data, sync last N hours + start_date = end_date - timedelta(hours=hours_back) + self.logger.info( + f"No previous data for {metering_point_id}, syncing last {hours_back} hours" + ) + + # Format dates for API + date_from = start_date.strftime("%Y-%m-%d") + date_to = end_date.strftime("%Y-%m-%d") + + # Get consumption data + consumption_data = self.client.get_consumption_data( + metering_point_id, date_from, date_to + ) + + if consumption_data: + # Transform and store + _, readings = transform_consumption_data(consumption_data) + + if readings: + if self.db.store_consumption_readings( + metering_point_id, readings + ): + total_readings += len(readings) + self.logger.info( + f"Stored {len(readings)} readings for {metering_point_id}" + ) + else: + self.logger.error( + f"Failed to store readings for {metering_point_id}" + ) + continue + + success_count += 1 + + except Exception as e: + self.logger.error(f"Error processing {metering_point_id}: {e}") + continue + + self.logger.info( + f"Incremental sync completed: {success_count}/{len(metering_points)} " + f"metering points, {total_readings} total readings" + ) + + return success_count > 0 + + except Exception as e: + self.logger.error(f"Incremental sync failed: {e}") + return False + + def sync_full_backfill(self, days: int = 30) -> bool: + """ + Perform full backfill sync - sync large amount of historical data. + + Args: + days: Number of days to backfill + + Returns: + True if successful, False otherwise + """ + try: + self.logger.info(f"Starting full backfill sync for last {days} days") + + metering_points = self.client.get_metering_points() + if not metering_points: + self.logger.error("No metering points found") + return False + + success_count = 0 + total_readings = 0 + + for mp_data in metering_points: + metering_point_id = mp_data["meteringPointId"] + + try: + # Store metering point data + transformed_mp = transform_metering_point_data(mp_data) + self.db.store_metering_point(transformed_mp) + + # Get consumption data for the full period + consumption_data = self.client.get_consumption_data_last_days( + metering_point_id, days + ) + + if consumption_data: + _, readings = transform_consumption_data(consumption_data) + + if readings: + if self.db.store_consumption_readings( + metering_point_id, readings + ): + total_readings += len(readings) + self.logger.info( + f"Stored {len(readings)} readings for {metering_point_id}" + ) + else: + self.logger.error( + f"Failed to store readings for {metering_point_id}" + ) + continue + + success_count += 1 + + except Exception as e: + self.logger.error(f"Error processing {metering_point_id}: {e}") + continue + + self.logger.info( + f"Full backfill completed: {success_count}/{len(metering_points)} " + f"metering points, {total_readings} total readings" + ) + + return success_count > 0 + + except Exception as e: + self.logger.error(f"Full backfill sync failed: {e}") + return False + + def health_check(self) -> bool: + """ + Perform a health check of the system. + + Returns: + True if all systems are healthy, False otherwise + """ + try: + self.logger.info("Starting health check") + + # Check API connection + if not self.client.get_access_token(): + self.logger.error("Failed to get API access token") + return False + + # Check database connection + if not self.db.connection: + self.logger.error("No database connection") + return False + + # Try to get metering points + metering_points = self.client.get_metering_points() + if not metering_points: + self.logger.error("No metering points found") + return False + + self.logger.info( + f"Health check passed: {len(metering_points)} metering points available" + ) + return True + + except Exception as e: + self.logger.error(f"Health check failed: {e}") + return False + + @classmethod + def from_env(cls) -> "ScheduledSync": + """ + Create ScheduledSync from environment variables. + + Returns: + ScheduledSync instance + """ + client = EloverblikClient.from_env() + db = DatabaseStorage.from_env() + return cls(client, db) diff --git a/src/energy_consumption_ingester/utils.py b/src/energy_consumption_ingester/utils.py new file mode 100644 index 0000000..d914f12 --- /dev/null +++ b/src/energy_consumption_ingester/utils.py @@ -0,0 +1,124 @@ +"""Utility functions for data transformation and processing.""" + +from datetime import datetime, timedelta +from typing import Any + + +def transform_metering_point_data(api_data: dict[str, Any]) -> dict[str, Any]: + """ + Transform API metering point data to database format. + + Args: + api_data: Raw metering point data from API + + Returns: + Dictionary formatted for database storage + """ + # Parse consumer start date if present + consumer_start_date = None + if api_data.get("consumerStartDate"): + try: + consumer_start_date = datetime.fromisoformat( + api_data["consumerStartDate"].replace("Z", "+00:00") + ) + except (ValueError, AttributeError): + pass + + return { + "meteringPointId": api_data.get("meteringPointId"), + "streetCode": api_data.get("streetCode"), + "streetName": api_data.get("streetName"), + "buildingNumber": api_data.get("buildingNumber"), + "floorId": api_data.get("floorId"), + "roomId": api_data.get("roomId"), + "citySubDivisionName": api_data.get("citySubDivisionName"), + "municipalityCode": api_data.get("municipalityCode"), + "locationDescription": api_data.get("locationDescription"), + "settlementMethod": api_data.get("settlementMethod"), + "meterReadingOccurrence": api_data.get("meterReadingOccurrence"), + "firstConsumerPartyName": api_data.get("firstConsumerPartyName"), + "secondConsumerPartyName": api_data.get("secondConsumerPartyName"), + "meterNumber": api_data.get("meterNumber"), + "consumerStartDate": consumer_start_date, + "typeOfMP": api_data.get("typeOfMP"), + "balanceSupplierName": api_data.get("balanceSupplierName"), + "postcode": api_data.get("postcode"), + } + + +def transform_consumption_data( + consumption_json: dict[str, Any], +) -> tuple[str | None, list[dict[str, Any]]]: + """ + Transform API consumption data to database format. + + Args: + consumption_json: Raw consumption data from API + + Returns: + Tuple of (metering_point_id, list_of_consumption_readings) + """ + if not consumption_json or "result" not in consumption_json: + return None, [] + + try: + # Extract the time series data + market_document = consumption_json["result"][0]["MyEnergyData_MarketDocument"] + time_series = market_document["TimeSeries"][0] + metering_point_id = time_series["mRID"] + periods = time_series["Period"] + + consumption_readings = [] + + for period in periods: + period_start = datetime.fromisoformat( + period["timeInterval"]["start"].replace("Z", "+00:00") + ) + resolution = period["resolution"] + + if "Point" in period: + for point in period["Point"]: + # Calculate the actual timestamp for this point + position = int(point["position"]) + # Position is 1-based, so subtract 1 to get hours offset + hours_offset = position - 1 + + point_timestamp = period_start + timedelta(hours=hours_offset) + + reading = { + "timestamp": point_timestamp, + "consumption_kwh": float(point["out_Quantity.quantity"]), + "quality": point["out_Quantity.quality"], + "period_resolution": resolution, + } + consumption_readings.append(reading) + + return metering_point_id, consumption_readings + + except Exception as e: + print(f"Error transforming consumption data: {e}") + return None, [] + + +def calculate_consumption_stats(readings: list[dict[str, Any]]) -> dict[str, float]: + """ + Calculate consumption statistics from readings. + + Args: + readings: List of consumption reading dictionaries + + Returns: + Dictionary with consumption statistics + """ + if not readings: + return {} + + consumptions = [r["consumption_kwh"] for r in readings] + + return { + "total_kwh": sum(consumptions), + "average_kwh": sum(consumptions) / len(consumptions), + "min_kwh": min(consumptions), + "max_kwh": max(consumptions), + "count": len(consumptions), + } diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8b95f4d --- /dev/null +++ b/uv.lock @@ -0,0 +1,1029 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "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 = "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 = "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/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 = "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/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 = "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 = "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 = "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 = "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/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 = "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 = "energy-consumption-ingester" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "psycopg2-binary" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "flake8" }, + { name = "ipykernel" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "pyupgrade" }, + { name = "ruff" }, + { name = "types-psycopg2" }, +] + +[package.metadata] +requires-dist = [ + { name = "psycopg2-binary", specifier = ">=2.9.0" }, + { name = "pydantic", specifier = ">=2.12.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=23.0.0" }, + { name = "flake8", specifier = ">=6.0.0" }, + { name = "ipykernel", specifier = ">=7.0.1" }, + { name = "mypy", specifier = ">=1.0.0" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pytest", specifier = ">=7.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "pyupgrade", specifier = ">=3.21.0" }, + { name = "ruff", specifier = ">=0.14.2" }, + { name = "types-psycopg2", specifier = ">=2.9.21.20251012" }, +] + +[[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 = "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 = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[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 = "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 = "ipykernel" +version = "7.0.1" +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/a8/4c/9f0024c8457286c6bfd5405a15d650ec5ea36f420ef9bbc58b301f66cfc5/ipykernel-7.0.1.tar.gz", hash = "sha256:2d3fd7cdef22071c2abbad78f142b743228c5d59cd470d034871ae0ac359533c", size = 171460, upload-time = "2025-10-14T16:17:07.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/f7/761037905ffdec673533bfa43af8d4c31c859c778dfc3bbb71899875ec18/ipykernel-7.0.1-py3-none-any.whl", hash = "sha256:87182a8305e28954b6721087dec45b171712610111d494c17bb607befa1c4000", size = 118157, upload-time = "2025-10-14T16:17:05.606Z" }, +] + +[[package]] +name = "ipython" +version = "9.6.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/2a/34/29b18c62e39ee2f7a6a3bba7efd952729d8aadd45ca17efc34453b717665/ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731", size = 4396932, upload-time = "2025-09-29T10:55:53.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl", hash = "sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196", size = 616170, upload-time = "2025-09-29T10:55:47.676Z" }, +] + +[[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 = "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 = "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 = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[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/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 = "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 = "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 = "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.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 = "4.3.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/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[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 = "psutil" +version = "7.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/ec/7b8e6b9b1d22708138630ef34c53ab2b61032c04f16adfdbb96791c8c70c/psutil-7.1.2.tar.gz", hash = "sha256:aa225cdde1335ff9684708ee8c72650f6598d5ed2114b9a7c5802030b1785018", size = 487424, upload-time = "2025-10-25T10:46:34.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/d9/b56cc9f883140ac10021a8c9b0f4e16eed1ba675c22513cdcbce3ba64014/psutil-7.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0cc5c6889b9871f231ed5455a9a02149e388fffcb30b607fb7a8896a6d95f22e", size = 238575, upload-time = "2025-10-25T10:46:38.728Z" }, + { url = "https://files.pythonhosted.org/packages/36/eb/28d22de383888deb252c818622196e709da98816e296ef95afda33f1c0a2/psutil-7.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8e9e77a977208d84aa363a4a12e0f72189d58bbf4e46b49aae29a2c6e93ef206", size = 239297, upload-time = "2025-10-25T10:46:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/220039e2f28cc129626e54d63892ab05c0d56a29818bfe7268dcb5008932/psutil-7.1.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d9623a5e4164d2220ecceb071f4b333b3c78866141e8887c072129185f41278", size = 280420, upload-time = "2025-10-25T10:46:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/286f0e1c167445b2ef4a6cbdfc8c59fdb45a5a493788950cf8467201dc73/psutil-7.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:364b1c10fe4ed59c89ec49e5f1a70da353b27986fa8233b4b999df4742a5ee2f", size = 283049, upload-time = "2025-10-25T10:46:47.095Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cc/7eb93260794a42e39b976f3a4dde89725800b9f573b014fac142002a5c98/psutil-7.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f101ef84de7e05d41310e3ccbdd65a6dd1d9eed85e8aaf0758405d022308e204", size = 248713, upload-time = "2025-10-25T10:46:49.573Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/0681a92b53366e01f0a099f5237d0c8a2f79d322ac589cccde5e30c8a4e2/psutil-7.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:20c00824048a95de67f00afedc7b08b282aa08638585b0206a9fb51f28f1a165", size = 244644, upload-time = "2025-10-25T10:46:51.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/f1c5c746b4ed5320952acd3002d3962fe36f30524c00ea79fdf954cc6779/psutil-7.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e09cfe92aa8e22b1ec5e2d394820cf86c5dff6367ac3242366485dfa874d43bc", size = 238640, upload-time = "2025-10-25T10:46:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/32/ee/fd26216a735395cc25c3899634e34aeb41fb1f3dbb44acc67d9e594be562/psutil-7.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa6342cf859c48b19df3e4aa170e4cfb64aadc50b11e06bb569c6c777b089c9e", size = 239303, upload-time = "2025-10-25T10:46:56.932Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cd/7d96eaec4ef7742b845a9ce2759a2769ecce4ab7a99133da24abacbc9e41/psutil-7.1.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:625977443498ee7d6c1e63e93bacca893fd759a66c5f635d05e05811d23fb5ee", size = 281717, upload-time = "2025-10-25T10:46:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1a/7f0b84bdb067d35fe7fade5fff888408688caf989806ce2d6dae08c72dd5/psutil-7.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a24bcd7b7f2918d934af0fb91859f621b873d6aa81267575e3655cd387572a7", size = 284575, upload-time = "2025-10-25T10:47:00.944Z" }, + { url = "https://files.pythonhosted.org/packages/de/05/7820ef8f7b275268917e0c750eada5834581206d9024ca88edce93c4b762/psutil-7.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:329f05610da6380982e6078b9d0881d9ab1e9a7eb7c02d833bfb7340aa634e31", size = 249491, upload-time = "2025-10-25T10:47:03.174Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/58de399c7cb58489f08498459ff096cd76b3f1ddc4f224ec2c5ef729c7d0/psutil-7.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7b04c29e3c0c888e83ed4762b70f31e65c42673ea956cefa8ced0e31e185f582", size = 244880, upload-time = "2025-10-25T10:47:05.228Z" }, + { url = "https://files.pythonhosted.org/packages/ae/89/b9f8d47ddbc52d7301fc868e8224e5f44ed3c7f55e6d0f54ecaf5dd9ff5e/psutil-7.1.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c9ba5c19f2d46203ee8c152c7b01df6eec87d883cfd8ee1af2ef2727f6b0f814", size = 237244, upload-time = "2025-10-25T10:47:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7a/8628c2f6b240680a67d73d8742bb9ff39b1820a693740e43096d5dcb01e5/psutil-7.1.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a486030d2fe81bec023f703d3d155f4823a10a47c36784c84f1cc7f8d39bedb", size = 238101, upload-time = "2025-10-25T10:47:09.523Z" }, + { url = "https://files.pythonhosted.org/packages/30/28/5e27f4d5a0e347f8e3cc16cd7d35533dbce086c95807f1f0e9cd77e26c10/psutil-7.1.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3efd8fc791492e7808a51cb2b94889db7578bfaea22df931424f874468e389e3", size = 258675, upload-time = "2025-10-25T10:47:11.082Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5c/79cf60c9acf36d087f0db0f82066fca4a780e97e5b3a2e4c38209c03d170/psutil-7.1.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2aeb9b64f481b8eabfc633bd39e0016d4d8bbcd590d984af764d80bf0851b8a", size = 260203, upload-time = "2025-10-25T10:47:13.226Z" }, + { url = "https://files.pythonhosted.org/packages/f7/03/0a464404c51685dcb9329fdd660b1721e076ccd7b3d97dee066bcc9ffb15/psutil-7.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:8e17852114c4e7996fe9da4745c2bdef001ebbf2f260dec406290e66628bdb91", size = 246714, upload-time = "2025-10-25T10:47:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/6a/32/97ca2090f2f1b45b01b6aa7ae161cfe50671de097311975ca6eea3e7aabc/psutil-7.1.2-cp37-abi3-win_arm64.whl", hash = "sha256:3e988455e61c240cc879cb62a008c2699231bf3e3d061d7fce4234463fd2abb4", size = 243742, upload-time = "2025-10-25T10:47:17.302Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[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 = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[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 = "pydantic" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[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 = "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 = "pytokens" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/c2/dbadcdddb412a267585459142bfd7cc241e6276db69339353ae6e241ab2b/pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43", size = 15368, upload-time = "2025-10-15T08:02:42.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/5a/c269ea6b348b6f2c32686635df89f32dbe05df1088dd4579302a6f8f99af/pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8", size = 12038, upload-time = "2025-10-15T08:02:41.694Z" }, +] + +[[package]] +name = "pyupgrade" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tokenize-rt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/11/b08f5e4d50575c944e4ea0a86f070a1ba2c0d5a4dc42fac571a605ada78d/pyupgrade-3.21.0.tar.gz", hash = "sha256:3e63a882ec1d16f5621736d938952df3cdc2446501fb049e711415cb8d273960", size = 45210, upload-time = "2025-10-09T19:32:26.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/4a/d18f2d6b2f9b90ec7e10843ea10e4cde0dc36091782443ee923e29d653bb/pyupgrade-3.21.0-py2.py3-none-any.whl", hash = "sha256:3a200cdc537980f371b4867f1d662e37166a7869d7713f309c1c3241c310c754", size = 62638, upload-time = "2025-10-09T19:32:25.086Z" }, +] + +[[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/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 = "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/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" }, +] + +[[package]] +name = "ruff" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" }, + { url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" }, + { url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" }, + { url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" }, + { url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, +] + +[[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 = "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 = "tokenize-rt" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476, upload-time = "2025-05-23T23:48:00.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004, upload-time = "2025-05-23T23:47:58.812Z" }, +] + +[[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 = "types-psycopg2" +version = "2.9.21.20251012" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/b3/2d09eaf35a084cffd329c584970a3fa07101ca465c13cad1576d7c392587/types_psycopg2-2.9.21.20251012.tar.gz", hash = "sha256:4cdafd38927da0cfde49804f39ab85afd9c6e9c492800e42f1f0c1a1b0312935", size = 26710, upload-time = "2025-10-12T02:55:39.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/0c/05feaf8cb51159f2c0af04b871dab7e98a2f83a3622f5f216331d2dd924c/types_psycopg2-2.9.21.20251012-py3-none-any.whl", hash = "sha256:712bad5c423fe979e357edbf40a07ca40ef775d74043de72bd4544ca328cc57e", size = 24883, upload-time = "2025-10-12T02:55:38.439Z" }, +] + +[[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 = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[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" }, +]