diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b109607..d862309 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,40 +6,38 @@ labels: ['bug', 'triage'] assignees: '' --- -## ๐Ÿ› Bug Description +## Description -## ๐Ÿ”„ Steps to Reproduce +## Steps to reproduce 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error -## โœ… Expected Behavior +## Expected behavior -## โŒ Actual Behavior +## Actual behavior -## ๐Ÿ“ธ Screenshots - - -## ๐ŸŒ Environment +## Environment - OS: [e.g. Ubuntu 22.04, macOS 14.0, Windows 11] - Python Version: [e.g. 3.13.0] - smppai Version: [e.g. 0.1.0] - Installation Method: [e.g. pip, uv, poetry] -## ๐Ÿ“ Additional Context +## Additional context + -## ๐Ÿ” Error Logs +## Error logs ``` Paste error logs here ``` -## ๐Ÿ’ก Possible Solution +## Possible solution diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md index 3d1755f..0d8d745 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -6,10 +6,10 @@ labels: ['documentation', 'triage'] assignees: '' --- -## ๐Ÿ“š Documentation Issue +## Description -## ๐Ÿ“ Location +## Location - [ ] README.md - [ ] API Documentation @@ -18,21 +18,11 @@ assignees: '' - [ ] Docstrings - [ ] Other: [specify] -## ๐Ÿ”— Link/File +## Link -## โŒ Current Content - - -## โœ… Suggested Improvement +## Suggested improvement -## ๐ŸŽฏ Impact - -- [ ] New users -- [ ] Experienced users -- [ ] Contributors -- [ ] API consumers - -## ๐Ÿ“ Additional Context +## Additional context diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 5743671..aae8064 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,41 +6,27 @@ labels: ['enhancement', 'triage'] assignees: '' --- -## ๐Ÿš€ Feature Description +## Description -## ๐ŸŽฏ Problem Statement +## Problem statement I'm always frustrated when [...] -## ๐Ÿ’ก Proposed Solution +## Proposed solution - -## ๐Ÿ”„ Alternative Solutions - - -## ๐Ÿ“Š Use Case - -## ๐ŸŒ Impact - -- [ ] SMPP Client users -- [ ] SMPP Server users -- [ ] Protocol developers -- [ ] Performance improvements -- [ ] Ease of use improvements - -## ๐ŸŽจ Implementation Ideas -## ๐Ÿ“š References +## References -## ๐Ÿ”ง Breaking Changes +## Breaking changes - [ ] Yes, this would require breaking changes - [ ] No, this is backward compatible - [ ] Unsure -## ๐Ÿ“ Additional Context +## Additional context + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ddf41e..ee95af1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,15 @@ on: branches: [main] env: - PYTHON_VERSION: "3.13" + PRIMARY_PYTHON_VERSION: '3.13' + UV_CACHE_DIR: ~/.cache/uv + PYTHONUNBUFFERED: "1" + FORCE_COLOR: "1" jobs: lint: runs-on: ubuntu-latest - name: Lint & Format Check + name: Lint & Format check steps: - name: Checkout repository uses: actions/checkout@v4 @@ -21,9 +24,17 @@ jobs: uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "uv.lock" - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} + # Cache pre-commit hooks (only needed for lint job) + - name: Cache pre-commit + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Set up Python ${{ env.PRIMARY_PYTHON_VERSION }} + run: uv python install ${{ env.PRIMARY_PYTHON_VERSION }} - name: Install dependencies run: | @@ -38,7 +49,7 @@ jobs: type-check: runs-on: ubuntu-latest - name: Type Check + name: Type check steps: - name: Checkout repository uses: actions/checkout@v4 @@ -47,9 +58,10 @@ jobs: uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "uv.lock" - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} + - name: Set up Python ${{ env.PRIMARY_PYTHON_VERSION }} + run: uv python install ${{ env.PRIMARY_PYTHON_VERSION }} - name: Install dependencies run: | @@ -72,9 +84,10 @@ jobs: uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "uv.lock" - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} + - name: Set up Python ${{ env.PRIMARY_PYTHON_VERSION }} + run: uv python install ${{ env.PRIMARY_PYTHON_VERSION }} - name: Install dependencies run: | @@ -88,15 +101,48 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: bandit-report + name: bandit-report-${{ env.PRIMARY_PYTHON_VERSION }} path: bandit-report.json - test: - runs-on: ubuntu-latest + unit-tests: + runs-on: ${{ matrix.os }} + needs: [lint, type-check] + timeout-minutes: 30 # Prevent hanging jobs strategy: + fail-fast: false # Don't cancel other jobs on failure + # matrix: + # python-version: ["3.12", "3.13"] + # os: ["ubuntu-latest"] matrix: - python-version: ["3.13"] - name: Test Python ${{ matrix.python-version }} + include: + # --- Lightweight combinations + - python-version: "3.10" + os: "ubuntu-latest" + # test-type: "minimal" + coverage: false + - python-version: "3.11" + os: "ubuntu-latest" + # test-type: "minimal" + coverage: false + - python-version: "3.12" + os: "ubuntu-latest" + # test-type: "standard" + coverage: false + # --- Full test on latest + - python-version: "3.13" + os: "ubuntu-latest" + # test-type: "full" + coverage: true + # --- Cross-platform on latest Python only + - python-version: "3.13" + os: "windows-latest" + # test-type: "standard" + coverage: false + - python-version: "3.13" + os: "macos-latest" + # test-type: "standard" + coverage: false + name: Unit tests Python ${{ matrix.python-version }} on ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -105,8 +151,9 @@ jobs: uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "uv.lock" - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} run: uv python install ${{ matrix.python-version }} - name: Install dependencies @@ -114,53 +161,51 @@ jobs: uv sync --all-extras --dev uv pip install -e . + # - name: Run tests with coverage + # env: + # PYTHONPATH: ${{ github.workspace }}/src + # run: | + # uv run python -m pytest tests/ \ + # --cov=src/smpp \ + # --cov-report=html \ + # --cov-report=xml \ + # --cov-report=term-missing \ + # --junitxml=pytest-report.xml + # - name: Upload coverage + # uses: actions/upload-artifact@v4 + # with: + # name: coverage-${{ matrix.python-version }}-${{ matrix.os }} + # path: | + # coverage.xml + # htmlcov/ - name: Run tests with coverage env: PYTHONPATH: ${{ github.workspace }}/src + shell: bash run: | - uv run python -m pytest tests/ \ - --cov=src/smpp \ - --cov-report=html \ - --cov-report=term-missing \ - --junitxml=pytest-report.xml - - build: - runs-on: ubuntu-latest - name: Build Package - needs: [lint, type-check, test] - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: | - uv sync --all-extras --dev - uv pip install -e . - - - name: Build package - run: uv build - - - name: Check package - run: uv run twine check dist/* - - - name: Upload build artifacts + if [[ "${{ matrix.coverage }}" == "true" ]]; then + uv run python -m pytest tests/ \ + --cov=src/smpp \ + --cov-report=xml \ + --cov-report=html \ + --junitxml=pytest-report.xml + else + uv run python -m pytest tests/ \ + --junitxml=pytest-report.xml + fi + - name: Upload coverage + if: matrix.coverage == true uses: actions/upload-artifact@v4 with: - name: dist - path: dist/ + name: coverage-${{ matrix.python-version }}-${{ matrix.os }} + path: | + coverage.xml + htmlcov/ integration-test: runs-on: ubuntu-latest - name: Integration Tests needs: [lint, type-check] + name: Integration Tests on Python 3.13 and ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 @@ -169,9 +214,10 @@ jobs: uses: astral-sh/setup-uv@v6 with: enable-cache: true + cache-dependency-glob: "uv.lock" - - name: Set up Python - run: uv python install ${{ env.PYTHON_VERSION }} + - name: Set up Python ${{ env.PRIMARY_PYTHON_VERSION }} + run: uv python install ${{ env.PRIMARY_PYTHON_VERSION }} - name: Install dependencies run: | @@ -194,29 +240,120 @@ jobs: env: PYTHONPATH: ${{ github.workspace }}/src run: | - uv run python -c " + uv run python -c "$(cat << 'EOF' import smpp from smpp.client import SMPPClient from smpp.server import SMPPServer from smpp.protocol import BindTransmitter, SubmitSm - print('โœ“ All imports successful') - " + print('All imports successful') + EOF + )" + + coverage-report: + runs-on: ubuntu-latest + needs: [unit-tests] + if: always() && needs.unit-tests.result == 'success' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download coverage from Python 3.13 + uses: actions/download-artifact@v4 + with: + name: coverage-3.13-ubuntu-latest + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: codcod/smppai + file: ./coverage.xml + fail_ci_if_error: false + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + with: + name: final-coverage-report + path: | + coverage.xml + htmlcov/ + + build: + runs-on: ubuntu-latest + name: Build package + needs: [lint, type-check] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python ${{ env.PRIMARY_PYTHON_VERSION }} + run: uv python install ${{ env.PRIMARY_PYTHON_VERSION }} + + - name: Install dependencies + run: | + uv sync --all-extras --dev + uv pip install -e . + + - name: Build package + run: uv build + + - name: Check package + run: uv run twine check dist/* + + - name: Test installation + run: | + pip install dist/*.whl + python -c "import smpp; print('Package installs correctly')" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-${{ env.PRIMARY_PYTHON_VERSION }} + path: dist/ all-checks: if: always() runs-on: ubuntu-latest - name: All Checks Status - needs: [lint, type-check, security, test, build, integration-test] + name: All checks status + needs: [lint, type-check, security, unit-tests, integration-test, build] + # steps: + # - name: Check all jobs status + # run: | + # if [[ "${{ needs.lint.result }}" != "success" || \ + # "${{ needs.type-check.result }}" != "success" || \ + # "${{ needs.test.result }}" != "success" || \ + # "${{ needs.build.result }}" != "success" || \ + # "${{ needs.integration-test.result }}" != "success" ]]; then + # echo "One or more checks failed" + # exit 1 + # else + # echo "All checks passed successfully" + # fi steps: - - name: Check all jobs status + - name: Summarize results + run: | + echo "## CI Results" >> $GITHUB_STEP_SUMMARY + echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Lint | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Type Check | ${{ needs.type-check.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Security | ${{ needs.security.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Tests | ${{ needs.unit-tests.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Integration | ${{ needs.integration-test.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY + + - name: Check required jobs run: | + # Fix: Use proper bash syntax for needs reference if [[ "${{ needs.lint.result }}" != "success" || \ "${{ needs.type-check.result }}" != "success" || \ - "${{ needs.test.result }}" != "success" || \ - "${{ needs.build.result }}" != "success" || \ - "${{ needs.integration-test.result }}" != "success" ]]; then - echo "โŒ One or more checks failed" + "${{ needs.unit-tests.result }}" != "success" ]]; then + echo "Required job failed" exit 1 - else - echo "โœ… All checks passed successfully" fi diff --git a/.github/workflows/validate-commits.yml b/.github/workflows/validate-commits.yml index 003b743..37b0519 100644 --- a/.github/workflows/validate-commits.yml +++ b/.github/workflows/validate-commits.yml @@ -19,4 +19,5 @@ jobs: with: configFile: '.commitlintrc.json' failOnWarnings: false + failOnErrors: false helpURL: 'https://github.com/nicos/smppai/blob/main/CONTRIBUTING.md#conventional-commits' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..aa9130c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-toml + - id: check-yaml + args: ["--allow-multiple-documents"] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.0 + hooks: + - id: ruff + - id: ruff-format + # - repo: https://github.com/jackdewinter/pymarkdown + # rev: v0.9.24 + # hooks: + # - id: pymarkdown + # exclude: "CHANGELOG.md" + # - repo: https://github.com/econchick/interrogate + # rev: 1.7.0 + # hooks: + # - id: interrogate + # pass_filenames: false # needed if excluding files with pyproject.toml or setup.cfg diff --git a/examples/client.py b/examples/client.py index 8a4d43f..16d3386 100644 --- a/examples/client.py +++ b/examples/client.py @@ -3,19 +3,20 @@ SMPP Client Example This example demonstrates how to use the SMPP client to connect to an SMSC, -send SMS messages, and handle delivery receipts. - -Updated for the new modular code structure with clean imports from the main smpp package. +send SMS messages, handle delivery receipts, and properly handle enhanced shutdown +notifications from the server. + +Shutdown features: +- Receives and responds to server shutdown notifications +- Graceful disconnection when server requests shutdown +- Proper handling of connection loss during shutdown +- Interactive commands for testing shutdown scenarios """ import asyncio import logging -import os -import sys from typing import Optional -# Add parent directory to path for imports -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from smpp import ( BindType, @@ -27,15 +28,24 @@ TonType, ) -# Configure logging +# Configure logging with enhanced format for shutdown monitoring logging.basicConfig( - level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + level=logging.INFO, + format='%(asctime)s [%(levelname)-8s] [%(name)s] %(message)s', + datefmt='%H:%M:%S', ) logger = logging.getLogger(__name__) class SMSClient: - """Example SMS client using SMPP""" + """ + Example SMS client. + + Features: + - Shutdown notification handling + - Graceful disconnection on server shutdown + - Interactive command support + """ def __init__(self, host: str, port: int, system_id: str, password: str): self.client = SMPPClient( @@ -43,11 +53,18 @@ def __init__(self, host: str, port: int, system_id: str, password: str): port=port, system_id=system_id, password=password, - system_type='SMS_CLIENT', + system_type='CLIENT', # SMPP system_type must be <= 13 characters enquire_link_interval=30.0, response_timeout=10.0, ) + # Shutdown handling with thread safety + self._shutdown_lock = asyncio.Lock() + self._shutdown_state = 'running' # running, shutting_down, stopped + self._shutdown_grace_period = 0 + self._received_shutdown_notification = False + self._received_shutdown_reminder = False + # Set up event handlers self.client.on_deliver_sm = self.handle_deliver_sm self.client.on_connection_lost = self.handle_connection_lost @@ -55,48 +72,167 @@ def __init__(self, host: str, port: int, system_id: str, password: str): self.client.on_unbind = self.handle_unbind def handle_deliver_sm(self, client: SMPPClient, pdu: DeliverSm) -> None: - """Handle incoming deliver_sm (delivery receipts or MO SMS)""" + """Handle incoming deliver_sm with enhanced shutdown notification detection.""" try: message = pdu.short_message.decode('utf-8', errors='ignore') # Check if this is a delivery receipt if pdu.esm_class & 0x04: # Delivery receipt - logger.info('Delivery receipt received:') - logger.info(f' From: {pdu.source_addr}') - logger.info(f' To: {pdu.destination_addr}') - logger.info(f' Receipt: {message}') - else: # Mobile Originated SMS - logger.info('MO SMS received:') - logger.info(f' From: {pdu.source_addr}') - logger.info(f' To: {pdu.destination_addr}') - logger.info(f' Message: {message}') + logger.info('๐Ÿ“ง Delivery receipt received:') + logger.info(f' From: {pdu.source_addr} โ†’ To: {pdu.destination_addr}') + logger.debug(f' Receipt: {message}') + + # Check for shutdown notifications from the server + elif self._is_shutdown_notification(pdu, message): + self._handle_shutdown_notification(pdu, message) + + else: # Mobile Originated SMS or server message + logger.info('๐Ÿ“ฅ Message received:') + logger.info(f' From: {pdu.source_addr} โ†’ To: {pdu.destination_addr}') + logger.info(f' Content: "{message}"') + + # Handle interactive responses if this is a response to our commands + if pdu.source_addr == 'SYSTEM': + logger.info('๐Ÿค– Server response received') except Exception as e: - logger.error(f'Error handling deliver_sm: {e}') + logger.error(f'โŒ Error handling deliver_sm: {e}') + + def _is_shutdown_notification(self, pdu: DeliverSm, message: str) -> bool: + """Check if this message is a server shutdown notification.""" + # Check for shutdown notification patterns + shutdown_indicators = [ + 'shutdown notification', + 'shutdown reminder', + 'server shutdown', + 'server shutting down', + 'final warning', + 'grace period', + ] + + message_lower = message.lower() + is_from_system = pdu.source_addr in ( + 'SYSTEM', + 'SMSC', + 'DEMO_SMSC', + ) # Fixed: Updated for SMPP-compliant system IDs + + return is_from_system and any( + indicator in message_lower for indicator in shutdown_indicators + ) + + def _handle_shutdown_notification(self, pdu: DeliverSm, message: str) -> None: + """Handle server shutdown notifications with appropriate responses.""" + message_lower = message.lower() + + if 'reminder' in message_lower or 'final warning' in message_lower: + if not self._received_shutdown_reminder: + logger.warning('๐Ÿ”” SHUTDOWN REMINDER received from server!') + logger.warning(f' Message: "{message}"') + logger.warning( + 'โš ๏ธ Server will force disconnect soon - preparing for graceful shutdown' + ) + self._received_shutdown_reminder = True + # Start graceful shutdown process + asyncio.create_task(self._initiate_graceful_shutdown(urgent=True)) + + elif not self._received_shutdown_notification: + logger.info('๐Ÿ›‘ SHUTDOWN NOTIFICATION received from server!') + logger.info(f' Message: "{message}"') + + # Extract grace period if mentioned + try: + if 'grace period:' in message_lower: + # Try to extract the grace period value + parts = message.split('Grace period:') + if len(parts) > 1: + grace_str = parts[1].split('s')[0].strip() + self._shutdown_grace_period = float(grace_str) + logger.info( + f'๐Ÿ“… Grace period: {self._shutdown_grace_period} seconds' + ) + except Exception: + pass # Continue even if grace period extraction fails + + self._received_shutdown_notification = True + logger.info( + 'โœ… Acknowledged shutdown notification - will disconnect gracefully' + ) + + # Start graceful shutdown process + asyncio.create_task(self._initiate_graceful_shutdown()) + + async def _initiate_graceful_shutdown( + self, urgent: bool = False, default_delay: float = 0.5 + ) -> None: + """Thread-safe graceful shutdown process.""" + async with self._shutdown_lock: + if self._shutdown_state != 'running': + return # Already shutting down or stopped + + self._shutdown_state = 'shutting_down' + + try: + if urgent: + logger.warning('๐Ÿšจ Urgent shutdown - disconnecting immediately') + delay = 0.1 # Minimal delay for urgent shutdown + else: + # Give some time for any pending operations, but respect server's grace period + if self._shutdown_grace_period > 0: + # Use a fraction of the server's grace period, but cap it reasonably + delay = min(max(0.1, self._shutdown_grace_period * 0.5), 2.0) + else: + delay = default_delay # Default to 0.5s instead of 3.0s for faster tests + logger.info(f'โฑ๏ธ Graceful shutdown in {delay:.1f} seconds...') + + await asyncio.sleep(delay) + + logger.info('๐Ÿ‘‹ Initiating graceful disconnect from server') + await self.disconnect() + + except asyncio.CancelledError: + raise # Re-raise cancellation + except Exception as e: + logger.error(f'โŒ Error during graceful shutdown: {e}') + finally: + async with self._shutdown_lock: + self._shutdown_state = 'stopped' def handle_connection_lost(self, client: SMPPClient, error: Exception) -> None: - """Handle connection lost event""" - logger.error(f'Connection lost: {error}') - # Could implement reconnection logic here + """Handle connection lost event with enhanced shutdown awareness.""" + if self._shutdown_state != 'running' or self._received_shutdown_notification: + logger.info('๐Ÿ”Œ Connection closed - server shutdown completed') + else: + logger.error(f'โŒ Unexpected connection lost: {error}') + logger.warning( + '๐Ÿ›‘ Server has disconnected unexpectedly - initiating graceful client shutdown' + ) + # When server disconnects unexpectedly, treat it as a shutdown request + asyncio.create_task(self._initiate_graceful_shutdown(urgent=True)) def handle_bind_success(self, client: SMPPClient, bind_type: BindType) -> None: - """Handle successful bind""" - logger.info(f'Successfully bound as {bind_type.value}') + """Handle successful bind with enhanced logging.""" + logger.info(f'๐Ÿ” Successfully bound as {bind_type.value}') + logger.info('โœ… Client ready to send/receive messages') + logger.info('๐Ÿ›‘ Will respond appropriately to server shutdown notifications') def handle_unbind(self, client: SMPPClient) -> None: - """Handle unbind event""" - logger.info('Client unbound from SMSC') + """Handle unbind event with enhanced logging.""" + if self._shutdown_state == 'shutting_down': + logger.info('๐Ÿ‘‹ Gracefully unbound from SMSC during shutdown') + else: + logger.info('๐Ÿ”“ Client unbound from SMSC') async def connect_and_bind( self, bind_type: BindType = BindType.TRANSCEIVER ) -> None: - """Connect to SMSC and bind""" + """Connect to SMSC and bind with enhanced shutdown awareness.""" try: # Connect to SMSC await self.client.connect() - logger.info('Connected to SMSC') + logger.info('๐Ÿ”— Connected to SMSC') - # Bind as transceiver (can send and receive) + # Bind as requested type if bind_type == BindType.TRANSMITTER: await self.client.bind_transmitter() elif bind_type == BindType.RECEIVER: @@ -105,7 +241,7 @@ async def connect_and_bind( await self.client.bind_transceiver() except Exception as e: - logger.error(f'Failed to connect and bind: {e}') + logger.error(f'โŒ Failed to connect and bind: {e}') raise async def send_sms( @@ -122,6 +258,9 @@ async def send_sms( """Send SMS message""" try: if not self.client.is_bound: + if self.shutdown_requested: + logger.debug('๐Ÿ”‡ Skipping SMS send - shutdown in progress') + return None raise Exception('Client is not bound to SMSC') # Set registered delivery if receipt is requested @@ -143,13 +282,33 @@ async def send_sms( data_coding=DataCoding.DEFAULT, ) - logger.info(f'SMS sent successfully, message ID: {message_id}') + logger.info(f'๐Ÿ“ค SMS sent successfully, message ID: {message_id}') return message_id except Exception as e: - logger.error(f'Failed to send SMS: {e}') + if not self.shutdown_requested: + logger.error(f'โŒ Failed to send SMS: {e}') return None + async def send_command(self, command: str) -> Optional[str]: + """ + Send a command to the server (for testing enhanced shutdown). + + Uses proper SMPP address types to ensure validation passes: + - Source: Numeric short code with UNKNOWN TON/NPI + - Destination: Alphanumeric "SERVER" with ALPHANUMERIC TON + """ + return await self.send_sms( + source_addr='99999', # Use numeric address for source + destination_addr='SERVER', # Use alphanumeric destination for server commands + message=command, + request_delivery_receipt=False, + source_ton=TonType.UNKNOWN, # Unknown for numeric short code + source_npi=NpiType.UNKNOWN, + dest_ton=TonType.ALPHANUMERIC, # Alphanumeric for "SERVER" destination + dest_npi=NpiType.UNKNOWN, + ) + async def send_unicode_sms( self, source_addr: str, @@ -182,127 +341,349 @@ async def send_unicode_sms( registered_delivery=registered_delivery, ) - logger.info(f'Unicode SMS sent successfully, message ID: {message_id}') + logger.info(f'๐Ÿ“ค Unicode SMS sent successfully, message ID: {message_id}') return message_id except Exception as e: - logger.error(f'Failed to send Unicode SMS: {e}') + logger.error(f'โŒ Failed to send Unicode SMS: {e}') return None async def disconnect(self) -> None: - """Disconnect from SMSC""" + """Disconnect from SMSC with enhanced shutdown handling.""" try: - await self.client.disconnect() - logger.info('Disconnected from SMSC') + if self.client.is_connected: + await self.client.disconnect() + logger.info('๐Ÿ‘‹ Disconnected from SMSC') + else: + logger.info('๐Ÿ”Œ Already disconnected from SMSC') except Exception as e: - logger.error(f'Error during disconnect: {e}') + logger.error(f'โŒ Error during disconnect: {e}') + + @property + def shutdown_requested(self) -> bool: + """Check if shutdown has been requested.""" + return self._shutdown_state != 'running' + + def get_shutdown_status(self) -> dict: + """Get current shutdown status information.""" + return { + 'shutdown_requested': self.shutdown_requested, + 'shutdown_state': self._shutdown_state, + 'received_notification': self._received_shutdown_notification, + 'received_reminder': self._received_shutdown_reminder, + 'grace_period': self._shutdown_grace_period, + 'is_connected': self.client.is_connected, + 'is_bound': self.client.is_bound, + } async def main(): - """Main example function""" - # SMSC connection details + """ + Main example function demonstrating enhanced shutdown handling. + + Features demonstrated: + - Connecting and binding to SMSC + - Sending various types of messages + - Interactive command support + - Shutdown notification handling + - Graceful disconnection + """ + # SMSC connection details - compatible with SMPP protocol (max 8 chars for password) SMSC_HOST = 'localhost' - SMSC_PORT = 2775 + SMSC_PORT = 2775 # Changed to high port number SYSTEM_ID = 'test_client' - PASSWORD = 'password' + PASSWORD = 'password' # Fixed: SMPP passwords must be <= 8 characters - # Create SMS client + # Create enhanced SMS client sms_client = SMSClient(SMSC_HOST, SMSC_PORT, SYSTEM_ID, PASSWORD) try: - # Connect and bind as transceiver + logger.info('๐Ÿš€ Enhanced SMPP Client starting...') + + # Connect and bind as transceiver (can send and receive) await sms_client.connect_and_bind(BindType.TRANSCEIVER) - # Send a simple SMS + # Send initial greeting await sms_client.send_sms( - source_addr='1234', - destination_addr='5678', - message='Hello from SMPP client!', + source_addr='12345', + destination_addr='67890', + message='Hello from enhanced SMPP client!', request_delivery_receipt=True, ) - # Send a Unicode SMS + # Test server commands + logger.info('๐Ÿงช Testing server commands...') + + # Send HELP command to see available server commands + await sms_client.send_command('HELP') + await asyncio.sleep(2) + + # Send STATUS command to see server status + await sms_client.send_command('STATUS') + await asyncio.sleep(2) + + # Send CLIENTS command to see connected clients + await sms_client.send_command('CLIENTS') + await asyncio.sleep(2) + + # Send a Unicode message await sms_client.send_unicode_sms( - source_addr='1234', - destination_addr='5678', - message='Hello ไธ–็•Œ! ๐ŸŒ', + source_addr='12345', + destination_addr='67890', + message='Unicode test: Hello ไธ–็•Œ! ๐ŸŒ Shutdown ready!', request_delivery_receipt=True, ) - # Keep the connection alive for a while to receive delivery receipts - logger.info('Waiting for delivery receipts...') - await asyncio.sleep(10) - - # Send multiple messages - for i in range(3): - await sms_client.send_sms( - source_addr='1234', - destination_addr='5678', - message=f'Test message {i + 1}', - request_delivery_receipt=True, - ) - await asyncio.sleep(1) - - # Wait a bit more - await asyncio.sleep(5) + # Simulate some activity while monitoring for shutdown notifications + logger.info('๐Ÿ“ก Monitoring for messages and shutdown notifications...') + logger.info('๐Ÿ’ก To test enhanced shutdown:') + logger.info(' 1. Send "SHUTDOWN" command: triggers demo shutdown') + logger.info(' 2. Press Ctrl+C on server: triggers signal-based shutdown') + logger.info(' 3. Watch this client respond to shutdown notifications') + logger.info('') + + # Keep the client active and responsive + activity_counter = 0 + while not sms_client.shutdown_requested: + # Check if we're still connected before trying to send messages + if not sms_client.client.is_connected: + logger.warning('โš ๏ธ No longer connected to server - stopping activity') + break + + # Send periodic activity messages + if activity_counter % 30 == 0: # Every 30 seconds + try: + await sms_client.send_sms( + source_addr='12345', + destination_addr='67890', + message=f'Periodic activity message #{activity_counter // 30 + 1}', + request_delivery_receipt=True, + ) + logger.info( + f'๐Ÿ“Š Activity counter: {activity_counter} (client running normally)' + ) + except Exception as e: + logger.warning(f'โš ๏ธ Failed to send activity message: {e}') + # If we can't send messages, something is wrong + break + + # Check if user wants to trigger shutdown demo + if activity_counter == 60: # After 1 minute, offer to test shutdown + logger.info('') + logger.info( + '๐Ÿงช Testing enhanced shutdown - sending SHUTDOWN command...' + ) + try: + await sms_client.send_command('SHUTDOWN') + logger.info( + '๐ŸŽฌ Enhanced shutdown demo initiated! Watch the logs...' + ) + except Exception as e: + logger.warning(f'โš ๏ธ Failed to send shutdown command: {e}') + + await asyncio.sleep(2) + activity_counter += 2 + + # Break if we've been running for too long without shutdown + if activity_counter > 300: # 5 minutes max + logger.info('โฐ Timeout reached - ending demo') + break + + # If shutdown was requested, wait a bit for it to complete + if sms_client.shutdown_requested: + logger.info('โณ Waiting for shutdown to complete...') + await asyncio.sleep(3) except KeyboardInterrupt: - logger.info('Interrupted by user') + logger.info('๐Ÿ”ด Client interrupted by user') except Exception as e: - logger.error(f'Error in main: {e}') + logger.error(f'โŒ Error in main: {e}', exc_info=True) finally: + # Show final shutdown status + status = sms_client.get_shutdown_status() + logger.info('๐Ÿ“‹ Final shutdown status:') + for key, value in status.items(): + logger.info(f' {key}: {value}') + # Clean disconnect await sms_client.disconnect() + logger.info('โœ… Enhanced SMPP client shutdown complete') async def simple_send_example(): - """Simple example of sending one SMS""" + """Simple example of sending one SMS with enhanced shutdown awareness.""" + logger.info('๐Ÿš€ Simple enhanced client example...') + async with SMPPClient( - host='localhost', port=2775, system_id='test_client', password='password' + host='localhost', + port=2775, # Changed to high port number + system_id='test_client', + password='password', # Fixed: SMPP passwords must be <= 8 characters ) as client: # Bind as transmitter await client.bind_transmitter() + logger.info('๐Ÿ” Bound as transmitter') # Send SMS message_id = await client.submit_sm( - source_addr='1234', destination_addr='5678', short_message='Hello World!' + source_addr='12345', + destination_addr='67890', + short_message='Hello World from enhanced client!', ) - print(f'Message sent with ID: {message_id}') + logger.info(f'๐Ÿ“ค Message sent with ID: {message_id}') async def monitor_messages_example(): - """Example of monitoring incoming messages""" + """ + Example of monitoring incoming messages with enhanced shutdown handling. + + This client will properly handle server shutdown notifications. + """ + logger.info('๐ŸŽง Enhanced message monitoring client starting...') + client = SMPPClient( - host='localhost', port=2775, system_id='test_receiver', password='password' + host='localhost', + port=2775, # Changed to high port number + system_id='test_receiver', + password='password', # Fixed: SMPP passwords must be <= 8 characters ) + shutdown_requested = False + def handle_message(client: SMPPClient, pdu: DeliverSm): + nonlocal shutdown_requested + message = pdu.short_message.decode('utf-8', errors='ignore') - print(f'Received message from {pdu.source_addr}: {message}') + + # Check for shutdown notifications + if ( + pdu.source_addr + in ('SYSTEM', 'SMSC') # Fixed: Updated for SMPP-compliant system IDs + and 'shutdown' in message.lower() + ): + logger.warning(f'๐Ÿ›‘ Server shutdown notification: {message}') + logger.info('โœ… Will disconnect gracefully...') + shutdown_requested = True + else: + logger.info(f'๐Ÿ“ฅ Message from {pdu.source_addr}: {message}') client.on_deliver_sm = handle_message try: await client.connect() await client.bind_receiver() + logger.info('๐Ÿ” Bound as receiver') - print('Monitoring for incoming messages... Press Ctrl+C to stop') + logger.info('๐Ÿ‘‚ Monitoring for incoming messages and shutdown notifications...') + logger.info('๐Ÿ›‘ Press Ctrl+C to stop, or server will notify when shutting down') - # Keep running until interrupted - while True: + # Keep running until interrupted or shutdown requested + while not shutdown_requested: await asyncio.sleep(1) + logger.info('๐Ÿ Shutdown requested - exiting gracefully') + except KeyboardInterrupt: - print('\nStopping...') + logger.info('๐Ÿ”ด Client interrupted by user') finally: await client.disconnect() + logger.info('๐Ÿ‘‹ Monitor client disconnected') + + +async def interactive_client_example(): + """ + Interactive client that can send commands to test enhanced shutdown. + """ + logger.info('๐ŸŽฎ Interactive enhanced SMPP client starting...') + + # Create enhanced client + sms_client = SMSClient( + 'localhost', 2775, 'demo_client', 'demo_pas' + ) # Changed to high port number + + try: + await sms_client.connect_and_bind(BindType.TRANSCEIVER) + + logger.info('') + logger.info('๐ŸŽฏ Interactive commands available:') + logger.info(' HELP - Show server help') + logger.info(' STATUS - Show server status') + logger.info(' CLIENTS - List connected clients') + logger.info(' TIME - Get server time') + logger.info(' SHUTDOWN - Trigger enhanced shutdown demo') + logger.info(' QUIT - Disconnect client') + logger.info('') + logger.info('๐Ÿ’ก The client will automatically handle shutdown notifications!') + logger.info('') + + # Send initial status request + await sms_client.send_command('STATUS') + + # Interactive loop + command_count = 0 + while not sms_client.shutdown_requested and command_count < 10: + # Simulate interactive commands + commands = ['HELP', 'STATUS', 'CLIENTS', 'TIME'] + command = commands[command_count % len(commands)] + + logger.info(f'๐Ÿ“ค Sending command: {command}') + await sms_client.send_command(command) + + # After a few commands, trigger shutdown demo + if command_count == 3: + logger.info('') + logger.info('๐Ÿงช Testing enhanced shutdown...') + await sms_client.send_command('SHUTDOWN') + logger.info('๐ŸŽฌ Shutdown demo initiated!') + + await asyncio.sleep(5) + command_count += 1 + + # Wait for shutdown to complete if requested + if sms_client.shutdown_requested: + logger.info('โณ Waiting for shutdown sequence to complete...') + await asyncio.sleep(5) + + except Exception as e: + logger.error(f'โŒ Interactive client error: {e}') + finally: + await sms_client.disconnect() + logger.info('โœ… Interactive client session ended') if __name__ == '__main__': - # Run the main example - asyncio.run(main()) + print('๐ŸŽฏ Enhanced SMPP Client Example') + print('=' * 40) + print() + print('Features demonstrated:') + print('โ€ข Enhanced shutdown notification handling') + print('โ€ข Graceful disconnection on server shutdown') + print('โ€ข Interactive command support') + print('โ€ข Proper connection lifecycle management') + print('โ€ข Automatic response to server shutdown notifications') + print() + print('To test enhanced shutdown:') + print('1. Start the enhanced server (examples/server.py)') + print('2. Run this client') + print('3. Watch how client handles server shutdown notifications') + print('4. Client will disconnect gracefully when server shuts down') + print() + print('Starting enhanced client...') + print() + + # Run the main enhanced client example + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('\n๐Ÿ›‘ Client interrupted - enhanced shutdown handling demonstrated') # Uncomment to run other examples: + # print("Running simple send example...") # asyncio.run(simple_send_example()) + # + # print("Running message monitor example...") # asyncio.run(monitor_messages_example()) + # + # print("Running interactive client example...") + # asyncio.run(interactive_client_example()) diff --git a/examples/server.py b/examples/server.py index e9db4a8..397a802 100644 --- a/examples/server.py +++ b/examples/server.py @@ -2,8 +2,14 @@ """ SMPP Server Example -This example demonstrates how to use the SMPP server to accept client connections, -handle bind requests, and process SMS messages. +This example demonstrates how to use the SMPP server with enhanced shutdown features +to accept client connections, handle bind requests, and process SMS messages. + +Enhanced Shutdown Features: +- Graceful notification to bound clients +- Configurable grace periods and timeouts +- Automatic unbind sequence +- Comprehensive logging of shutdown process Updated for the new modular code structure with clean imports from the main smpp package. """ @@ -20,92 +26,172 @@ from smpp import DataCoding, SMPPServer, SubmitSm from smpp.server import ClientSession -# Configure logging +# Configure logging with enhanced format for shutdown monitoring logging.basicConfig( - level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + level=logging.INFO, + format='%(asctime)s [%(levelname)-8s] [%(name)s] %(message)s', + datefmt='%H:%M:%S', ) logger = logging.getLogger(__name__) class SMSCServer: - """Example SMSC server using SMPP""" - - def __init__(self, host: str = 'localhost', port: int = 2775): + """ + Example SMSC server using SMPP with enhanced shutdown capabilities. + + Features: + - Enhanced shutdown with client notifications and grace periods + - Message processing with echo commands + - Broadcast messaging capabilities + - Delivery receipt simulation + - Comprehensive client authentication + """ + + def __init__( + self, host: str = 'localhost', port: int = 2775 + ): # Changed to high port number + # Initialize server with enhanced shutdown enabled self.server = SMPPServer( host=host, port=port, - system_id='TEST_SMSC', + system_id='SMSC', # Fixed: SMPP system_id must be <= 16 characters max_connections=50, + setup_signal_handlers=True, # Enable signal-based shutdown ) - # Store for received messages + # Message storage and state self.message_store = {} + self._shutdown_requested = False + self._background_tasks: set[asyncio.Task] = set() + self._cleanup_lock = asyncio.Lock() + + # Configure enhanced shutdown parameters + self._configure_enhanced_shutdown() # Set up event handlers + self._setup_event_handlers() + + def _configure_enhanced_shutdown(self) -> None: + """Configure enhanced shutdown with reasonable timeouts for demonstration.""" + try: + # Use the new configure_shutdown method for all parameters at once + self.server.configure_shutdown( + grace_period=15.0, # Give clients 15 seconds to disconnect gracefully + reminder_delay=5.0, # Send reminder after 5 more seconds + shutdown_timeout=10.0, # Force disconnect after 10 additional seconds + ) + + logger.info('Enhanced shutdown configuration applied successfully') + + # Log the current configuration for visibility + config = self.server.get_shutdown_config() + logger.info('Shutdown configuration:') + for key, value in config.items(): + logger.info(f' {key}: {value}') + + except ValueError as e: + logger.error(f'Failed to configure shutdown: {e}') + # Fall back to individual configuration if needed + logger.info('Using individual shutdown configuration methods as fallback') + self.server.set_shutdown_grace_period(15.0) + self.server.set_shutdown_reminder_delay(5.0) + self.server.set_shutdown_timeout(10.0) + + def _setup_event_handlers(self) -> None: + """Set up all server event handlers.""" self.server.authenticate = self.authenticate_client self.server.on_client_connected = self.handle_client_connected self.server.on_client_disconnected = self.handle_client_disconnected self.server.on_client_bound = self.handle_client_bound self.server.on_message_received = self.handle_message_received - # Store for message tracking - def authenticate_client( self, system_id: str, password: str, system_type: str ) -> bool: - """Authenticate client credentials""" - # Simple authentication - in production, use proper credential validation + """ + Authenticate client credentials with enhanced logging. + + Args: + system_id: Client system identifier + password: Client password + system_type: Client system type + + Returns: + True if authentication successful, False otherwise + """ + # Enhanced client credentials for demonstration (SMPP passwords must be <= 8 chars) valid_clients = { 'test_client': 'password', 'test_receiver': 'password', 'test_transmitter': 'password', + 'demo_client': 'demo_pas', + 'demo_client_1': 'demo123', # For shutdown demo + 'demo_client_2': 'demo123', # For shutdown demo + 'demo_client_3': 'demo123', # For shutdown demo + 'shutdown_test': 'shutdown', # Special client for shutdown testing + # Integration test clients + 'client_0': 'password', + 'client_1': 'password', + 'client_2': 'password', + 'client_3': 'password', + 'client_4': 'password', } is_valid = valid_clients.get(system_id) == password if is_valid: - logger.info(f'Authentication successful for {system_id}') + logger.info( + f'โœ“ Authentication successful for {system_id} (type: {system_type})' + ) else: - logger.warning(f'Authentication failed for {system_id}') + logger.warning( + f'โœ— Authentication failed for {system_id} (type: {system_type})' + ) return is_valid def handle_client_connected( self, server: SMPPServer, session: ClientSession ) -> None: - """Handle new client connection""" - logger.info( - f'Client connected from {session.connection.host}:{session.connection.port}' - ) + """Handle new client connection with enhanced logging.""" + client_info = f'{session.connection.host}:{session.connection.port}' + logger.info(f'๐Ÿ”— New client connected from {client_info}') + + # Log current connection count + logger.info(f'๐Ÿ“Š Total active connections: {server.client_count}') def handle_client_disconnected( self, server: SMPPServer, session: ClientSession ) -> None: - """Handle client disconnection""" - logger.info(f'Client {session.system_id} disconnected') + """Handle client disconnection with enhanced logging.""" + logger.info(f'๐Ÿ”Œ Client {session.system_id or "unknown"} disconnected') + logger.info(f'๐Ÿ“Š Total active connections: {server.client_count}') def handle_client_bound(self, server: SMPPServer, session: ClientSession) -> None: - """Handle successful client bind""" - logger.info(f'Client {session.system_id} bound as {session.bind_type}') + """Handle successful client bind with enhanced logging.""" + logger.info(f'๐Ÿ” Client {session.system_id} bound as {session.bind_type}') + + # Send welcome message to clients that can receive + if session.bind_type in ('receiver', 'transceiver'): + asyncio.create_task(self._send_welcome_message(session)) def handle_message_received( self, server: SMPPServer, session: ClientSession, pdu: SubmitSm ) -> Optional[str]: - """Handle SMS message from client""" + """Handle SMS message from client with enhanced logging and commands.""" try: # Decode message message = pdu.short_message.decode('utf-8', errors='ignore') - logger.info(f'Message received from {session.system_id}:') - logger.info(f' From: {pdu.source_addr}') - logger.info(f' To: {pdu.destination_addr}') - logger.info(f' Message: {message}') - logger.info(f' Data Coding: {pdu.data_coding}') + logger.info(f'๐Ÿ“ฅ Message received from {session.system_id}:') + logger.info(f' From: {pdu.source_addr} โ†’ To: {pdu.destination_addr}') + logger.info(f' Content: "{message}"') + logger.info(f' Data Coding: {pdu.data_coding}') # Generate custom message ID message_id = f'MSG_{session.system_id}_{session.message_counter + 1:06d}' - # Store message for later delivery or processing + # Store message for later processing self.message_store[message_id] = { 'source': pdu.source_addr, 'destination': pdu.destination_addr, @@ -115,71 +201,100 @@ def handle_message_received( 'data_coding': pdu.data_coding, } - # Simulate message processing - asyncio.create_task(self.process_message(message_id, session, pdu)) + # Process message asynchronously + task = asyncio.create_task(self.process_message(message_id, session, pdu)) + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) return message_id except Exception as e: - logger.error(f'Error handling message: {e}') + logger.error(f'โŒ Error handling message: {e}') return None async def process_message( self, message_id: str, session: ClientSession, pdu: SubmitSm ) -> None: - """Process received message (simulate delivery)""" + """Process received message with enhanced command support.""" try: # Simulate processing delay - await asyncio.sleep(1) + await asyncio.sleep(0.5) message_info = self.message_store.get(message_id) if not message_info: return - logger.info(f'Processing message {message_id}') + logger.info(f'โš™๏ธ Processing message {message_id}') + + # Enhanced command processing + message = message_info['message'].lower().strip() - # Check if this is an echo request - message = message_info['message'].lower() if message.startswith('echo '): # Echo the message back echo_text = message_info['message'][5:] # Remove 'echo ' prefix - await self.send_echo_response(session, pdu, echo_text) + await self._send_response(session, pdu, echo_text) elif message == 'help': - # Send help message - help_text = 'Available commands: ECHO , HELP, STATUS, TIME' - await self.send_echo_response(session, pdu, help_text) + # Send enhanced help message (shortened to fit SMS limits) + help_text = ( + 'Commands: ECHO , HELP, STATUS, TIME, SHUTDOWN, CLIENTS' + ) + await self._send_response(session, pdu, help_text) elif message == 'status': - # Send server status + # Send enhanced server status + stats = self.get_server_stats() status_text = ( - f'Server running. Connected clients: {self.server.client_count}' + f'Server Status:\n' + f'Total connections: {stats["total_connections"]}\n' + f'Bound clients: {stats["bound_clients"]}\n' + f'Total messages: {stats["total_messages"]}\n' + f'Shutdown requested: {self._shutdown_requested}' ) - await self.send_echo_response(session, pdu, status_text) + await self._send_response(session, pdu, status_text) elif message == 'time': # Send current time import datetime time_text = f'Server time: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' - await self.send_echo_response(session, pdu, time_text) + await self._send_response(session, pdu, time_text) + + elif message == 'clients': + # List connected clients + bound_clients = self.server.get_bound_clients() + if bound_clients: + client_list = '\n'.join( + [ + f'{client.system_id} ({client.bind_type})' + for client in bound_clients + ] + ) + clients_text = f'Connected clients:\n{client_list}' + else: + clients_text = 'No bound clients' + await self._send_response(session, pdu, clients_text) + + elif message == 'shutdown': + # Demonstrate enhanced shutdown + await self._demonstrate_shutdown(session, pdu) # Simulate delivery receipt if requested if pdu.registered_delivery: - await asyncio.sleep(2) # Simulate delivery delay - await self.send_delivery_receipt(session, message_id, pdu) + await asyncio.sleep(1.0) # Simulate delivery delay + await self._send_delivery_receipt(session, message_id, pdu) except Exception as e: - logger.error(f'Error processing message {message_id}: {e}') + logger.error(f'โŒ Error processing message {message_id}: {e}') - async def send_echo_response( + async def _send_response( self, session: ClientSession, original_pdu: SubmitSm, response_text: str ) -> None: - """Send an echo response back to the client""" + """Send a response back to the client.""" try: if session.bind_type not in ('receiver', 'transceiver'): logger.warning( - f'Cannot send response to {session.system_id} - not bound as receiver' + f'โš ๏ธ Cannot send response to {session.system_id} - not bound as receiver' ) return @@ -196,23 +311,66 @@ async def send_echo_response( ) if success: - logger.info(f'Echo response sent to {session.system_id}') + logger.info(f'๐Ÿ“ค Response sent to {session.system_id}') else: - logger.warning(f'Failed to send echo response to {session.system_id}') + logger.warning(f'โš ๏ธ Failed to send response to {session.system_id}') except Exception as e: - logger.error(f'Error sending echo response: {e}') + logger.error(f'โŒ Error sending response: {e}') + + async def _demonstrate_shutdown( + self, session: ClientSession, original_pdu: SubmitSm + ) -> None: + """Demonstrate the enhanced shutdown feature.""" + try: + demo_text = ( + f'Enhanced shutdown demo initiated by {session.system_id}!\n' + 'Watch the logs for:\n' + '1. Shutdown notifications to all clients\n' + '2. Grace period countdown\n' + '3. Reminder messages\n' + '4. Unbind sequence\n' + '5. Force disconnect\n' + 'Server will shut down in 10 seconds...' + ) + + await self._send_response(session, original_pdu, demo_text) - async def send_delivery_receipt( + # Broadcast shutdown demo announcement + await self.broadcast_message( + source_addr='SYSTEM', + message='DEMO: Enhanced shutdown will begin in 10 seconds!', + ) + + # Schedule shutdown after delay + async def delayed_shutdown(): + await asyncio.sleep(10) + logger.info('๐Ÿ›‘ Demo shutdown initiated by client command') + self._shutdown_requested = True + # Trigger shutdown via the server's shutdown event + self.server._shutdown_event.set() + + task = asyncio.create_task(delayed_shutdown()) + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) + + except Exception as e: + logger.error(f'โŒ Error in shutdown demonstration: {e}') + + async def _send_delivery_receipt( self, session: ClientSession, message_id: str, original_pdu: SubmitSm ) -> None: - """Send delivery receipt to client""" + """Send delivery receipt to client.""" try: if session.bind_type not in ('receiver', 'transceiver'): return # Create delivery receipt text - receipt_text = f'id:{message_id} sub:001 dlvrd:001 submit date:2023010112000000 done date:2023010112000100 stat:DELIVRD err:000 text:' + receipt_text = ( + f'id:{message_id} sub:001 dlvrd:001 ' + f'submit date:2025010112000000 done date:2025010112000100 ' + f'stat:DELIVRD err:000 text:' + ) success = await self.server.deliver_sm( target_system_id=session.system_id, @@ -224,25 +382,53 @@ async def send_delivery_receipt( ) if success: - logger.info(f'Delivery receipt sent for message {message_id}') + logger.info(f'๐Ÿ“ง Delivery receipt sent for message {message_id}') else: logger.warning( - f'Failed to send delivery receipt for message {message_id}' + f'โš ๏ธ Failed to send delivery receipt for message {message_id}' ) except Exception as e: - logger.error(f'Error sending delivery receipt: {e}') + logger.error(f'โŒ Error sending delivery receipt: {e}') async def start(self) -> None: - """Start the SMSC server""" + """Start the SMSC server.""" await self.server.start() + logger.info(f'๐Ÿš€ SMSC server started on {self.server.host}:{self.server.port}') async def stop(self) -> None: - """Stop the SMSC server""" + """Stop the SMSC server with enhanced shutdown.""" + if self._shutdown_requested: + return + + logger.info('๐Ÿ›‘ SMSC server stopping - enhanced shutdown sequence will begin') + self._shutdown_requested = True + + # Clean up all background tasks + await self._cleanup_background_tasks() + + # Trigger the enhanced shutdown sequence via the server await self.server.stop() + async def _cleanup_background_tasks(self) -> None: + """Clean up all background tasks gracefully.""" + async with self._cleanup_lock: + if not self._background_tasks: + return + + logger.debug(f'Cancelling {len(self._background_tasks)} background tasks') + + for task in self._background_tasks.copy(): + if not task.done(): + task.cancel() + + if self._background_tasks: + await asyncio.gather(*self._background_tasks, return_exceptions=True) + + self._background_tasks.clear() + async def broadcast_message(self, source_addr: str, message: str) -> None: - """Broadcast message to all connected receiver clients""" + """Broadcast message to all connected receiver clients.""" bound_clients = self.server.get_bound_clients() receiver_clients = [ client @@ -251,12 +437,33 @@ async def broadcast_message(self, source_addr: str, message: str) -> None: ] if not receiver_clients: - logger.info('No receiver clients to broadcast to') + logger.info('๐Ÿ“ข No receiver clients to broadcast to') return - logger.info(f'Broadcasting message to {len(receiver_clients)} clients') + logger.info(f'๐Ÿ“ข Broadcasting message to {len(receiver_clients)} clients') + # Send to all clients concurrently + tasks = [] for client in receiver_clients: + task = asyncio.create_task( + self._send_broadcast_to_client(client, source_addr, message) + ) + tasks.append(task) + + # Wait for all broadcasts to complete + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Log results + successful = sum(1 for result in results if result is True) + failed = len(results) - successful + + logger.info(f'๐Ÿ“ข Broadcast complete: {successful} successful, {failed} failed') + + async def _send_broadcast_to_client( + self, client: ClientSession, source_addr: str, message: str + ) -> bool: + """Send broadcast message to a specific client.""" + try: success = await self.server.deliver_sm( target_system_id=client.system_id, source_addr=source_addr, @@ -266,15 +473,21 @@ async def broadcast_message(self, source_addr: str, message: str) -> None: ) if success: - logger.info(f'Broadcast message sent to {client.system_id}') + logger.debug(f'๐Ÿ“ค Broadcast sent to {client.system_id}') else: - logger.warning( - f'Failed to send broadcast message to {client.system_id}' - ) + logger.warning(f'โš ๏ธ Failed to send broadcast to {client.system_id}') + + return success + + except Exception as e: + logger.error(f'โŒ Error sending broadcast to {client.system_id}: {e}') + return False def get_server_stats(self) -> dict: - """Get server statistics""" + """Get comprehensive server statistics.""" bound_clients = self.server.get_bound_clients() + shutdown_config = self.server.get_shutdown_config() + return { 'total_connections': self.server.client_count, 'bound_clients': len(bound_clients), @@ -286,78 +499,205 @@ def get_server_stats(self) -> dict: [c for c in bound_clients if c.bind_type == 'transceiver'] ), 'total_messages': len(self.message_store), + 'background_tasks': len(self._background_tasks), + 'shutdown_requested': self._shutdown_requested, + 'shutdown_config': shutdown_config, } + async def _send_welcome_message(self, session: ClientSession) -> None: + """Send welcome message to newly bound clients.""" + try: + await asyncio.sleep(1) # Brief delay after bind -async def main(): - """Main server function""" - # Create SMSC server - smsc = SMSCServer(host='localhost', port=2775) + welcome_text = ( + f'Welcome to Enhanced SMSC, {session.system_id}! ' + 'Enhanced shutdown features are active. ' + 'Commands: ECHO , HELP, STATUS, TIME, SHUTDOWN' + ) - try: - # Start server - await smsc.start() - logger.info('SMSC server started. Waiting for connections...') - - # Print server stats periodically - async def print_stats(): - while True: - await asyncio.sleep(30) - stats = smsc.get_server_stats() - logger.info(f'Server stats: {stats}') - - # Start stats task - stats_task = asyncio.create_task(print_stats()) - - # Simulate broadcast messages periodically - async def send_broadcasts(): - await asyncio.sleep(60) # Wait a minute before first broadcast - counter = 1 - while True: - await smsc.broadcast_message( - source_addr='SYSTEM', message=f'System broadcast message #{counter}' + success = await self.server.deliver_sm( + target_system_id=session.system_id, + source_addr='SYSTEM', + destination_addr=session.system_id, + short_message=welcome_text, + data_coding=DataCoding.DEFAULT, + ) + + if success: + logger.info(f'๐Ÿ“จ Welcome message sent to {session.system_id}') + else: + logger.warning( + f'โš ๏ธ Failed to send welcome message to {session.system_id}' ) - counter += 1 - await asyncio.sleep(300) # Broadcast every 5 minutes - # Start broadcast task - broadcast_task = asyncio.create_task(send_broadcasts()) + except Exception as e: + logger.error( + f'โŒ Error sending welcome message to {session.system_id}: {e}' + ) - # Keep server running - while True: - await asyncio.sleep(1) - except KeyboardInterrupt: - logger.info('Server interrupted by user') +async def main(): + """ + Main server function demonstrating enhanced shutdown capabilities. + + Features demonstrated: + - Enhanced shutdown with client notifications + - Graceful client disconnection handling + - Background task management + - Comprehensive logging and monitoring + """ + # Create SMSC server with enhanced shutdown + smsc = SMSCServer(host='localhost', port=2775) # Changed to high port number + + # Background task references + stats_task = None + broadcast_task = None + + try: + # Create and start background tasks + stats_task = asyncio.create_task(run_stats_monitor(smsc)) + broadcast_task = asyncio.create_task(run_broadcast_scheduler(smsc)) + + # Display startup information + logger.info('๐ŸŽฏ Enhanced SMSC server ready for connections!') + logger.info('๐Ÿ”ง Enhanced shutdown features:') + config = smsc.server.get_shutdown_config() + logger.info(f' โ€ข Grace period: {config["grace_period"]}s') + logger.info(f' โ€ข Reminder delay: {config["reminder_delay"]}s') + logger.info(f' โ€ข Force disconnect timeout: {config["shutdown_timeout"]}s') + logger.info('') + logger.info('๐Ÿ’ก Test enhanced shutdown by:') + logger.info(' 1. Connect clients (see examples/client.py)') + logger.info(' 2. Send "SHUTDOWN" command to trigger demo') + logger.info(' 3. Or press Ctrl+C to trigger signal-based shutdown') + logger.info(' 4. Watch the enhanced shutdown sequence in logs') + logger.info('') + + # Run server until shutdown (serve_forever handles starting the server) + await smsc.server.serve_forever() + except Exception as e: - logger.error(f'Server error: {e}') + logger.error(f'โŒ Server error: {e}', exc_info=True) finally: - # Stop server - if 'stats_task' in locals(): - stats_task.cancel() - if 'broadcast_task' in locals(): - broadcast_task.cancel() + # Clean up background tasks + await cleanup_background_tasks(stats_task, broadcast_task) + + # Mark wrapper as stopped + smsc._shutdown_requested = True + logger.info('โœ… SMSC server shutdown complete') + + +async def run_stats_monitor(smsc: SMSCServer) -> None: + """Monitor and log server statistics periodically.""" + try: + # Wait before first stats output + await asyncio.sleep(30) + + while not smsc._shutdown_requested: + stats = smsc.get_server_stats() + logger.info( + f'๐Ÿ“Š Server Stats: {stats["total_connections"]} connections, ' + f'{stats["bound_clients"]} bound, {stats["total_messages"]} messages processed' + ) + + # Wait for next stats cycle + await asyncio.sleep(60) # Every minute + + except asyncio.CancelledError: + logger.debug('๐Ÿ“Š Stats monitor task cancelled') + raise + except Exception as e: + logger.error(f'โŒ Error in stats monitor: {e}') - await smsc.stop() - logger.info('SMSC server stopped') + +async def run_broadcast_scheduler(smsc: SMSCServer) -> None: + """Send periodic broadcast messages to demonstrate server capabilities.""" + try: + # Wait before first broadcast + await asyncio.sleep(120) # Wait 2 minutes before first broadcast + counter = 1 + + while not smsc._shutdown_requested: + await smsc.broadcast_message( + source_addr='SYSTEM', + message=f'๐Ÿ“ข System broadcast #{counter} - Enhanced SMSC is running!', + ) + counter += 1 + + # Wait for next broadcast + await asyncio.sleep(300) # Every 5 minutes + + except asyncio.CancelledError: + logger.debug('๐Ÿ“ข Broadcast scheduler task cancelled') + raise + except Exception as e: + logger.error(f'โŒ Error in broadcast scheduler: {e}') + + +async def cleanup_background_tasks(*tasks) -> None: + """Clean up background tasks gracefully.""" + for task in tasks: + if task and not task.done(): + logger.debug(f'๐Ÿงน Cancelling background task: {task.get_name()}') + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except Exception as e: + logger.warning(f'โš ๏ธ Error cancelling task {task.get_name()}: {e}') async def simple_server_example(): - """Simple server example""" - async with SMPPServer(host='localhost', port=2775) as _server: - logger.info('Simple SMSC server running...') + """ + Simple server example showcasing async context manager with enhanced shutdown. + + This demonstrates the cleanest way to use the enhanced shutdown features. + """ + logger.info('๐Ÿš€ Starting simple enhanced SMSC server...') + + async with SMPPServer( + host='localhost', + port=2775, # Changed to high port number + system_id='SIMPLE_SMSC', # Fixed: SMPP system_id must be <= 16 characters + setup_signal_handlers=True, + ) as server: + # Configure enhanced shutdown + server.configure_shutdown( + grace_period=10.0, reminder_delay=5.0, shutdown_timeout=15.0 + ) - # Keep running until interrupted - try: - while True: - await asyncio.sleep(1) - except KeyboardInterrupt: - logger.info('Server stopped') + logger.info('โœ… Simple enhanced SMSC server running...') + logger.info('๐Ÿ›‘ Press Ctrl+C to see enhanced shutdown in action') + + # Server will run until shutdown signal (SIGTERM/SIGINT) + # The async context manager will handle the enhanced shutdown automatically + await server.serve_forever() if __name__ == '__main__': - # Run the main server + print('๐ŸŽฏ Enhanced SMPP Server Example') + print('=' * 40) + print() + print('Features demonstrated:') + print('โ€ข Enhanced shutdown with client notifications') + print('โ€ข Graceful client disconnection handling') + print('โ€ข Interactive commands (ECHO, HELP, STATUS, TIME, SHUTDOWN)') + print('โ€ข Background task management') + print('โ€ข Comprehensive logging and monitoring') + print() + print('To test enhanced shutdown:') + print('1. Run this server') + print('2. Connect clients using examples/client.py') + print("3. Send 'SHUTDOWN' command or press Ctrl+C") + print('4. Watch the enhanced shutdown sequence in logs') + print() + print('Starting server...') + print() + + # Run the main enhanced server asyncio.run(main()) - # Uncomment to run simple example: + # Uncomment to run simple example instead: + # print("Running simple server example...") # asyncio.run(simple_server_example()) diff --git a/pyproject.toml b/pyproject.toml index ba848c9..5ae8296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,34 @@ name = "smppai" version = "0.1.0" description = "Async SMPP (Short Message Peer-to-Peer) Protocol v3.4 Implementation" readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 3 - Alpha", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Typing :: Typed", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Operating System :: OS Independent", +] +keywords = [ + "async", + "asyncio", + "library", + "smpp", + "smppv3.4", +] dependencies = ["typing-extensions>=4.0.0"] [dependency-groups] dev = [ "bandit>=1.8.5", + "black>=25.1.0", "build>=1.2.2.post1", "mypy-extensions>=1.1.0", "mypy>=1.0.0", @@ -34,6 +56,22 @@ reportMissingImports = true # Report missing imports reportUnusedImports = true # Report unused imports reportUnusedFunction = true # Report unused functions +[tool.black] +line-length = 88 +target-version = ["py313"] +include = '\.pyi?$' +exclude = '''( + /( + .git + | .mypy_cache + | .pytest_cache + | build + | dist + )/ +)''' +skip-string-normalization = true +fast = false + [tool.ruff] line-length = 88 indent-width = 4 diff --git a/src/smpp/server/server.py b/src/smpp/server/server.py index 525f972..e0984e8 100644 --- a/src/smpp/server/server.py +++ b/src/smpp/server/server.py @@ -7,8 +7,9 @@ import asyncio import logging +import signal from dataclasses import dataclass -from typing import Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional from ..exceptions import SMPPException from ..protocol import ( @@ -53,10 +54,40 @@ class ClientSession: class SMPPServer: """ - Async SMPP Server (SMSC) Implementation - - Provides a basic SMPP server that can handle client connections, - bind operations, and message submission/delivery. + Enhanced SMPP Server with graceful shutdown capabilities. + + This server implementation provides comprehensive shutdown handling with: + + Shutdown Sequence: + 1. Stop accepting new connections + 2. Send shutdown notifications to all bound clients + 3. Wait for grace period for clients to disconnect gracefully + 4. Send reminder notifications to remaining clients + 5. Send unbind requests to remaining clients + 6. Force disconnect after timeout + + Configuration: + grace_period: Time to wait after initial notification (default: 30s) + reminder_delay: Additional time after reminder (default: 10s) + shutdown_timeout: Maximum time for force disconnect (default: 30s) + + Features: + - Thread-safe shutdown operations + - Signal handler support (SIGTERM/SIGINT) + - Proper resource cleanup + - Restart capability after shutdown + - Comprehensive error handling + + Usage: + # Context manager (recommended) + async with SMPPServer() as server: + server.configure_shutdown(grace_period=15.0, reminder_delay=5.0) + await server.serve_forever() + + # Manual lifecycle + server = SMPPServer() + await server.start() + await server.serve_forever() # Handles shutdown automatically """ def __init__( @@ -66,6 +97,7 @@ def __init__( system_id: str = 'SMSC', interface_version: int = 0x34, max_connections: int = 100, + setup_signal_handlers: bool = True, ): """ Initialize SMPP Server @@ -76,17 +108,38 @@ def __init__( system_id: Server system ID interface_version: SMPP interface version max_connections: Maximum concurrent connections + setup_signal_handlers: Whether to set up signal handlers for graceful shutdown """ self.host = host self.port = port self.system_id = system_id self.interface_version = interface_version self.max_connections = max_connections + self._setup_signals = setup_signal_handlers self._server: Optional[asyncio.Server] = None self._running = False self._clients: Dict[str, ClientSession] = {} self._message_id_counter = 1 + self._shutdown_event = asyncio.Event() # Always create for clarity + self._shutdown_timeout = 30.0 # seconds to wait for graceful shutdown + self._signal_handlers_set = False # Track signal handler registration + self._original_signal_handlers: Dict[int, Any] = {} # Store original handlers + + # Enhanced shutdown configuration + self._shutdown_grace_period = ( + 30.0 # seconds to wait after shutdown notification + ) + self._shutdown_reminder_delay = 10.0 # seconds after grace period for reminder + self._shutdown_in_progress = ( + False # Flag to control acceptance of new connections/messages + ) + self._shutdown_lock = asyncio.Lock() # Thread-safe shutdown operations + self._loop: Optional[asyncio.AbstractEventLoop] = ( + None # Store event loop reference + ) + self._accept_new_connections = True # Flag to control new connection acceptance + self._accept_new_messages = True # Flag to control new message acceptance # Authentication callback - should return True if credentials are valid self.authenticate: Optional[Callable[[str, str, str], bool]] = None @@ -108,6 +161,20 @@ def __init__( # Default authentication (allows all) self.authenticate = self._default_authenticate + def set_shutdown_timeout(self, timeout: float) -> None: + """ + Set the timeout for graceful shutdown. + + Args: + timeout: Maximum time in seconds to wait for clients to disconnect + """ + if timeout < 0: + raise ValueError('Shutdown timeout must be non-negative') + if timeout > 7200: # 2 hours max + raise ValueError('Shutdown timeout must not exceed 7200 seconds') + self._shutdown_timeout = timeout + logger.debug(f'Shutdown timeout set to {self._shutdown_timeout}s') + def _default_authenticate( self, system_id: str, password: str, system_type: str ) -> bool: @@ -122,6 +189,15 @@ def is_running(self) -> bool: """Check if server is running""" return self._running and self._server is not None + def _ensure_shutdown_event(self) -> asyncio.Event: + """Get shutdown event (always exists now)""" + return self._shutdown_event + + @property + def shutdown_requested(self) -> bool: + """Check if shutdown has been requested""" + return self._shutdown_event.is_set() + @property def client_count(self) -> int: """Get number of connected clients""" @@ -139,37 +215,347 @@ async def start(self) -> None: ) self._running = True + + # Set up signal handlers for graceful shutdown (if enabled and not already set) + if self._setup_signals and not self._signal_handlers_set: + self._setup_signal_handlers() + self._signal_handlers_set = True + logger.info(f'SMPP server started on {self.host}:{self.port}') async def stop(self) -> None: - """Stop the SMPP server""" - if not self.is_running: - return + """ + Stop the SMPP server gracefully with enhanced shutdown sequence. - logger.info('Stopping SMPP server') - self._running = False + This method is thread-safe and ensures the full enhanced shutdown sequence + runs exactly once, regardless of how shutdown is triggered. + """ + async with self._shutdown_lock: + if self._shutdown_in_progress: + return # Already shutting down - # Close server + if not self._running: + return # Not running + + self._shutdown_in_progress = True + logger.info('Initiating graceful shutdown of SMPP server') + self._running = False + + # Set shutdown event to unblock serve_forever if it's waiting + self._shutdown_event.set() + + try: + # First, gracefully disconnect all clients with enhanced sequence + await self._graceful_client_shutdown() + + # Then stop accepting new connections + await self._stop_server() + + except Exception as e: + logger.error(f'Error during server shutdown: {e}') + finally: + await self._reset_shutdown_state() + + async def _stop_server(self) -> None: + """Stop the server and close connections.""" if self._server: self._server.close() await self._server.wait_closed() self._server = None + logger.info('Server stopped accepting new connections') + + async def _reset_shutdown_state(self) -> None: + """Reset shutdown state for potential restart.""" + async with self._shutdown_lock: + # Unregister signal handlers + if self._setup_signals: + self._unregister_signal_handlers() + + # Reset shutdown event and flags for potential restart + self._shutdown_event = asyncio.Event() + self._shutdown_in_progress = False + self._accept_new_connections = True + self._accept_new_messages = True + logger.info('SMPP server stopped') + + def _setup_signal_handlers(self) -> None: + """Set up signal handlers for graceful shutdown using proper async patterns.""" + if not self._setup_signals: + return + + try: + # Store loop reference during setup + self._loop = asyncio.get_running_loop() + + def signal_handler(signum, frame): + logger.info(f'Received signal {signum}, initiating graceful shutdown') + if self._loop and not self._loop.is_closed(): + self._loop.call_soon_threadsafe( + self._handle_shutdown_signal, signum + ) + else: + logger.warning('Event loop not available for signal handling') + + # Set up handlers for common shutdown signals + for sig in [signal.SIGTERM, signal.SIGINT]: + try: + original_handler = signal.signal(sig, signal_handler) + self._original_signal_handlers[sig] = original_handler + self._signal_handlers_set = True + logger.debug(f'Signal handler registered for {sig}') + except (ValueError, OSError) as e: + logger.debug(f'Could not register signal handler for {sig}: {e}') + + except Exception as e: + logger.warning(f'Could not set up signal handlers: {e}') + + def _handle_shutdown_signal(self, signum: int) -> None: + """Handle shutdown signal in async context.""" + logger.debug(f'Processing shutdown signal {signum} in async context') + self._shutdown_event.set() + + def _unregister_signal_handlers(self) -> None: + """Unregister signal handlers to clean up on shutdown""" + try: + for sig, original_handler in self._original_signal_handlers.items(): + try: + signal.signal(sig, original_handler) + logger.debug(f'Signal handler restored for {sig}') + except (ValueError, OSError) as e: + logger.debug(f'Could not restore signal handler for {sig}: {e}') + self._original_signal_handlers.clear() + self._signal_handlers_set = False + except Exception as e: + logger.warning(f'Could not unregister signal handlers: {e}') + + async def _send_shutdown_notification( + self, session: ClientSession, is_reminder: bool = False + ) -> bool: + """ + Send shutdown notification to a client session with comprehensive error handling. + + Returns: + True if notification was sent successfully, False otherwise + """ + try: + message_type = 'reminder' if is_reminder else 'notification' + grace_period_seconds = self._shutdown_grace_period + + # Format grace period appropriately + if grace_period_seconds >= 1.0: + grace_period_str = f'{int(grace_period_seconds)}s' + else: + grace_period_str = f'{grace_period_seconds:.1f}s' + + # Create shutdown notification as a deliver_sm PDU with special message + message_text = f'SMPP server shutdown {message_type}. ' + if not is_reminder: + message_text += ( + f'Grace period: {grace_period_str}. Please disconnect gracefully.' + ) + else: + message_text += 'Final warning - server shutting down shortly.' + + # Send as deliver_sm + deliver_pdu = DeliverSm( + service_type='', + source_addr_ton=TonType.UNKNOWN, + source_addr_npi=NpiType.UNKNOWN, + source_addr=self.system_id[:21], # Truncate to max 21 chars + dest_addr_ton=TonType.UNKNOWN, + dest_addr_npi=NpiType.UNKNOWN, + destination_addr=session.system_id or 'unknown', + esm_class=0, + protocol_id=0, + priority_flag=0, + schedule_delivery_time='', + validity_period='', + registered_delivery=0, + replace_if_present_flag=0, + data_coding=DataCoding.DEFAULT, + sm_default_msg_id=0, + short_message=message_text.encode('utf-8'), + ) + + await session.connection.send_pdu(deliver_pdu, wait_response=False) + logger.info(f'Sent shutdown {message_type} to {session.system_id}') + return True + + except asyncio.CancelledError: + raise # Re-raise cancellation + except Exception as e: + logger.error( + f'Failed to send shutdown notification to {session.system_id}: {e}' + ) + return False + + async def _broadcast_shutdown_notification(self, is_reminder: bool = False) -> None: + """Send shutdown notification to all bound clients""" + notification_type = 'reminder' if is_reminder else 'notification' + logger.info( + f'Broadcasting shutdown {notification_type} to {len(self._clients)} clients' + ) + + tasks = [] + for session in self._clients.values(): + if session.bound and session.bind_type in ('receiver', 'transceiver'): + task = asyncio.create_task( + self._send_shutdown_notification(session, is_reminder) + ) + tasks.append(task) + + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + async def _graceful_client_shutdown(self) -> None: + """ + Enhanced graceful shutdown with broadcast notifications and grace periods. + """ + logger.info('Starting enhanced graceful shutdown sequence') + self._shutdown_in_progress = True + self._accept_new_connections = False + self._accept_new_messages = False - # Disconnect all clients - clients_to_disconnect = list(self._clients.values()) - for client in clients_to_disconnect: + if not self._clients: + logger.info( + 'No clients connected - still respecting grace period for demonstration' + ) + logger.info( + f'Grace period: waiting {self._shutdown_grace_period}s as configured' + ) + await asyncio.sleep(self._shutdown_grace_period) + logger.info('Grace period complete - enhanced shutdown sequence complete') + return + + logger.info(f'Shutting down with {len(self._clients)} clients connected') + + # Notify clients and wait for graceful disconnection + await self._notify_and_wait_for_disconnect() + + # Force disconnect any remaining clients + await self._force_disconnect_remaining_clients() + + async def _notify_and_wait_for_disconnect(self) -> None: + """Send notifications and wait for clients to disconnect gracefully.""" + # Send initial shutdown notification + await self._broadcast_shutdown_notification(is_reminder=False) + + # Wait for grace period + if await self._wait_for_graceful_disconnect(): + return # All clients disconnected + + # Send reminder and wait additional time + remaining_bound = [c for c in self._clients.values() if c.bound] + if remaining_bound: + logger.info( + f'Sending shutdown reminder to {len(remaining_bound)} remaining clients' + ) + await self._broadcast_shutdown_notification(is_reminder=True) + logger.info(f'Waiting {self._shutdown_reminder_delay}s after reminder') + await asyncio.sleep(self._shutdown_reminder_delay) + + async def _wait_for_graceful_disconnect(self) -> bool: + """ + Wait for grace period and check if clients disconnect. + + Returns: + True if all clients disconnected, False if timeout reached + """ + logger.info( + f'Grace period started - waiting {self._shutdown_grace_period}s for clients to disconnect' + ) + grace_start = asyncio.get_event_loop().time() + + while ( + asyncio.get_event_loop().time() - grace_start + ) < self._shutdown_grace_period: + bound_clients = [c for c in self._clients.values() if c.bound] + if not bound_clients: + logger.info('All clients disconnected during grace period') + return True + await asyncio.sleep(1.0) # Check every second + + return False + + async def _force_disconnect_remaining_clients(self) -> None: + """Force disconnect all remaining clients.""" + clients_to_cleanup = list(self._clients.values()) + self._clients.clear() + + # Send unbind requests to bound clients + await self._send_unbind_requests(clients_to_cleanup) + + # Force disconnect all clients + await self._disconnect_all_clients(clients_to_cleanup) + + async def _send_unbind_requests(self, clients: List[ClientSession]) -> None: + """Send unbind requests to all bound clients.""" + unbind_tasks = [ + self._send_unbind_to_client(client) for client in clients if client.bound + ] + + if unbind_tasks: + try: + await asyncio.wait_for( + asyncio.gather(*unbind_tasks, return_exceptions=True), timeout=10.0 + ) + logger.debug('Sent unbind requests to all bound clients') + except asyncio.TimeoutError: + logger.warning('Timeout waiting for unbind responses (10s)') + + async def _disconnect_all_clients(self, clients: List[ClientSession]) -> None: + """Force disconnect all clients.""" + disconnect_tasks = [] + for client in clients: try: - await client.connection.disconnect() + task = asyncio.create_task(client.connection.disconnect()) + disconnect_tasks.append(task) except Exception as e: - logger.warning(f'Error disconnecting client: {e}') + logger.warning(f'Error creating disconnect task for client: {e}') - self._clients.clear() - logger.info('SMPP server stopped') + if disconnect_tasks: + try: + await asyncio.wait_for( + asyncio.gather(*disconnect_tasks, return_exceptions=True), + timeout=self._shutdown_timeout, + ) + logger.debug('All clients disconnected') + except asyncio.TimeoutError: + logger.warning( + f'Timeout after {self._shutdown_timeout}s waiting for client disconnections' + ) + + async def _send_unbind_to_client(self, client: ClientSession) -> None: + """Send unbind request to a client""" + try: + if client.connection and client.bound: + # Create unbind PDU + unbind_pdu = Unbind() + + # Send unbind and wait for response + await client.connection.send_pdu( + unbind_pdu, wait_response=True, timeout=5.0 + ) + logger.debug(f'Sent unbind to client {client.system_id}') + + # Update client state + client.bound = False + client.bind_type = '' + + except Exception as e: + logger.warning(f'Error sending unbind to client {client.system_id}: {e}') async def _handle_client_connection( self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter ) -> None: """Handle new client connection""" + # Check if we're accepting new connections + if not self._accept_new_connections: + logger.info('Rejecting new connection - server shutdown in progress') + writer.close() + await writer.wait_closed() + return + client_addr = writer.get_extra_info('peername') logger.info(f'New client connection from {client_addr}') @@ -346,25 +732,31 @@ async def _send_bind_response( resp_pdu = BindTransmitterResp( # type: ignore[call-arg] sequence_number=bind_pdu.sequence_number, command_status=status, - system_id=self.system_id # type: ignore[call-arg] - if status == CommandStatus.ESME_ROK - else '', + system_id=( + self.system_id # type: ignore[call-arg] + if status == CommandStatus.ESME_ROK + else '' + ), ) elif isinstance(bind_pdu, BindReceiver): resp_pdu = BindReceiverResp( # type: ignore[call-arg] sequence_number=bind_pdu.sequence_number, command_status=status, - system_id=self.system_id # type: ignore[call-arg] - if status == CommandStatus.ESME_ROK - else '', + system_id=( + self.system_id # type: ignore[call-arg] + if status == CommandStatus.ESME_ROK + else '' + ), ) elif isinstance(bind_pdu, BindTransceiver): resp_pdu = BindTransceiverResp( # type: ignore[call-arg] sequence_number=bind_pdu.sequence_number, command_status=status, - system_id=self.system_id # type: ignore[call-arg] - if status == CommandStatus.ESME_ROK - else '', + system_id=( + self.system_id # type: ignore[call-arg] + if status == CommandStatus.ESME_ROK + else '' + ), ) else: return @@ -400,6 +792,13 @@ async def _handle_unbind_request(self, session: ClientSession, pdu: Unbind) -> N async def _handle_submit_sm(self, session: ClientSession, pdu: SubmitSm) -> None: """Handle submit_sm request from client""" try: + # Check if we're accepting new messages + if not self._accept_new_messages: + await self._send_submit_sm_response( + session, pdu, CommandStatus.ESME_RSUBMITFAIL + ) + return + if not session.bound or session.bind_type not in ( 'transmitter', 'transceiver', @@ -587,6 +986,26 @@ def _get_next_message_id(self) -> str: self._message_id_counter += 1 return msg_id + async def serve_forever(self) -> None: + """ + Start the server and run until shutdown signal is received. + + This method handles graceful shutdown when SIGTERM or SIGINT is received. + The server will run until the shutdown event is set, either by signal + handlers or programmatically via stop(). + """ + await self.start() + + try: + logger.info('Server running, waiting for shutdown signal...') + # Wait for shutdown signal + await self._shutdown_event.wait() + logger.info('Shutdown signal received, stopping server...') + except Exception as e: + logger.error(f'Error in serve_forever: {e}') + finally: + await self.stop() + def get_client_sessions(self) -> List[ClientSession]: """Get list of all client sessions""" return list(self._clients.values()) @@ -595,13 +1014,95 @@ def get_bound_clients(self) -> List[ClientSession]: """Get list of bound client sessions""" return [session for session in self._clients.values() if session.bound] + def set_shutdown_grace_period(self, seconds: float) -> None: + """Set the grace period for shutdown notifications""" + if seconds < 0: + raise ValueError('Grace period must be non-negative') + if seconds > 3600: # 1 hour max + raise ValueError('Grace period must not exceed 3600 seconds') + self._shutdown_grace_period = seconds + logger.info(f'Shutdown grace period set to {self._shutdown_grace_period}s') + + def set_shutdown_reminder_delay(self, seconds: float) -> None: + """Set the delay after grace period before final shutdown""" + if seconds < 0: + raise ValueError('Reminder delay must be non-negative') + if seconds > 3600: # 1 hour max + raise ValueError('Reminder delay must not exceed 3600 seconds') + self._shutdown_reminder_delay = seconds + logger.info(f'Shutdown reminder delay set to {self._shutdown_reminder_delay}s') + + def get_shutdown_config(self) -> dict: + """Get current shutdown configuration""" + return { + 'grace_period': self._shutdown_grace_period, + 'reminder_delay': self._shutdown_reminder_delay, + 'shutdown_timeout': self._shutdown_timeout, + 'shutdown_in_progress': self._shutdown_in_progress, + 'accept_new_connections': self._accept_new_connections, + 'accept_new_messages': self._accept_new_messages, + } + + def configure_shutdown( + self, + grace_period: float = 30.0, + reminder_delay: float = 10.0, + shutdown_timeout: float = 30.0, + ) -> None: + """ + Configure all shutdown parameters with comprehensive validation. + + Args: + grace_period: Seconds to wait after shutdown notification + reminder_delay: Seconds to wait after reminder before force disconnect + shutdown_timeout: Maximum seconds to wait for client disconnections + + Raises: + TypeError: If parameters are not numeric + ValueError: If any parameter is invalid or total time exceeds reasonable limits + """ + # Type validation + for name, value in [ + ('grace_period', grace_period), + ('reminder_delay', reminder_delay), + ('shutdown_timeout', shutdown_timeout), + ]: + if not isinstance(value, (int, float)): + raise TypeError(f'{name} must be a number, got {type(value).__name__}') + if value < 0: + raise ValueError(f'{name} must be non-negative, got {value}') + + # Range validation + if grace_period > 3600: + raise ValueError('Grace period must not exceed 1 hour') + if reminder_delay > 3600: + raise ValueError('Reminder delay must not exceed 1 hour') + if shutdown_timeout > 7200: + raise ValueError('Shutdown timeout must not exceed 2 hours') + + # Logical validation + total_time = grace_period + reminder_delay + 15 # 15s for unbind operations + if total_time > 3600: + raise ValueError( + f'Total shutdown time ({total_time:.1f}s) exceeds 1 hour limit' + ) + + # Set values atomically + self._shutdown_grace_period = grace_period + self._shutdown_reminder_delay = reminder_delay + self._shutdown_timeout = shutdown_timeout + + logger.info( + f'Shutdown configured: grace={grace_period}s, reminder={reminder_delay}s, timeout={shutdown_timeout}s (total~{total_time:.1f}s)' + ) + async def __aenter__(self): """Async context manager entry""" await self.start() return self async def __aexit__(self, exc_type, exc_val, exc_tb): - """Async context manager exit""" + """Async context manager exit with graceful shutdown""" await self.stop() def __repr__(self) -> str: diff --git a/tests/integration/test_shutdown_integration.py b/tests/integration/test_shutdown_integration.py new file mode 100644 index 0000000..ce21b27 --- /dev/null +++ b/tests/integration/test_shutdown_integration.py @@ -0,0 +1,255 @@ +""" +Integration tests for SMPP Server and Client shutdown interaction + +These tests verify the complete shutdown flow between server and client, +ensuring proper notification handling and graceful disconnection. +""" + +import asyncio +import pytest +from unittest.mock import Mock + +from examples.client import SMSClient +from examples.server import SMSCServer + + +@pytest.mark.integration +class TestShutdownIntegration: + """Integration tests for server-client shutdown interaction""" + + @pytest.fixture(autouse=True) + async def cleanup_after_test(self): + """Ensure clean state after each test.""" + yield + # Cancel any remaining tasks + tasks = [t for t in asyncio.all_tasks() if not t.done()] + for task in tasks: + if not task.done(): + task.cancel() + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + @pytest.mark.asyncio + async def test_server_client_shutdown_integration(self): + """Test complete server-client shutdown interaction.""" + # Create server with fast timeouts for testing + server = SMSCServer(host='127.0.0.1', port=0) + server.server.configure_shutdown( + grace_period=0.5, # 500ms + reminder_delay=0.2, # 200ms + shutdown_timeout=0.5, # 500ms + ) + + # Start server + await server.start() + actual_port = server.server._server.sockets[0].getsockname()[1] + + # Create and connect client + client = SMSClient('127.0.0.1', actual_port, 'test_client', 'password') + + try: + await client.connect_and_bind() + + # Verify client is connected + assert client.client.is_connected + assert client.client.is_bound + + # Send a test message + message_id = await client.send_sms( + '12345', '67890', 'Test message before shutdown' + ) + assert message_id is not None + + # Give server time to process message + await asyncio.sleep(0.1) + + # Trigger server shutdown + shutdown_task = asyncio.create_task(server.stop()) + + # Wait for client to detect shutdown and disconnect gracefully + # The client should receive shutdown notification and disconnect within grace period + await asyncio.sleep(1.0) # Wait for full shutdown sequence + + # Verify client received shutdown notification and disconnected + assert client.shutdown_requested or not client.client.is_connected + + # Wait for server shutdown to complete + await shutdown_task + + assert not server.server.is_running + + finally: + # Cleanup + try: + await client.disconnect() + except Exception: + pass + try: + await server.stop() + except Exception: + pass + + @pytest.mark.asyncio + async def test_multiple_clients_shutdown(self): + """Test shutdown with multiple connected clients.""" + # Create server + server = SMSCServer(host='127.0.0.1', port=0) + server.server.configure_shutdown( + grace_period=0.3, reminder_delay=0.2, shutdown_timeout=0.5 + ) + + await server.start() + actual_port = server.server._server.sockets[0].getsockname()[1] + + # Create multiple clients + clients = [] + for i in range(3): + client = SMSClient('127.0.0.1', actual_port, f'client_{i}', 'password') + await client.connect_and_bind() + clients.append(client) + + try: + # Verify all clients are connected + for client in clients: + assert client.client.is_connected + assert client.client.is_bound + + # Trigger server shutdown + start_time = asyncio.get_event_loop().time() + shutdown_task = asyncio.create_task(server.stop()) + + # Wait for shutdown to complete + await shutdown_task + + shutdown_time = asyncio.get_event_loop().time() - start_time + + # Verify shutdown took at least the grace period + assert shutdown_time >= 0.3, f'Shutdown too fast: {shutdown_time:.3f}s' + + # All clients should have been notified and disconnected + for client in clients: + assert client.shutdown_requested or not client.client.is_connected + + finally: + # Cleanup + for client in clients: + try: + await client.disconnect() + except Exception: + pass + try: + await server.stop() + except Exception: + pass + + @pytest.mark.asyncio + async def test_client_reconnection_after_server_restart(self): + """Test that client can reconnect after server restart.""" + # Create server + server = SMSCServer(host='127.0.0.1', port=0) + server.server.configure_shutdown( + grace_period=0.2, reminder_delay=0.1, shutdown_timeout=0.3 + ) + + await server.start() + actual_port = server.server._server.sockets[0].getsockname()[1] + + # Create client + client = SMSClient('127.0.0.1', actual_port, 'test_client', 'password') + await client.connect_and_bind() + + # Verify initial connection + assert client.client.is_connected + + # Shutdown server + await server.stop() + + # Wait for client to detect disconnection + await asyncio.sleep(0.5) + + # Verify client detected shutdown + assert not client.client.is_connected or client.shutdown_requested + + # Restart server (create new instance on same port) + server = SMSCServer(host='127.0.0.1', port=actual_port) + await server.start() + + try: + # Create new client connection + client = SMSClient('127.0.0.1', actual_port, 'test_client', 'password') + await client.connect_and_bind() + + # Verify reconnection works + assert client.client.is_connected + assert client.client.is_bound + + # Send message to verify functionality + message_id = await client.send_sms('12345', '67890', 'Test after restart') + assert message_id is not None + + finally: + try: + await client.disconnect() + except Exception: + pass + try: + await server.stop() + except Exception: + pass + + +@pytest.mark.performance +class TestShutdownPerformance: + """Performance tests for shutdown operations""" + + @pytest.mark.asyncio + async def test_shutdown_performance_with_many_clients(self): + """Test shutdown performance with many concurrent clients.""" + server = SMSCServer(host='127.0.0.1', port=0) + server.server.configure_shutdown( + grace_period=0.5, reminder_delay=0.2, shutdown_timeout=1.0 + ) + + await server.start() + actual_port = server.server._server.sockets[0].getsockname()[1] + + # Create mock clients (simplified for performance testing) + from smpp.server.server import ClientSession + from smpp.transport import SMPPConnection + from unittest.mock import AsyncMock + + clients = [] + for i in range(50): # 50 clients for performance test + mock_connection = Mock(spec=SMPPConnection) + mock_connection.disconnect = AsyncMock() + mock_connection.send_pdu = AsyncMock() + mock_connection.is_connected = True + mock_connection.host = '127.0.0.1' + mock_connection.port = actual_port + + session = ClientSession( + connection=mock_connection, + system_id=f'client_{i}', + bind_type='transceiver', + bound=True, + ) + server.server._clients[f'client_{i}'] = session + clients.append(session) + + try: + # Measure shutdown time + import time + + start_time = time.time() + await server.stop() + shutdown_time = time.time() - start_time + + # Should complete within reasonable time even with many clients + assert shutdown_time < 3.0, f'Shutdown took too long: {shutdown_time:.2f}s' + + # Verify all clients received shutdown notifications + for session in clients: + session.connection.send_pdu.assert_called() + + finally: + await server.stop() diff --git a/uv.lock b/uv.lock index adde0e7..29ec77d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = ">=3.13" +requires-python = ">=3.10" [[package]] name = "annotated-types" @@ -11,6 +11,15 @@ 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 = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + [[package]] name = "bandit" version = "1.8.5" @@ -26,14 +35,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/b0/5c8976e61944f91904d4fd33bdbe55248138bfbd1a6092753b1b0fb7abbc/bandit-1.8.5-py3-none-any.whl", hash = "sha256:cb2e57524e99e33ced48833c6cc9c12ac78ae970bb6a450a83c4b506ecc1e2f9", size = 131759, upload-time = "2025-06-17T01:43:35.045Z" }, ] +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + [[package]] name = "build" version = "1.2.2.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } wheels = [ @@ -58,6 +103,29 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, @@ -73,6 +141,45 @@ version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, @@ -128,6 +235,38 @@ version = "7.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/78/1c1c5ec58f16817c09cbacb39783c3655d54a221b6552f47ff5ac9297603/coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", size = 212028, upload-time = "2025-06-13T13:00:29.293Z" }, + { url = "https://files.pythonhosted.org/packages/98/db/e91b9076f3a888e3b4ad7972ea3842297a52cc52e73fd1e529856e473510/coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", size = 212420, upload-time = "2025-06-13T13:00:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d0/2b3733412954576b0aea0a16c3b6b8fbe95eb975d8bfa10b07359ead4252/coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", size = 241529, upload-time = "2025-06-13T13:00:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/b3/00/5e2e5ae2e750a872226a68e984d4d3f3563cb01d1afb449a17aa819bc2c4/coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3", size = 239403, upload-time = "2025-06-13T13:00:37.399Z" }, + { url = "https://files.pythonhosted.org/packages/37/3b/a2c27736035156b0a7c20683afe7df498480c0dfdf503b8c878a21b6d7fb/coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", size = 240548, upload-time = "2025-06-13T13:00:39.647Z" }, + { url = "https://files.pythonhosted.org/packages/98/f5/13d5fc074c3c0e0dc80422d9535814abf190f1254d7c3451590dc4f8b18c/coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", size = 240459, upload-time = "2025-06-13T13:00:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/36/24/24b9676ea06102df824c4a56ffd13dc9da7904478db519efa877d16527d5/coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", size = 239128, upload-time = "2025-06-13T13:00:42.343Z" }, + { url = "https://files.pythonhosted.org/packages/be/05/242b7a7d491b369ac5fee7908a6e5ba42b3030450f3ad62c645b40c23e0e/coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", size = 239402, upload-time = "2025-06-13T13:00:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/73/e0/4de7f87192fa65c9c8fbaeb75507e124f82396b71de1797da5602898be32/coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", size = 214518, upload-time = "2025-06-13T13:00:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/5e4e2fe458907d2a65fab62c773671cfc5ac704f1e7a9ddd91996f66e3c2/coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", size = 215436, upload-time = "2025-06-13T13:00:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, + { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, + { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, + { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, + { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, + { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, + { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, + { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, @@ -150,9 +289,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, ] +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cryptography" version = "45.0.4" @@ -180,6 +325,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732, upload-time = "2025-06-10T00:03:27.896Z" }, + { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424, upload-time = "2025-06-10T00:03:29.992Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438, upload-time = "2025-06-10T00:03:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622, upload-time = "2025-06-10T00:03:33.491Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899, upload-time = "2025-06-10T00:03:38.659Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900, upload-time = "2025-06-10T00:03:40.233Z" }, + { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422, upload-time = "2025-06-10T00:03:41.827Z" }, + { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475, upload-time = "2025-06-10T00:03:43.493Z" }, ] [[package]] @@ -212,6 +365,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, ] +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -257,6 +422,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "importlib-resources" version = "6.5.2" @@ -291,6 +468,9 @@ wheels = [ name = "jaraco-context" version = "6.0.1" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, @@ -334,6 +514,7 @@ name = "keyring" version = "25.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "jaraco-classes" }, { name = "jaraco-context" }, { name = "jaraco-functools" }, @@ -364,6 +545,36 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, @@ -411,10 +622,29 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" }, + { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, @@ -494,6 +724,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997, upload-time = "2025-02-04T14:28:03.168Z" }, ] +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -536,6 +775,47 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, @@ -553,6 +833,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] [[package]] @@ -579,10 +877,12 @@ version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ @@ -606,7 +906,7 @@ name = "pytest-cov" version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] @@ -691,6 +991,33 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, @@ -759,6 +1086,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ @@ -832,7 +1160,7 @@ wheels = [ [[package]] name = "smppai" -version = "0.0.0" +version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "typing-extensions" }, @@ -841,6 +1169,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "bandit" }, + { name = "black" }, { name = "build" }, { name = "mypy" }, { name = "mypy-extensions" }, @@ -858,6 +1187,7 @@ requires-dist = [{ name = "typing-extensions", specifier = ">=4.0.0" }] [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.5" }, + { name = "black", specifier = ">=25.1.0" }, { name = "build", specifier = ">=1.2.2.post1" }, { name = "mypy", specifier = ">=1.0.0" }, { name = "mypy-extensions", specifier = ">=1.1.0" }, @@ -881,6 +1211,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533, upload-time = "2025-02-20T14:03:55.849Z" }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + [[package]] name = "tomlkit" version = "0.13.3" @@ -946,6 +1315,39 @@ version = "1.17.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, @@ -970,3 +1372,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]