diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index 4f71ecc..608f659 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -2,7 +2,7 @@ name: tests on: push: - branches: [ "main", "development" ] + branches: ["**"] pull_request: branches: [ "main" ] schedule: @@ -14,6 +14,34 @@ permissions: security-events: write # Required for security findings jobs: + quality: + name: Code Quality + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6.2.0 + with: + python-version: "3.13" + + - name: Set up uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.9.18" + + - name: Install dependencies + run: uv sync --dev + + - name: Lint with Ruff + run: uv run ruff check . + + - name: Type check with MyPy + run: uv run mypy . + + - name: Security check with Bandit + run: uv run bandit -c pyproject.toml -r pyoutlineapi + test: name: Run Tests runs-on: ubuntu-latest @@ -21,38 +49,35 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - - name: Install Poetry - uses: snok/install-poetry@v1 + - name: Set up uv + uses: astral-sh/setup-uv@v3 with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true + version: "0.9.18" - name: Load cached venv - id: cached-poetry-dependencies + id: cached-uv-dependencies uses: actions/cache@v4 with: path: .venv - key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/uv.lock', '**/pyproject.toml') }} - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --with dev + if: steps.cached-uv-dependencies.outputs.cache-hit != 'true' + run: uv sync --dev - name: Run tests run: | - poetry run pytest --cov=./ --cov-report=xml + uv run pytest --cov=./ --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 @@ -65,13 +90,12 @@ jobs: name: Security Checks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.13" - cache: 'pip' - name: Install security tools run: | @@ -89,4 +113,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: - category: "/language:python" \ No newline at end of file + category: "/language:python" diff --git a/.gitignore b/.gitignore index 4c2e18c..808fd0f 100644 --- a/.gitignore +++ b/.gitignore @@ -132,4 +132,11 @@ logs/ tmp/ temp/ -main.py \ No newline at end of file +main.py +/.uv-cache/ +/.cache/codeclone/cache.json +/AGENTS.md +/comprehensive_demo.py +/.cache/codeclone/report.html +/.cache/codeclone/report.txt +/api.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index ff74ff3..cde54dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,29 +5,514 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.3.0] - 2025-06-XX +## [0.4.0] - 2026-01-30 + +### Changed + +### Added + +#### **Enterprise Features** + +- **Circuit Breaker Pattern** (`circuit_breaker.py`) + - Automatic failure detection and recovery with configurable thresholds + - Three-state circuit (CLOSED, OPEN, HALF_OPEN) with smart transitions + - Comprehensive metrics tracking (success rate, failure rate, state changes) + - Timeout enforcement with exponential backoff + - Thread-safe implementation with asyncio.Lock protection + - Configurable via `CircuitConfig` with validation + - Example: Protects against cascading failures in distributed systems + +- **Audit Logging System** (`audit.py`) + - Production-ready audit logger with async queue processing + - Singleton pattern for global audit logger management + - `@AuditDecorator` for automatic action logging + - Sensitive data sanitization (passwords, tokens, secrets) + - Structured logging with correlation IDs for request tracing + - Support for both sync and async logging operations + - Graceful shutdown with queue draining + - `NoOpAuditLogger` for disabling audit without code changes + +- **Health Monitoring** (`health_monitoring.py`) + - `HealthMonitor` class with configurable caching (1-300 seconds TTL) + - Quick health checks for fast connectivity testing + - Comprehensive health checks with multiple dimensions + - Custom health check registration support + - Performance metrics tracking with EMA (Exponential Moving Average) smoothing + - `wait_for_healthy()` method for graceful startup coordination + - Immutable `HealthStatus` results for thread safety + +- **Metrics Collection** (`metrics_collector.py`) + - Advanced `MetricsCollector` with automatic periodic collection + - Memory-efficient storage using `collections.deque` (binary search optimized via `bisect`) + - Prometheus export format with extensive metrics: + - Traffic metrics (bytes, megabytes, gigabytes transferred) + - Rate metrics (bytes/second, megabytes/second) + - Peak metrics and active key tracking + - Location-based metrics from experimental API + - Bandwidth metrics (current, peak with timestamps) + - Collection metadata (uptime, interval, snapshot count) + - Per-key usage statistics with temporal queries + - Configurable history limits (1-100,000 snapshots) + - Size validation to prevent memory exhaustion (max 10MB per snapshot) + - Context manager support for automatic lifecycle management + - `experimental_since` option for `MetricsCollector` to control experimental metrics range. + +- **Batch Operations** (`batch_operations.py`) + - Generic `BatchProcessor` with concurrency control + - Type-safe batch operations for multiple operations: + - `create_multiple_keys()` - Bulk key creation + - `delete_multiple_keys()` - Bulk key deletion + - `rename_multiple_keys()` - Bulk key renaming + - `set_multiple_data_limits()` - Bulk limit configuration + - `fetch_multiple_keys()` - Parallel key retrieval + - `execute_custom_operations()` - Generic batch executor + - Comprehensive validation with detailed error reporting + - `fail_fast` mode for error handling strategy + - Immutable `BatchResult` with rich statistics: + - Success/failure counts and rates + - Validation error tracking + - Type-safe result extraction methods + - Dynamic concurrency adjustment during runtime + +#### **Configuration System** + +- **Enhanced Configuration** (`config.py`) + - `OutlineClientConfig` with Pydantic validation + - Environment-based configuration with `.env` file support + - Preset configurations: + - `DevelopmentConfig` - Relaxed security for local development + - `ProductionConfig` - Strict security enforcement + - `SecretStr` type for sensitive data protection + - Automatic circuit breaker timeout adjustment + - Immutable configuration snapshots with `model_copy_immutable()` + - `get_sanitized_config()` for safe logging + - `create_env_template()` utility for quick setup + - Factory methods: `from_env()`, `create_minimal()` + - `allow_private_networks` configuration flag (defaults to true). + - `resolve_dns_for_ssrf` strict SSRF mode (DNS resolution with rebinding guard). + - Production config now defaults to stricter SSRF settings (blocks private networks, DNS recheck enabled). + +#### **Type System & Validation** + +- **Common Types Module** (`common_types.py`) + - Extensive type aliases for better IDE support: + - `Port`, `Bytes`, `TimestampMs`, `TimestampSec` + - `JsonDict`, `JsonList`, `JsonPayload`, `ResponseData` + - `AuditDetails`, `MetricsTags`, `QueryParams` + - `Constants` class with security limits and defaults + - Enhanced `Validators` utility class with DRY optimizations + - Security utilities: `secure_compare()`, `mask_sensitive_data()` + - Strict SSRF DNS validation to block private/reserved IPs in hostnames + - `ConfigOverrides` and `ClientDependencies` TypedDict for type safety + - Comprehensive sensitive key detection (32+ patterns) + +- **Enhanced Exception Hierarchy** (`exceptions.py`) + - Rich error context with separate internal/safe details + - Message length limits to prevent DoS attacks + - Retry guidance with `is_retryable` and `get_retry_delay()` + - Specific exception types: + - `CircuitOpenError` - With retry_after information + - `ConfigurationError` - With field and security_issue flags + - `ValidationError` - With field and model context + - `ConnectionError` - With host and port details + - `TimeoutError` - With timeout value and operation name + - Helper functions: `get_safe_error_dict()`, `format_error_chain()` + +#### **API Client Enhancements** + +- **Modular Architecture** (`api_mixins.py`) + - `ServerMixin` - Server management operations + - `AccessKeyMixin` - Access key CRUD operations + - `DataLimitMixin` - Global data limit management + - `MetricsMixin` - Metrics collection operations + - `HTTPClientProtocol` - Runtime-checkable protocol + - `AuditableMixin` - Automatic audit logger access + - `JsonFormattingMixin` - JSON format preference resolution + +- **Response Parser** (`response_parser.py`) + - Type-safe response parsing with overloads + - Comprehensive validation error logging (max 10 errors) + - `parse_simple()` for boolean success responses + - `validate_response_structure()` for lightweight pre-validation + - `extract_error_message()` with fallback strategies + - `is_error_response()` for error detection + +- **Base HTTP Client** (`base_client.py`) + - Enhanced `BaseHTTPClient` with enterprise features: + - Correlation ID tracking with `ContextVar` + - Cryptographically secure ID generation + - Rate limiting with dynamic adjustment + - Graceful shutdown with active request tracking + - Metrics collection integration + - Certificate pinning via SHA-256 fingerprint + - Connection pooling with configurable limits + - Request/response logging with sanitization + - `RateLimiter` class with thread-safe operations + - `RetryHelper` for DRY retry logic with jitter + - `NoOpMetrics` for optional metrics collection + +#### **Model Enhancements** + +- **Extended Models** (`models.py`) + - DRY mixins: `ByteConversionMixin`, `TimeConversionMixin` + - Factory methods: `DataLimit.from_kilobytes/megabytes/gigabytes()` + - Utility methods: + - `AccessKey.has_data_limit`, `display_name` + - `AccessKeyList.get_by_id()`, `get_by_name()`, filtering methods + - `Server.has_global_limit`, `created_timestamp_seconds` + - `ServerMetrics.get_top_consumers()`, `get_usage_for_key()` + - Enhanced experimental metrics models with proper validation + - `HealthCheckResult` and `ServerSummary` for monitoring + +#### **Developer Experience** + +- **Enhanced Package Interface** (`__init__.py`) + - `quick_setup()` - Interactive configuration wizard + - `print_type_info()` - Type alias documentation + - `get_version()` - Version information + - Better error messages for common mistakes + - Interactive help in REPL mode + - Comprehensive `__all__` export list (60+ symbols) + +--- + +### Changed + +#### **Breaking Changes** + +- Migrated tooling from Poetry to uv (PEP 621 metadata + dependency groups). +- Data-limit endpoints now send the schema-conformant payload (`{\"bytes\": ...}`). +- Experimental metrics `since` now accepts ISO-8601 timestamps in addition to relative durations. +- Monotonic time is used for internal timers to improve Python 3.14 compatibility. + +- **Client Constructor Signature**: + ```python + # Old (v0.3.0) + client = AsyncOutlineClient(api_url, cert_sha256, json_format=True) + + # New (v0.4.0) + client = AsyncOutlineClient( + api_url=api_url, + cert_sha256=cert_sha256, + timeout=10, + enable_logging=True + ) + # Or use configuration object + config = OutlineClientConfig.from_env() + client = AsyncOutlineClient(config) + ``` + +- **Default Values**: + - `timeout`: Changed from 30s to 10s for better responsiveness + - `retry_attempts`: Changed from 3 to 2 for faster failure detection + - `json_format`: Remains `False` (returns Pydantic models by default) + +- **Parameter Names**: + - `cert_sha256` now expects `SecretStr` in config (automatic in constructor) + - `user_agent` default changed to include version number + +#### **API Improvements** + +- **Method Enhancements**: + - All methods now support `as_json` parameter for runtime format selection + - Better type hints with overloads for precise return types + - Consistent error handling across all operations + - Improved parameter validation with descriptive error messages + +- **Client Architecture**: + - Split into mixin-based architecture for better code organization + - Separated concerns: HTTP client, API mixins, configuration + - Protocol-based design for better testability + - Enhanced session management with proper cleanup + +#### **Internal Optimizations** + +- **Performance**: + - Lazy initialization of expensive resources + - Connection pooling with keep-alive + - Efficient retry logic with exponential backoff + jitter + - Memory-optimized data structures (SortedList, __slots__) + +- **Code Quality**: + - DRY principles applied throughout (extracted 20+ helper methods) + - Type safety with runtime_checkable protocols + - Immutable data structures where appropriate + - Comprehensive docstrings with examples + +--- + +### Fixed + +- **Validation Issues**: + - Fixed empty name handling in `AccessKey` validation + - Improved port validation with clear error messages + - Better certificate fingerprint format validation + - Fixed URL validation edge cases + +- **Connection Stability**: + - Proper session cleanup on errors + - Fixed race conditions in session initialization + - Better handling of connection timeouts + - Improved SSL context creation error handling + +- **Error Handling**: + - More descriptive error messages with context + - Proper error chain preservation + - Better handling of API error responses + - Fixed validation error logging + +- **Memory Management**: + - Fixed potential memory leaks in metrics collection + - Proper cleanup of background tasks + - Limited history size with automatic trimming + - Snapshot size validation + +--- + +### Removed + +- **Deprecated Features**: + - Removed direct dictionary usage in internal methods + - Removed redundant validation code (now in Validators class) + - Removed hardcoded retry values (now configurable) + +- **Simplified API**: + - Removed internal helper methods now covered by mixins + - Consolidated duplicate code into DRY utilities + +--- + +### 📦 Dependencies + +- **New Dependencies**: + - `pydantic-settings` - For configuration management + +- **Updated Dependencies**: + - `pydantic` - Now requires v2.0+ + - `aiohttp` - Updated for better async support + +--- + +### 📚 Documentation + +- Enhanced docstrings with type information +- Comprehensive examples in each method +- Updated README with new features +- Configuration guide with best practices +- Migration guide from v0.3.0 + +--- + +### 🚀 Migration Guide (v0.3.0 → v0.4.0) + +#### **1. Python Version** + +```bash +# Ensure Python 3.10+ +python --version # Should be 3.10 or higher +``` + +#### **2. Installation** + +```bash +# Install new version +pip install --upgrade pyoutlineapi + +# Or with optional dependencies +pip install pyoutlineapi[dev] +``` + +#### **3. Basic Client Usage** + +```python +# Old way (v0.3.0) +from pyoutlineapi import AsyncOutlineClient + +client = AsyncOutlineClient( + "https://server.com/path", + "abc123...", + json_format=False, + timeout=30 +) + +# New way (v0.4.0) - Option 1: Direct instantiation +client = AsyncOutlineClient( + api_url="https://server.com/path", + cert_sha256="abc123...", + timeout=10, + enable_logging=True, + rate_limit=50 +) + +# New way (v0.4.0) - Option 2: Configuration object +from pyoutlineapi import OutlineClientConfig + +config = OutlineClientConfig.create_minimal( + api_url="https://server.com/path", + cert_sha256="abc123...", + timeout=10, + enable_logging=True +) +client = AsyncOutlineClient(config) + +# New way (v0.4.0) - Option 3: Environment variables +# Create .env file first +client = AsyncOutlineClient.from_env() +``` + +#### **4. Using New Features** + +```python +# Circuit breaker (automatic protection) +async with AsyncOutlineClient.create( + api_url=url, + cert_sha256=cert, + enable_circuit_breaker=True, + circuit_failure_threshold=5 +) as client: + # Automatically protected against cascading failures + await client.get_server_info() + +# Health monitoring +from pyoutlineapi.health_monitoring import HealthMonitor + +monitor = HealthMonitor(client, cache_ttl=30) +health = await monitor.comprehensive_check() +print(f"Healthy: {health.healthy}") +print(f"Failed checks: {health.failed_checks}") + +# Metrics collection +from pyoutlineapi.metrics_collector import MetricsCollector + +async with MetricsCollector(client, interval=60) as collector: + await collector.start() + # ... your code ... + stats = collector.get_usage_stats(period_minutes=60) + print(f"Total GB: {stats.gigabytes_transferred:.2f}") + +# Batch operations +from pyoutlineapi.batch_operations import BatchOperations + +batch = BatchOperations(client, max_concurrent=5) +configs = [ + {"name": "User1", "port": 8388}, + {"name": "User2", "port": 8389}, +] +result = await batch.create_multiple_keys(configs) +print(f"Success rate: {result.success_rate:.1%}") + +# Audit logging +from pyoutlineapi import DefaultAuditLogger + +audit = DefaultAuditLogger(enable_async=True) +client = AsyncOutlineClient(config, audit_logger=audit) +# All operations are now automatically audited +``` + +#### **5. Configuration Migration** + +```python +# Old: Hardcoded values +client = AsyncOutlineClient(url, cert, timeout=30) + +# New: Configuration object with validation +from pyoutlineapi import DevelopmentConfig, ProductionConfig + +# Development +dev_config = DevelopmentConfig( + api_url=url, + cert_sha256=cert, + enable_logging=True +) + +# Production +prod_config = ProductionConfig.from_env(".env.prod") +``` + +--- + +### 🎯 Use Cases + +#### **Enterprise Production Deployment** + +```python +from pyoutlineapi import ( + AsyncOutlineClient, + ProductionConfig, + DefaultAuditLogger, +) + +# Load production config with strict validation +config = ProductionConfig.from_env( + ".env.prod", + enable_circuit_breaker=True, + circuit_failure_threshold=3, + rate_limit=50 +) + +# Initialize with audit logging +audit_logger = DefaultAuditLogger( + enable_async=True, + queue_size=1000 +) + +async with AsyncOutlineClient( + config, + audit_logger=audit_logger +) as client: + # All operations are protected and audited + health = await client.health_check() + if health: + keys = await client.get_access_keys() +``` + +#### **Development & Testing** + +```python +from pyoutlineapi import DevelopmentConfig + +config = DevelopmentConfig( + api_url="http://localhost:8080/secret", + cert_sha256="0" * 64, # Dev cert + enable_logging=True, + enable_circuit_breaker=False +) + +async with AsyncOutlineClient(config) as client: + # Development with detailed logging + pass +``` + +--- + +## [0.3.0] - 2025-06-09 ### Added - **New API methods**: - `create_access_key_with_id()` - Create access key with specific custom ID - - `get_experimental_metrics(since)` - Get detailed experimental server metrics (requires mandatory `since` parameter) + - `get_experimental_metrics(since)` - Get detailed experimental server metrics (requires mandatory `since` + parameter) - `set_global_data_limit()` - Set global data transfer limit for all access keys - `remove_global_data_limit()` - Remove global data transfer limit + - **Enhanced models and validation**: - New request models: `AccessKeyNameRequest`, `DataLimitRequest`, `HostnameRequest`, `MetricsEnabledRequest`, `PortRequest`, `ServerNameRequest` - `ExperimentalMetrics` model for detailed server analytics - Better type safety with dedicated request/response models + - **Improved error handling**: - Separated exceptions into dedicated module (`exceptions.py`) - Enhanced error messages with more context - Better exception hierarchy + - **Retry mechanism enhancements**: - Configurable retry attempts via constructor parameter - Robust retry logic with exponential backoff - Automatic retry for transient failures (HTTP 408, 429, 500, 502, 503, 504) - Enhanced error tracking with attempt numbers + - **Constants and configuration**: - `MIN_PORT` and `MAX_PORT` constants for port validation - `DEFAULT_RETRY_ATTEMPTS`, `DEFAULT_RETRY_DELAY` for retry configuration @@ -40,11 +525,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Default timeout reduced from 30 to 10 seconds for better responsiveness - Access key ID parameters changed from `int` to `str` type for better API compatibility - Method signatures updated to use dedicated request models instead of raw dictionaries + - **API improvements**: - All request methods now use proper Pydantic models with `by_alias=True` serialization - Better handling of optional parameters with `exclude_none=True` - Improved type annotations throughout the codebase - Enhanced method documentation with updated examples + - **Internal optimizations**: - Refactored request handling with separate `_make_request` and `_retry_request` methods - Better session management and connection handling @@ -57,10 +544,12 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Removed deprecated `MetricsPeriod` parameter from `get_transfer_metrics()` (API doesn't support period filtering) - Fixed metrics status response parsing - **Documentation**: Corrected examples for `get_experimental_metrics()` to show that `since` parameter is mandatory + - **Data validation**: - Better handling of API response formats - Improved error messages for validation failures - Fixed SSL certificate fingerprint validation + - **Connection stability**: - More robust handling of connection failures and timeouts - Better cleanup of resources during session closure @@ -71,11 +560,12 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Deprecated features**: - `MetricsPeriod` enum and period parameter from `get_transfer_metrics()` - Direct dictionary usage in API requests (replaced with proper models) + - **Simplified API**: - Removed redundant parameter validation (now handled by Pydantic models) - Cleaned up internal helper methods -### Migration Guide +### Migration Guide (v0.3.0) For users upgrading from v0.2.0: @@ -147,8 +637,12 @@ For users upgrading from v0.2.0: - Support for custom certificate verification - Optional JSON response format +--- + +[0.4.0]: https://github.com/orenlab/pyoutlineapi/compare/v0.3.0...v0.4.0 + [0.3.0]: https://github.com/orenlab/pyoutlineapi/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/orenlab/pyoutlineapi/compare/v0.1.2...v0.2.0 -[0.1.2]: https://github.com/orenlab/pyoutlineapi/releases/tag/v0.1.2 \ No newline at end of file +[0.1.2]: https://github.com/orenlab/pyoutlineapi/releases/tag/v0.1.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d81392d..75ee408 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,14 +45,11 @@ To contribute code: 2. **Set Up Development Environment**: ```bash - # Install Poetry if you haven't already - curl -sSL https://install.python-poetry.org | python3 - + # Create a virtual environment + uv venv # Install dependencies - poetry install --with dev - - # Activate virtual environment - poetry shell + uv sync --dev ``` 3. **Create a Feature Branch**: @@ -71,16 +68,16 @@ To contribute code: 5. **Test Your Changes**: ```bash # Run tests with coverage - poetry run pytest + uv run pytest # Type checking - poetry run mypy pyoutlineapi + uv run mypy pyoutlineapi # Code formatting - poetry run black pyoutlineapi tests + uv run ruff format pyoutlineapi tests # Linting - poetry run flake8 pyoutlineapi tests + uv run ruff check pyoutlineapi tests ``` 6. **Submit a Pull Request**: @@ -173,15 +170,15 @@ Closes #123 1. **Required Dependencies**: - Python 3.10 or higher - - Poetry for package management + - uv for package management - Outline VPN server (for integration testing) 2. **Development Tools**: - All development dependencies are managed by Poetry and include: + All development dependencies are managed by uv and include: - pytest-cov for test coverage - black for code formatting - mypy for type checking - - flake8 for linting + - ruff for linting ## Project Configuration @@ -189,8 +186,8 @@ Key project settings are managed in `pyproject.toml`, including: - Python version requirement (3.10+) - Dependencies: - - pydantic (^2.9.2) - - aiohttp (^3.11.11) + - pydantic (>=2.12.3) + - aiohttp (>=3.13.2) - Development dependencies for testing and code quality - Pytest configuration with coverage reporting @@ -203,4 +200,4 @@ Key project settings are managed in `pyproject.toml`, including: By contributing, you agree that your contributions will be licensed under the MIT License. -Thank you for contributing to PyOutlineAPI! \ No newline at end of file +Thank you for contributing to PyOutlineAPI! diff --git a/README.md b/README.md index fb556c3..c1bf6fd 100644 --- a/README.md +++ b/README.md @@ -1,656 +1,113 @@ # PyOutlineAPI -A modern, async-first Python client for the [Outline VPN Server API](https://github.com/Jigsaw-Code/outline-server) with -full support for the latest schema and strict data validation using Pydantic. +**Enterprise-grade async Python client for Outline VPN Server Management API** [![tests](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml/badge.svg)](https://github.com/orenlab/pyoutlineapi/actions/workflows/python_tests.yml) [![codecov](https://codecov.io/gh/orenlab/pyoutlineapi/branch/main/graph/badge.svg?token=D0MPKCKFJQ)](https://codecov.io/gh/orenlab/pyoutlineapi) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=orenlab_pyoutlineapi&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=orenlab_pyoutlineapi) + ![PyPI - Downloads](https://img.shields.io/pypi/dm/pyoutlineapi) ![PyPI - Version](https://img.shields.io/pypi/v/pyoutlineapi) ![Python Version](https://img.shields.io/pypi/pyversions/pyoutlineapi) +![License](https://img.shields.io/pypi/l/pyoutlineapi) + +--- + +## Overview + +PyOutlineAPI is a modern, **production-ready** Python library for managing [Outline VPN](https://getoutline.org/) +servers. +It is async-first, type-safe, and designed for reliable operation in production. + +Highlights: -## Features - -- ⚡ **Async-first design** with full asyncio support and connection pooling -- 🔒 **Enterprise-grade security** with TLS certificate fingerprint verification -- ✅ **Type-safe** with comprehensive Pydantic models and static typing -- 🔄 **Smart retry logic** with exponential backoff for resilient connections -- 📊 **Complete metrics support** including experimental server metrics -- 🎯 **Advanced key management** with custom IDs, data limits, and flexible configuration -- 🌐 **Flexible response formats** - return JSON dict or typed Pydantic models -- 🛡️ **Robust error handling** with detailed exception hierarchy -- 📚 **Production-ready** with comprehensive logging and debugging support -- 🚀 **Batch operations** for creating multiple access keys efficiently -- 🏥 **Health checks** with automatic server status monitoring -- 🔧 **Advanced configuration** with rate limiting and connection management +- Async-first client built on aiohttp +- Certificate pinning + sensitive data protection +- Circuit breaker, health monitoring, retries with backoff +- Typed models (Pydantic v2) + rich validation +- Batch operations and rate limiting + +--- ## Installation -### From PyPI (Recommended) +**Requirements:** Python 3.10+ + +Using `pip`: ```bash pip install pyoutlineapi ``` -### With Poetry +Using `uv` (recommended): ```bash -poetry add pyoutlineapi +uv add pyoutlineapi ``` -### Development Installation +**Optional dependencies:** ```bash -git clone https://github.com/orenlab/pyoutlineapi.git -cd pyoutlineapi -pip install -e ".[dev]" +# Development tools +pip install pyoutlineapi[dev] ``` -## Quick Start - -```python -import asyncio -from pyoutlineapi import AsyncOutlineClient, DataLimit - - -async def main(): - # Initialize client with context manager (recommended) - async with AsyncOutlineClient( - api_url="https://your-outline-server:port/secret-path", - cert_sha256="your-certificate-fingerprint", - enable_logging=True # Enable debug logging - ) as client: - # Get server information - server = await client.get_server_info() - print(f"Connected to {server.name} (v{server.version})") - - # Create a new access key with data limit - key = await client.create_access_key( - name="Alice", - limit=DataLimit(bytes=5 * 1024 ** 3) # 5 GB limit - ) - print(f"Created key: {key.access_url}") +--- - # List all keys - keys = await client.get_access_keys() - print(f"Total keys: {len(keys.access_keys)}") +## Quick Start - # Get comprehensive server summary - summary = await client.get_server_summary() - print(f"Server healthy: {summary['healthy']}") - print(f"Access keys count: {summary['access_keys_count']}") +### 1) Setup configuration +```bash +# Generate .env template +python -c "from pyoutlineapi import quick_setup; quick_setup()" -if __name__ == "__main__": - asyncio.run(main()) +# Edit .env.example → .env +OUTLINE_API_URL=https://your-server.com:12345/your-secret-path +OUTLINE_CERT_SHA256=your-64-character-sha256-fingerprint ``` -## Configuration - -### Client Options +### 2) Minimal usage ```python from pyoutlineapi import AsyncOutlineClient +import asyncio -client = AsyncOutlineClient( - api_url="https://your-server:port/path", - cert_sha256="certificate-fingerprint", - json_format=False, # Return Pydantic models (default) or raw JSON - timeout=30, # Request timeout in seconds - retry_attempts=3, # Number of retry attempts for failed requests - enable_logging=True, # Enable debug logging - user_agent="MyApp/1.0", # Custom user agent - max_connections=10, # Maximum connections in pool - rate_limit_delay=0.1 # Minimum delay between requests (seconds) -) -``` - -### Factory Method - -```python -# Use factory method for one-off operations -async def quick_operation(): - async with AsyncOutlineClient.create( - "https://your-server:port/path", - "certificate-fingerprint", - enable_logging=True - ) as client: - server = await client.get_server_info() - return server -``` - -## Usage Guide - -### Server Management -```python -async def manage_server(): - async with AsyncOutlineClient(...) as client: - # Get server information +async def main(): + async with AsyncOutlineClient.from_env() as client: server = await client.get_server_info() - print(f"Server: {server.name}, Version: {server.version}") - - # Configure server - await client.rename_server("My VPN Server") - await client.set_hostname("vpn.example.com") - await client.set_default_port(8388) - - # Get comprehensive server summary - summary = await client.get_server_summary() - print(f"Server healthy: {summary['healthy']}") - print(f"Keys count: {summary['access_keys_count']}") + print(f"Server: {server.name} (v{server.version})") - if summary.get('metrics'): - total_bytes = sum(summary['metrics']['bytes_transferred_by_user_id'].values()) - print(f"Total data: {total_bytes / 1024 ** 3:.2f} GB") - - print("Server configured successfully") -``` - -### Health Monitoring - -```python -async def monitor_server_health(): - async with AsyncOutlineClient(...) as client: - # Manual health check - is_healthy = await client.health_check() - print(f"Server is healthy: {is_healthy}") - - # Force health check (ignore cache) - is_healthy = await client.health_check(force=True) - - # Check last health status - print(f"Last known health status: {client.is_healthy}") -``` - -### Access Key Management - -#### Basic Key Operations - -```python -async def basic_key_operations(): - async with AsyncOutlineClient(...) as client: - # Create a simple key - key = await client.create_access_key(name="John Doe") + key = await client.create_access_key(name="Alice") print(f"Access URL: {key.access_url}") - # Get specific key - retrieved_key = await client.get_access_key(key.id) - - # List all keys - all_keys = await client.get_access_keys() - for k in all_keys.access_keys: - print(f"Key {k.id}: {k.name or 'Unnamed'}") - - # Rename key - await client.rename_access_key(key.id, "John Smith") - - # Delete key - await client.delete_access_key(key.id) -``` - -#### Advanced Key Configuration - -```python -async def advanced_key_config(): - async with AsyncOutlineClient(...) as client: - # Create key with custom configuration - key = await client.create_access_key( - name="Premium User", - port=9999, - method="chacha20-ietf-poly1305", - password="custom-password", - limit=DataLimit(bytes=10 * 1024 ** 3) # 10 GB - ) - - # Create key with specific ID - custom_key = await client.create_access_key_with_id( - "custom-user-id", - name="Custom ID User", - limit=DataLimit(bytes=1024 ** 3) # 1 GB - ) - - # Manage data limits - await client.set_access_key_data_limit(key.id, 20 * 1024 ** 3) # 20 GB - await client.remove_access_key_data_limit(key.id) # Remove limit -``` - -### Batch Operations - -```python -async def batch_key_creation(): - async with AsyncOutlineClient(...) as client: - # Prepare configurations for multiple keys - configs = [ - {"name": "User1", "limit": DataLimit(bytes=1024 ** 3)}, # 1 GB - {"name": "User2", "port": 8388}, - {"name": "User3", "limit": DataLimit(bytes=5 * 1024 ** 3)}, # 5 GB - {"name": "User4", "method": "chacha20-ietf-poly1305"}, - ] - - # Create all keys in batch (fail on first error) - results = await client.batch_create_access_keys(configs, fail_fast=True) - print(f"Created {len(results)} keys successfully") - - # Create keys with error handling (continue on errors) - results = await client.batch_create_access_keys(configs, fail_fast=False) - - successful_keys = [] - failed_keys = [] - - for i, result in enumerate(results): - if isinstance(result, Exception): - failed_keys.append((i, result)) - print(f"Failed to create key {i}: {result}") - else: - successful_keys.append(result) - print(f"Created key: {result.name}") - - print(f"Successfully created: {len(successful_keys)} keys") - print(f"Failed: {len(failed_keys)} keys") -``` - -### Global Data Limits - -```python -async def manage_global_limits(): - async with AsyncOutlineClient(...) as client: - # Set global data limit for all keys - await client.set_global_data_limit(100 * 1024 ** 3) # 100 GB total - - # Remove global limit - await client.remove_global_data_limit() -``` - -### Metrics and Monitoring - -#### Transfer Metrics - -```python -async def monitor_usage(): - async with AsyncOutlineClient(...) as client: - # Enable metrics collection - await client.set_metrics_status(True) - - # Check if metrics are enabled - status = await client.get_metrics_status() - print(f"Metrics enabled: {status.metrics_enabled}") - - # Get transfer metrics - metrics = await client.get_transfer_metrics() - total_bytes = sum(metrics.bytes_transferred_by_user_id.values()) - print(f"Total data transferred: {total_bytes / 1024 ** 3:.2f} GB") - - # Per-user breakdown - for user_id, bytes_used in metrics.bytes_transferred_by_user_id.items(): - gb_used = bytes_used / 1024 ** 3 - print(f"User {user_id}: {gb_used:.2f} GB") -``` - -#### Experimental Metrics - -```python -async def detailed_metrics(): - async with AsyncOutlineClient(...) as client: - # Get detailed server metrics for the last 24 hours - metrics = await client.get_experimental_metrics("24h") - - # Server-level metrics - server_metrics = metrics.server - print(f"Server tunnel time: {server_metrics.tunnel_time.seconds}s") - print(f"Server data transferred: {server_metrics.data_transferred.bytes} bytes") - - # Access key metrics - for key_id, key_metrics in metrics.access_keys.items(): - print(f"Key {key_id}:") - print(f" Tunnel time: {key_metrics.tunnel_time.seconds}s") - print(f" Data transferred: {key_metrics.data_transferred.bytes} bytes") - - # Get metrics for the last 7 days - weekly_metrics = await client.get_experimental_metrics("7d") - - # Get metrics for the last 30 days - monthly_metrics = await client.get_experimental_metrics("30d") - - # Get metrics since a specific timestamp - custom_metrics = await client.get_experimental_metrics("2024-01-01T00:00:00Z") -``` - -### Advanced Configuration - -#### Logging Configuration - -```python -async def configure_logging(): - async with AsyncOutlineClient(...) as client: - # Configure logging level and format - client.configure_logging( - level="DEBUG", - format_string="%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - - # Now all API calls will be logged with debug information - server = await client.get_server_info() -``` - -#### Rate Limiting - -```python -async def rate_limited_client(): - # Client with rate limiting - async with AsyncOutlineClient( - api_url="https://your-server:port/path", - cert_sha256="your-cert-fingerprint", - rate_limit_delay=0.5 # 500ms delay between requests - ) as client: - # Requests will be automatically rate-limited - for i in range(10): - await client.get_server_info() # Each request waits 500ms -``` - -### Error Handling - -```python -from pyoutlineapi import AsyncOutlineClient, APIError, OutlineError - - -async def robust_client(): - try: - async with AsyncOutlineClient( - api_url="https://your-server:port/api", - cert_sha256="your-cert-fingerprint", - retry_attempts=5, # Increase retry attempts - enable_logging=True # Enable logging for debugging - ) as client: - # Check server health first - if not await client.health_check(): - print("Server is not healthy!") - return - - # Your operations here - server = await client.get_server_info() - print(f"Connected to {server.name}") - - except APIError as e: - # Handle API-specific errors (4xx, 5xx responses) - print(f"API Error {e.status_code}: {e.message}") - if e.status_code == 404: - print("Resource not found") - elif e.status_code >= 500: - print("Server error - try again later") - - except OutlineError as e: - # Handle other Outline-specific errors - print(f"Outline Error: {e}") - - except Exception as e: - # Handle unexpected errors - print(f"Unexpected error: {e}") -``` - -### Working with JSON Responses - -```python -async def json_responses(): - # Configure client to return raw JSON instead of Pydantic models - async with AsyncOutlineClient( - api_url="https://your-server:port/api", - cert_sha256="your-cert-fingerprint", - json_format=True # Return JSON dictionaries - ) as client: - server_data = await client.get_server_info() - print(f"Server name: {server_data['name']}") # Access as dict - - keys_data = await client.get_access_keys() - for key in keys_data['accessKeys']: - print(f"Key ID: {key['id']}") - - # Summary also returns JSON format - summary = await client.get_server_summary() - print(f"Healthy: {summary['healthy']}") -``` - -## Advanced Usage - -### Connection Management - -```python -async def manual_session_management(): - # Manual session management (not recommended for most use cases) - client = AsyncOutlineClient( - api_url="https://your-server:port/api", - cert_sha256="your-cert-fingerprint" - ) - - try: - # Manually enter context - await client.__aenter__() - - # Use client - server = await client.get_server_info() - print(f"Connected to {server.name}") - - # Check connection status - print(f"Session active: {client.session and not client.session.closed}") - print(f"API URL: {client.api_url}") - - finally: - # Always clean up - await client.__aexit__(None, None, None) -``` - -### Concurrent Operations - -```python -async def concurrent_operations(): - async with AsyncOutlineClient(...) as client: - # Create multiple keys concurrently - tasks = [ - client.create_access_key(name=f"User {i}") - for i in range(1, 6) - ] - keys = await asyncio.gather(*tasks) - - print(f"Created {len(keys)} keys") - - # Set data limits for all keys concurrently - limit_tasks = [ - client.set_access_key_data_limit(key.id, 5 * 1024 ** 3) - for key in keys - ] - await asyncio.gather(*limit_tasks) - - print("Applied data limits to all keys") -``` - -### Monitoring and Debugging - -```python -async def debug_session(): - async with AsyncOutlineClient( - api_url="https://your-server:port/api", - cert_sha256="your-cert-fingerprint", - enable_logging=True, - timeout=60, # Longer timeout for debugging - retry_attempts=1 # Disable retries for debugging - ) as client: - # Client provides useful debugging information - print(f"Client: {client}") # Shows connection status - print(f"API URL: {client.api_url}") - print(f"Is healthy: {client.is_healthy}") - - # All method calls are logged when logging is enabled - server = await client.get_server_info() - - # Get detailed server summary for monitoring - summary = await client.get_server_summary() - if not summary['healthy']: - print(f"Server error: {summary.get('error')}") -``` - -## Best Practices - -### 1. Always Use Context Managers - -```python -# ✅ Recommended -async with AsyncOutlineClient(...) as client: - await client.get_server_info() - -# ❌ Not recommended -client = AsyncOutlineClient(...) -await client.get_server_info() # Session not initialized -``` - -### 2. Handle Errors Appropriately - -```python -# ✅ Specific error handling -try: - key = await client.get_access_key("nonexistent") -except APIError as e: - if e.status_code == 404: - print("Key not found") - else: - raise # Re-raise unexpected API errors -``` - -### 3. Use Type Hints - -```python -from typing import List -from pyoutlineapi import AccessKey, AsyncOutlineClient - - -async def get_user_keys(client: AsyncOutlineClient) -> List[AccessKey]: - keys = await client.get_access_keys() - return keys.access_keys -``` - -### 4. Configure Timeouts and Retries Appropriately - -```python -# For slow networks or large operations -client = AsyncOutlineClient( - ..., - timeout=60, # 60 second timeout - retry_attempts=5, # More retry attempts - rate_limit_delay=0.1 # Small delay between requests -) -``` - -### 5. Use Batch Operations for Multiple Keys - -```python -# ✅ Efficient batch creation -configs = [{"name": f"User{i}"} for i in range(10)] -keys = await client.batch_create_access_keys(configs) - -# ❌ Inefficient individual creation -keys = [] -for i in range(10): - key = await client.create_access_key(name=f"User{i}") - keys.append(key) -``` + keys = await client.get_access_keys() + print(f"Total keys: {keys.count}") -### 6. Monitor Server Health -```python -# ✅ Check health before operations -async with AsyncOutlineClient(...) as client: - if not await client.health_check(): - print("Server is not responding") - return - - # Proceed with operations - await client.get_server_info() +asyncio.run(main()) ``` -## API Reference - -### Client Methods - -#### Server Management - -- `get_server_info() -> Server | JsonDict` -- `rename_server(name: str) -> bool` -- `set_hostname(hostname: str) -> bool` -- `set_default_port(port: int) -> bool` -- `get_server_summary() -> dict[str, Any]` - Comprehensive server information - -#### Access Key Management - -- `create_access_key(**kwargs) -> AccessKey | JsonDict` -- `create_access_key_with_id(key_id: str, **kwargs) -> AccessKey | JsonDict` -- `get_access_keys() -> AccessKeyList | JsonDict` -- `get_access_key(key_id: str) -> AccessKey | JsonDict` -- `rename_access_key(key_id: str, name: str) -> bool` -- `delete_access_key(key_id: str) -> bool` - -#### Batch Operations - -- `batch_create_access_keys(keys_config: list[dict], fail_fast: bool = True) -> list[AccessKey | Exception]` - -#### Data Limits - -- `set_access_key_data_limit(key_id: str, bytes_limit: int) -> bool` -- `remove_access_key_data_limit(key_id: str) -> bool` -- `set_global_data_limit(bytes_limit: int) -> bool` -- `remove_global_data_limit() -> bool` - -#### Metrics - -- `get_metrics_status() -> MetricsStatusResponse | JsonDict` -- `set_metrics_status(enabled: bool) -> bool` -- `get_transfer_metrics() -> ServerMetrics | JsonDict` -- `get_experimental_metrics(since: str) -> ExperimentalMetrics | JsonDict` - -#### Health and Monitoring - -- `health_check(force: bool = False) -> bool` - Check server health -- `configure_logging(level: str = "INFO", format_string: str = None) -> None` +--- -#### Properties +## Documentation -- `is_healthy: bool` - Last known health status -- `session: Optional[aiohttp.ClientSession]` - Current session -- `api_url: str` - API URL (without sensitive parts) +Detailed examples and guides are in `docs/guides/`. Start with the index: -## Requirements +- [Guides index](docs/guides/README.md) (full list) +- [Access keys](docs/guides/access-keys.md) +- [Server management](docs/guides/server-management.md) +- [Metrics](docs/guides/metrics.md) +- [Enterprise features](docs/guides/enterprise-features.md) +- [Configuration](docs/guides/configuration.md) +- [Error handling](docs/guides/error-handling.md) +- [Best practices](docs/guides/best-practices.md) -- Python 3.10+ -- aiohttp -- pydantic -- A running Outline VPN Server +--- ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## Changelog - -See [CHANGELOG.md](https://github.com/orenlab/pyoutlineapi/blob/main/CHANGELOG.md) for a detailed history of changes. - -## Support - -- 📖 [Documentation](https://orenlab.github.io/pyoutlineapi/) -- 🐛 [Issue Tracker](https://github.com/orenlab/pyoutlineapi/issues) -- 💬 [Discussions](https://github.com/orenlab/pyoutlineapi/discussions) - -## Related Projects - -- [Outline Server](https://github.com/Jigsaw-Code/outline-server) - The Outline VPN Server -- [Outline Client](https://github.com/Jigsaw-Code/outline-client) - Official Outline VPN clients - -## Acknowledgments - -- The Jigsaw team for creating Outline VPN -- All contributors to this project -- The Python async/typing community for inspiration - ---- - -**Made with ❤️ by the PyOutlineAPI team** \ No newline at end of file +MIT. See `LICENSE`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..99f9317 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,1794 @@ +# Security Policy + +## Table of Contents + +- [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities) +- [Security Architecture](#security-architecture) +- [Secure Configuration](#secure-configuration) +- [Certificate Pinning](#certificate-pinning) +- [Credential Management](#credential-management) +- [Network Security](#network-security) +- [Data Protection](#data-protection) +- [Audit Logging](#audit-logging) +- [Circuit Breaker Security](#circuit-breaker-security) +- [Deployment Security](#deployment-security) +- [Dependencies and Updates](#dependencies-and-updates) +- [Security Checklist](#security-checklist) +- [Incident Response](#incident-response) + +--- + +## Reporting Security Vulnerabilities + +We take security seriously. If you discover a security vulnerability in PyOutlineAPI, please report it responsibly. + +### How to Report + +**DO NOT** create a public GitHub issue for security vulnerabilities. + +Instead, please: + +1. **Email us directly**: `pytelemonbot@mail.ru` + - Subject: `[SECURITY] PyOutlineAPI Vulnerability Report` + - Include: Description, reproduction steps, impact assessment, suggested fix + +2. **Response Timeline**: + - ✅ **24 hours**: Initial acknowledgment + - ⚡ **72 hours**: Preliminary assessment and severity classification + - 📋 **7 days**: Detailed response with remediation timeline + - 🔧 **30 days**: Target resolution (varies by complexity and severity) + +3. **Responsible Disclosure**: + - Allow reasonable time to investigate and fix (minimum 90 days) + - Do not publicly disclose until patch is released + - We will credit you in security advisory (unless you prefer anonymity) + - Coordinated disclosure with CVE assignment for critical issues + +### Security Advisory Process + +When a vulnerability is confirmed: + +1. **Assessment**: Severity classification (Critical/High/Medium/Low) +2. **Patch Development**: Fix created and tested +3. **Security Advisory**: Published on GitHub Security Advisories +4. **CVE Assignment**: For vulnerabilities with CVSS score ≥ 4.0 +5. **Release**: Patched version published to PyPI +6. **Notification**: Security advisory sent to users + +--- + +## Security Architecture + +### Defense in Depth + +PyOutlineAPI v0.4.0 implements multiple security layers: + +``` +┌─────────────────────────────────────────┐ +│ Application Layer │ +│ • Input Validation (Pydantic v2) │ +│ • Output Sanitization │ +│ • Audit Logging │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Client Layer │ +│ • Circuit Breaker (Failure Protection) │ +│ • Rate Limiting (DoS Protection) │ +│ • Request Timeout Enforcement │ +│ • Correlation ID Tracking │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Transport Layer │ +│ • TLS 1.2+ Enforcement │ +│ • Certificate Pinning (SHA-256) │ +│ • Connection Pool Management │ +│ • Secure Headers │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Data Layer │ +│ • SecretStr for Credentials │ +│ • Sensitive Data Masking │ +│ • Memory Security (gc clearing) │ +│ • No Secrets in Logs │ +└─────────────────────────────────────────┘ +``` + +### Security Features Summary + +| Feature | Protection Against | Implementation | +|-------------------------|-------------------------|---------------------------------------------| +| **Certificate Pinning** | MITM attacks | SHA-256 fingerprint verification | +| **SecretStr** | Credential exposure | Pydantic SecretStr type | +| **Audit Logging** | Unauthorized actions | Async queue with sanitization | +| **Circuit Breaker** | Service degradation | 3-state circuit with metrics | +| **Rate Limiting** | DoS/resource exhaustion | Semaphore-based concurrency control | +| **Input Validation** | Injection attacks | Pydantic models with strict validation | +| **Output Sanitization** | Information disclosure | Automatic masking of 32+ sensitive patterns | +| **Correlation IDs** | Request tracking/replay | Cryptographically secure tokens | + +--- + +## Secure Configuration + +### Configuration Security Hierarchy (Best to Worst) + +```python +from pyoutlineapi import AsyncOutlineClient, ProductionConfig +from pydantic import SecretStr + +# ✅ BEST: Production config with environment variables +config = ProductionConfig.from_env() +# - Enforces HTTPS +# - Enables circuit breaker +# - Validates all settings +# - Secrets never in code + +async with AsyncOutlineClient(config) as client: + await client.get_server_info() + +# ✅ GOOD: Environment variables with overrides +async with AsyncOutlineClient.from_env( + enable_circuit_breaker=True, + circuit_failure_threshold=5, + enable_logging=False # Disable in production +) as client: + await client.get_server_info() + +# ⚠️ ACCEPTABLE: Direct config with SecretStr +config = OutlineClientConfig( + api_url="https://server.com/path", + cert_sha256=SecretStr("abc123..."), # SecretStr protects from accidental exposure + enable_circuit_breaker=True +) + +# ❌ DANGEROUS: Hardcoded credentials +client = AsyncOutlineClient( + api_url="https://server.com/secret", # Secret visible in code! + cert_sha256="abc123...", # String instead of SecretStr + enable_circuit_breaker=False # No protection +) +``` + +### Environment Variable Configuration + +**Recommended `.env` structure:** + +```bash +# === Required Security Settings === +OUTLINE_API_URL=https://your-server.com:12345/your-secret-path +OUTLINE_CERT_SHA256=your-64-character-sha256-fingerprint + +# === Security Features (Production Defaults) === +OUTLINE_ENABLE_CIRCUIT_BREAKER=true +OUTLINE_CIRCUIT_FAILURE_THRESHOLD=5 +OUTLINE_CIRCUIT_RECOVERY_TIMEOUT=60.0 +OUTLINE_ENABLE_LOGGING=false # CRITICAL: Disable in production + +# === Connection Security === +OUTLINE_TIMEOUT=10 +OUTLINE_RETRY_ATTEMPTS=2 +OUTLINE_MAX_CONNECTIONS=10 +OUTLINE_RATE_LIMIT=50 + +# === Optional Security Hardening === +OUTLINE_USER_AGENT=MySecureApp/1.0 +OUTLINE_JSON_FORMAT=false +``` + +**Critical Security Rules:** + +1. ✅ **Always add `.env` to `.gitignore`** +2. ✅ **Use different credentials per environment** +3. ✅ **Rotate credentials regularly (every 90 days)** +4. ✅ **Use read-only environment variables in production** +5. ❌ **Never commit `.env` files to version control** + +### Configuration Validation + +```python +from pyoutlineapi import OutlineClientConfig, ConfigurationError + + +def validate_production_config(): + """Validate configuration meets security requirements.""" + try: + config = OutlineClientConfig.from_env() + + # Security checks + checks = { + "HTTPS Enforced": config.api_url.startswith("https://"), + "Circuit Breaker Enabled": config.enable_circuit_breaker, + "Logging Disabled": not config.enable_logging, + "Reasonable Timeout": 5 <= config.timeout <= 30, + "Rate Limited": config.rate_limit <= 100, + } + + failed = [name for name, passed in checks.items() if not passed] + + if failed: + raise ConfigurationError( + f"Security validation failed: {', '.join(failed)}" + ) + + return config + + except ConfigurationError as e: + print(f"❌ Configuration error: {e}") + raise +``` + +--- + +## Certificate Pinning + +### Understanding Certificate Pinning + +PyOutlineAPI uses **SHA-256 certificate pinning** to prevent man-in-the-middle (MITM) attacks: + +```python +# How it works: +# 1. Client extracts server's TLS certificate +# 2. Calculates SHA-256 fingerprint +# 3. Compares with configured fingerprint +# 4. Connection ONLY proceeds if they match +# 5. Any mismatch = immediate connection failure + +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + # Certificate automatically verified on every request + server = await client.get_server_info() +``` + +### Obtaining Certificate Fingerprint + +**Method 1: OpenSSL (Recommended)** + +```bash +# Extract SHA-256 fingerprint +echo | openssl s_client -connect your-server.com:12345 2>/dev/null | \ + openssl x509 -fingerprint -sha256 -noout | \ + cut -d'=' -f2 | tr -d ':' | tr '[:upper:]' '[:lower:]' + +# Output: abc123def456... (64 characters) +``` + +**Method 2: Outline Manager** + +The certificate fingerprint is displayed in Outline Manager when you add a server. + +**Method 3: Python Script** + +```python +import ssl +import hashlib +import socket + + +def get_certificate_fingerprint(hostname: str, port: int) -> str: + """Extract SHA-256 fingerprint from server certificate.""" + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + with socket.create_connection((hostname, port)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + cert_der = ssock.getpeercert(binary_form=True) + fingerprint = hashlib.sha256(cert_der).hexdigest() + return fingerprint + + +# Usage +fingerprint = get_certificate_fingerprint("your-server.com", 12345) +print(f"Certificate SHA-256: {fingerprint}") +``` + +### Certificate Rotation + +**When server certificate changes:** + +```python +from pyoutlineapi import AsyncOutlineClient, ConnectionError +import asyncio + + +async def handle_certificate_rotation(): + """Gracefully handle certificate updates.""" + + # Strategy 1: Dual certificate support (recommended) + certificates = [ + os.getenv("OUTLINE_CERT_PRIMARY"), # Current cert + os.getenv("OUTLINE_CERT_BACKUP"), # New cert (during rotation) + ] + + for cert in certificates: + try: + async with AsyncOutlineClient.from_env( + cert_sha256=cert + ) as client: + await client.get_server_info() + print(f"✅ Connected with certificate: {cert[:16]}...") + return client + + except ConnectionError as e: + if "certificate" in str(e).lower(): + print(f"⚠️ Certificate mismatch: {cert[:16]}...") + continue + raise + + raise ConnectionError("All certificates failed validation") + + +# Strategy 2: Automated certificate monitoring +async def monitor_certificate_expiry(): + """Monitor certificate expiration and alert.""" + import ssl + import datetime + + # This would be implemented based on your certificate source + # Alert 30 days before expiration + pass +``` + +**Certificate Rotation Best Practices:** + +1. ✅ Test new certificate in staging first +2. ✅ Deploy new certificate to client before server rotation +3. ✅ Maintain both old and new certificates during transition (24-48h) +4. ✅ Monitor connection failures during rotation window +5. ✅ Document rotation procedure and schedule + +--- + +## Credential Management + +### Secure Credential Storage + +```python +from pyoutlineapi import AsyncOutlineClient +from pydantic import SecretStr +import os +from pathlib import Path + + +# ✅ BEST: Docker Secrets (Orchestrated environments) +def load_from_docker_secret(secret_name: str) -> str: + """Load credential from Docker secret.""" + secret_path = Path(f"/run/secrets/{secret_name}") + if not secret_path.exists(): + raise ValueError(f"Secret not found: {secret_name}") + + # Secrets are read-only, owned by root + return secret_path.read_text().strip() + + +async with AsyncOutlineClient( + api_url=load_from_docker_secret("outline_api_url"), + cert_sha256=load_from_docker_secret("outline_cert_sha256") +) as client: + pass + + +# ✅ GOOD: Environment Variables with Validation +def load_from_env_secure() -> dict: + """Load and validate credentials from environment.""" + api_url = os.getenv("OUTLINE_API_URL") + cert = os.getenv("OUTLINE_CERT_SHA256") + + if not api_url or not cert: + raise ValueError("Missing required credentials") + + # Validate format + if not api_url.startswith("https://"): + raise ValueError("API URL must use HTTPS") + + if len(cert) != 64 or not all(c in "0123456789abcdef" for c in cert.lower()): + raise ValueError("Invalid certificate fingerprint format") + + return {"api_url": api_url, "cert_sha256": cert} + + +# ✅ ACCEPTABLE: Vault/KMS Integration +async def load_from_vault(): + """Load credentials from HashiCorp Vault or AWS Secrets Manager.""" + # Example with hvac (HashiCorp Vault client) + import hvac + + client = hvac.Client(url=os.getenv("VAULT_ADDR")) + client.auth.approle.login( + role_id=os.getenv("VAULT_ROLE_ID"), + secret_id=os.getenv("VAULT_SECRET_ID") + ) + + secret = client.secrets.kv.v2.read_secret_version( + path="outline/production" + ) + + return { + "api_url": secret["data"]["data"]["api_url"], + "cert_sha256": secret["data"]["data"]["cert_sha256"] + } + + +# ❌ NEVER: Hardcoded Credentials +API_URL = "https://prod.example.com/secret123" # NEVER DO THIS! +CERT = "abc123..." # NEVER DO THIS! +``` + +### Credential Rotation + +```python +import asyncio +from datetime import datetime, timedelta + + +class CredentialRotator: + """Automated credential rotation handler.""" + + def __init__(self, rotation_interval_days: int = 90): + self.rotation_interval = timedelta(days=rotation_interval_days) + self.last_rotation = datetime.now() + + async def check_rotation_needed(self) -> bool: + """Check if credentials need rotation.""" + time_since_rotation = datetime.now() - self.last_rotation + return time_since_rotation >= self.rotation_interval + + async def rotate_credentials(self): + """Rotate credentials with zero downtime.""" + # 1. Generate new credentials + new_creds = await self.generate_new_credentials() + + # 2. Update service with new credentials + await self.update_service_credentials(new_creds) + + # 3. Wait for propagation (grace period) + await asyncio.sleep(60) + + # 4. Test new credentials + if await self.test_credentials(new_creds): + # 5. Update environment/secrets manager + await self.update_stored_credentials(new_creds) + + # 6. Mark rotation complete + self.last_rotation = datetime.now() + return True + + # Rollback if test fails + await self.rollback_credentials() + return False +``` + +### Access Key Security + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.models import DataLimit +import secrets +import hashlib + + +async def create_secure_access_key(): + """Create access key with security best practices.""" + + async with AsyncOutlineClient.from_env() as client: + # ✅ SECURE: Generate cryptographically secure key name + key_name = hashlib.sha256( + secrets.token_bytes(32) + ).hexdigest()[:16] + + # ✅ SECURE: Set appropriate data limit + key = await client.create_access_key( + name=f"user_{key_name}", + method="chacha20-ietf-poly1305", # Strong encryption + limit=DataLimit.from_gigabytes(10) # Enforce limits + ) + + # ✅ SECURE: Log creation without sensitive data + from pyoutlineapi import get_default_audit_logger + logger = get_default_audit_logger() + logger.log_action( + action="create_key", + resource=key.id, + details={"name": key.name, "has_limit": True} + ) + + return key + + +# ❌ INSECURE: Predictable patterns +await client.create_access_key(name="user1") # Sequential +await client.create_access_key(name="admin") # Reveals purpose +await client.create_access_key(name="test123") # Predictable + +# ❌ INSECURE: No limits +await client.create_access_key(name="unlimited") # Can exhaust resources +``` + +--- + +## Network Security + +### TLS Configuration + +```python +from pyoutlineapi import OutlineClientConfig, AsyncOutlineClient +from pydantic import SecretStr + +# ✅ SECURE: Enforce TLS 1.2+ (automatic in PyOutlineAPI) +config = OutlineClientConfig( + api_url="https://server.com/path", # HTTPS enforced + cert_sha256=SecretStr("abc123..."), + timeout=10, # Prevent slowloris attacks + max_connections=10, # Limit resource usage + rate_limit=50, # Prevent DoS +) + +# Production config automatically enforces HTTPS +from pyoutlineapi import ProductionConfig + +prod_config = ProductionConfig.from_env() +# - Raises error if HTTP is used +# - Enforces certificate pinning +# - Enables circuit breaker +``` + +### Rate Limiting (DoS Protection) + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env( + rate_limit=50, # Max 50 concurrent requests + max_connections=20, # Connection pool limit +) as client: + # Check rate limiter status + stats = client.get_rate_limiter_stats() + print(f"Active: {stats['active']}/{stats['limit']}") + print(f"Available: {stats['available']}") + + # Dynamic adjustment based on load + if stats['available'] < 5: + await client.set_rate_limit(100) # Increase temporarily + + # Monitor for abuse + if client.active_requests > stats['limit'] * 0.9: + print("⚠️ WARNING: High request rate detected") +``` + +### Request Timeout Protection + +```python +from pyoutlineapi import AsyncOutlineClient, TimeoutError + + +async def timeout_protected_operation(): + """Demonstrate timeout protection.""" + + async with AsyncOutlineClient.from_env(timeout=10) as client: + try: + # Operation automatically times out after 10s + server = await client.get_server_info() + + except TimeoutError as e: + print(f"Operation timed out after {e.timeout}s") + print(f"Operation: {e.operation}") + # Implement retry or fallback logic +``` + +### Network Isolation + +```yaml +# docker-compose.yml with network security +version: '3.8' + +services: + outline-manager: + image: myapp:latest + networks: + - internal + - outline_net + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + +networks: + internal: + driver: bridge + internal: true # No external access + + outline_net: + driver: bridge + ipam: + config: + - subnet: 172.28.0.0/16 +``` + +--- + +## Data Protection + +### Sensitive Data Handling + +```python +from pyoutlineapi.common_types import mask_sensitive_data, DEFAULT_SENSITIVE_KEYS + +# Automatic sensitive data masking +sensitive_data = { + "name": "Alice", + "password": "secret123", + "api_url": "https://server.com/secret", + "cert_sha256": "abc123...", + "user_id": "12345" +} + +# ✅ SECURE: Mask before logging +safe_data = mask_sensitive_data(sensitive_data) +print(safe_data) +# { +# "name": "Alice", +# "password": "***MASKED***", +# "api_url": "***MASKED***", +# "cert_sha256": "***MASKED***", +# "user_id": "12345" +# } + +# 32+ sensitive patterns automatically detected: +# password, passwd, pwd, secret, api_key, token, cert, private_key, etc. +``` + +### Memory Security + +```python +import gc +from pyoutlineapi import AsyncOutlineClient + + +async def memory_secure_operation(): + """Ensure sensitive data is cleared from memory.""" + + api_url = os.getenv("OUTLINE_API_URL") + cert = os.getenv("OUTLINE_CERT_SHA256") + + try: + async with AsyncOutlineClient( + api_url=api_url, + cert_sha256=cert + ) as client: + result = await client.get_server_info() + return result + + finally: + # ✅ SECURE: Clear sensitive variables + api_url = None + cert = None + + # Force garbage collection + gc.collect() +``` + +### Output Sanitization + +```python +from pyoutlineapi import AsyncOutlineClient + + +async def sanitized_output_example(): + """Demonstrate automatic output sanitization.""" + + async with AsyncOutlineClient.from_env() as client: + key = await client.create_access_key(name="Alice") + + # ✅ SECURE: Get sanitized config for logging + safe_config = client.get_sanitized_config() + logger.info(f"Using config: {safe_config}") + # Output: {'api_url': 'https://server.com/***', 'cert_sha256': '***MASKED***', ...} + + # ✅ SECURE: Safe string representation + print(client) + # Output: AsyncOutlineClient(host=https://server.com, status=connected) + + # ❌ NEVER: Log full key object + # logger.info(f"Created key: {key}") # Would expose access_url! + + # ✅ SECURE: Log only safe fields + logger.info(f"Created key ID: {key.id}, name: {key.name}") +``` + +--- + +## Audit Logging + +### Production Audit Logging + +```python +from pyoutlineapi import DefaultAuditLogger, AsyncOutlineClient + +# ✅ PRODUCTION: Async audit logger with queue +audit_logger = DefaultAuditLogger( + enable_async=True, # Non-blocking queue processing + queue_size=5000 # Large queue for high throughput +) + +async with AsyncOutlineClient.from_env(audit_logger=audit_logger) as client: + # All operations automatically audited + key = await client.create_access_key(name="Alice") + # 📝 [AUDIT] create_access_key on {key.id} | {'name': 'Alice', 'success': True} + + await client.rename_access_key(key.id, "Alice Smith") + # 📝 [AUDIT] rename_access_key on {key.id} | {'new_name': 'Alice Smith', 'success': True} + + try: + await client.delete_access_key("invalid-id") + except Exception: + pass + # 📝 [AUDIT] delete_access_key on invalid-id | {'success': False, 'error': '...'} + +# Graceful shutdown with queue draining +await audit_logger.shutdown(timeout=5.0) +``` + +### Custom Audit Logger (SIEM Integration) + +```python +from pyoutlineapi import AuditLogger +import json +import asyncio +import aiohttp + + +class SIEMauditLogger: + """Send audit logs to SIEM system.""" + + def __init__(self, siem_endpoint: str, api_key: str): + self.siem_endpoint = siem_endpoint + self.api_key = api_key + self.session = None + + def log_action(self, action: str, resource: str, **kwargs) -> None: + """Synchronous logging (for compatibility).""" + asyncio.create_task(self.alog_action(action, resource, **kwargs)) + + async def alog_action(self, action: str, resource: str, **kwargs) -> None: + """Async logging to SIEM.""" + if not self.session: + self.session = aiohttp.ClientSession() + + event = { + "timestamp": time.time(), + "service": "pyoutlineapi", + "action": action, + "resource": resource, + "severity": "INFO" if kwargs.get("success") else "WARNING", + **kwargs + } + + try: + async with self.session.post( + self.siem_endpoint, + json=event, + headers={"Authorization": f"Bearer {self.api_key}"} + ) as resp: + if resp.status != 200: + print(f"SIEM logging failed: {resp.status}") + + except Exception as e: + print(f"SIEM logging error: {e}") + + async def shutdown(self) -> None: + """Cleanup.""" + if self.session: + await self.session.close() + + +# Usage +siem_logger = SIEMauditLogger( + siem_endpoint="https://siem.company.com/events", + api_key=os.getenv("SIEM_API_KEY") +) + +async with AsyncOutlineClient.from_env(audit_logger=siem_logger) as client: + await client.create_access_key(name="User") +``` + +### Audit Log Security + +```python +# ✅ SECURE: Sensitive data automatically filtered +key = await client.create_access_key( + name="Alice", + password="secret123" # Automatically masked in audit logs +) +# Audit log: {'name': 'Alice', 'password': '***REDACTED***', 'success': True} + +# ✅ SECURE: Correlation IDs for request tracking +from pyoutlineapi.base_client import correlation_id + +correlation_id.set("request-abc-123") +await client.create_access_key(name="Bob") +# Audit log includes correlation ID for tracing + +# ✅ SECURE: Failed operations logged +try: + await client.delete_access_key("non-existent") +except Exception: + pass +# Audit log: {'success': False, 'error': 'Key not found', 'error_type': 'APIError'} +``` + +--- + +## Circuit Breaker Security + +### Preventing Cascading Failures + +```python +from pyoutlineapi import AsyncOutlineClient, CircuitOpenError + +# ✅ PRODUCTION: Enable circuit breaker +async with AsyncOutlineClient.from_env( + enable_circuit_breaker=True, + circuit_failure_threshold=5, # Open after 5 failures + circuit_recovery_timeout=60.0, # Test recovery after 60s + circuit_success_threshold=2, # Close after 2 successes + circuit_call_timeout=10.0 # Individual call timeout +) as client: + try: + await client.get_server_info() + + except CircuitOpenError as e: + # ✅ SECURE: Circuit prevents hammering failing service + print(f"Circuit open - service degraded") + print(f"Retry after: {e.retry_after}s") + + # Implement fallback or alert + await notify_operations_team( + "Outline service circuit breaker opened" + ) + + # Wait before retry + await asyncio.sleep(e.retry_after) + + # Monitor circuit health + metrics = client.get_circuit_metrics() + if metrics: + if metrics['state'] == 'OPEN': + print("⚠️ CRITICAL: Circuit breaker is OPEN") + elif metrics['state'] == 'HALF_OPEN': + print("⚠️ WARNING: Circuit breaker testing recovery") + + if metrics['success_rate'] < 0.5: + print("⚠️ WARNING: Low success rate detected") +``` + +### Circuit Breaker Monitoring + +```python +async def monitor_circuit_breaker(client: AsyncOutlineClient): + """Monitor circuit breaker for security incidents.""" + + while True: + metrics = client.get_circuit_metrics() + + if not metrics: + await asyncio.sleep(10) + continue + + # Security alerts + if metrics['state'] == 'OPEN': + await send_alert( + severity="HIGH", + message="Circuit breaker opened - service degraded", + metrics=metrics + ) + + if metrics['failed_calls'] > 100: + await send_alert( + severity="MEDIUM", + message=f"High failure count: {metrics['failed_calls']}", + metrics=metrics + ) + + if metrics['success_rate'] < 0.5: + await send_alert( + severity="MEDIUM", + message=f"Low success rate: {metrics['success_rate']:.2%}", + metrics=metrics + ) + + await asyncio.sleep(30) +``` + +--- + +## Deployment Security + +### Docker Security + +**Secure Dockerfile:** + +```dockerfile +# Use specific version (not 'latest') +FROM python:3.12-slim-bookworm + +# Create non-root user +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Set secure working directory +WORKDIR /app + +# Install dependencies as root +COPY requirements.txt . +RUN pip install --no-cache-dir --require-hashes -r requirements.txt + +# Copy application +COPY --chown=appuser:appuser . . + +# Switch to non-root user +USER appuser + +# Security hardening +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONH + +ASHSEED=random + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD python -c "import asyncio; from app import health_check; exit(0 if asyncio.run(health_check()) else 1)" + +# Run application +CMD ["python", "-u", "app.py"] +``` + +**Secure docker-compose.yml:** + +```yaml +version: '3.8' + +services: + outline-manager: + build: . + + # Use secrets (not environment variables) + secrets: + - outline_api_url + - outline_cert_sha256 + + environment: + - OUTLINE_API_URL_FILE=/run/secrets/outline_api_url + - OUTLINE_CERT_SHA256_FILE=/run/secrets/outline_cert_sha256 + - OUTLINE_ENABLE_CIRCUIT_BREAKER=true + - OUTLINE_ENABLE_LOGGING=false + + # Security constraints + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + tmpfs: + - /tmp:noexec,nosuid,size=100M + # Resource limits (prevent DoS) + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + + # Network isolation + networks: + - internal + + # Restart policy + restart: unless-stopped + + # Health check + healthcheck: + test: [ "CMD", "python", "-c", "import asyncio; from app import health_check; exit(0 if asyncio.run(health_check()) else 1)" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + # Logging + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + labels: "service,environment" + +secrets: + outline_api_url: + external: true + outline_cert_sha256: + external: true + +networks: + internal: + driver: bridge + internal: false + ipam: + config: + - subnet: 172.28.0.0/16 +``` + +### Kubernetes Security + +**Secure Kubernetes Deployment:** + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: outline-credentials +type: Opaque +stringData: + api-url: "https://your-server.com:12345/path" + cert-sha256: "your-certificate-fingerprint" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: outline-manager + labels: + app: outline-manager +spec: + replicas: 2 + selector: + matchLabels: + app: outline-manager + template: + metadata: + labels: + app: outline-manager + spec: + # Security context + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + + containers: + - name: outline-manager + image: myregistry/outline-manager:1.0.0 + imagePullPolicy: Always + + # Security context for container + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + capabilities: + drop: + - ALL + + # Environment from secrets + env: + - name: OUTLINE_API_URL + valueFrom: + secretKeyRef: + name: outline-credentials + key: api-url + - name: OUTLINE_CERT_SHA256 + valueFrom: + secretKeyRef: + name: outline-credentials + key: cert-sha256 + - name: OUTLINE_ENABLE_CIRCUIT_BREAKER + value: "true" + - name: OUTLINE_ENABLE_LOGGING + value: "false" + + # Resource limits + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "100m" + memory: "128Mi" + + # Health checks + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 2 + + # Volume mounts for tmp + volumeMounts: + - name: tmp + mountPath: /tmp + + volumes: + - name: tmp + emptyDir: + sizeLimit: 100Mi + + # Pod security + automountServiceAccountToken: false +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: outline-manager-pdb +spec: + minAvailable: 1 + selector: + matchLabels: + app: outline-manager +--- +apiVersion: v1 +kind: Service +metadata: + name: outline-manager +spec: + type: ClusterIP + selector: + app: outline-manager + ports: + - port: 8080 + targetPort: 8080 +``` + +### Secrets Management + +**HashiCorp Vault Integration:** + +```python +import hvac +from pyoutlineapi import AsyncOutlineClient + + +class VaultSecretLoader: + """Load secrets from HashiCorp Vault.""" + + def __init__(self, vault_addr: str, vault_token: str): + self.client = hvac.Client(url=vault_addr, token=vault_token) + + def get_outline_credentials(self, path: str) -> dict: + """Retrieve Outline credentials from Vault.""" + try: + secret = self.client.secrets.kv.v2.read_secret_version(path=path) + data = secret['data']['data'] + + return { + 'api_url': data['api_url'], + 'cert_sha256': data['cert_sha256'] + } + except Exception as e: + raise ValueError(f"Failed to load secrets from Vault: {e}") + + +# Usage +vault = VaultSecretLoader( + vault_addr=os.getenv("VAULT_ADDR"), + vault_token=os.getenv("VAULT_TOKEN") +) + +creds = vault.get_outline_credentials("secret/outline/production") + +async with AsyncOutlineClient( + api_url=creds['api_url'], + cert_sha256=creds['cert_sha256'] +) as client: + await client.get_server_info() +``` + +**AWS Secrets Manager Integration:** + +```python +import boto3 +import json +from pyoutlineapi import AsyncOutlineClient + + +class AWSSecretLoader: + """Load secrets from AWS Secrets Manager.""" + + def __init__(self, region: str = 'us-east-1'): + self.client = boto3.client('secretsmanager', region_name=region) + + def get_outline_credentials(self, secret_name: str) -> dict: + """Retrieve Outline credentials from AWS Secrets Manager.""" + try: + response = self.client.get_secret_value(SecretId=secret_name) + secret = json.loads(response['SecretString']) + + return { + 'api_url': secret['api_url'], + 'cert_sha256': secret['cert_sha256'] + } + except Exception as e: + raise ValueError(f"Failed to load secrets from AWS: {e}") + + +# Usage +aws_secrets = AWSSecretLoader(region='us-east-1') +creds = aws_secrets.get_outline_credentials("outline/production/credentials") + +async with AsyncOutlineClient( + api_url=creds['api_url'], + cert_sha256=creds['cert_sha256'] +) as client: + await client.get_server_info() +``` + +--- + +## Dependencies and Updates + +### Dependency Security Scanning + +```bash +# Install security tools +pip install safety pip-audit bandit + +# Check for known vulnerabilities +safety check --json + +# Audit dependencies +pip-audit --desc + +# Security linting +bandit -r pyoutlineapi/ -f json + +# Generate SBOM (Software Bill of Materials) +pip install cyclonedx-bom +cyclonedx-py requirements.txt -o sbom.json +``` + +**Automated Security Scanning in CI/CD:** + +```yaml +# .github/workflows/security.yml +name: Security Scan + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + schedule: + - cron: '0 0 * * 0' # Weekly + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + pip install safety pip-audit bandit + pip install -r requirements.txt + + - name: Run Safety check + run: safety check --json + + - name: Run pip-audit + run: pip-audit --desc + + - name: Run Bandit + run: bandit -r pyoutlineapi/ -f json -o bandit-report.json + + - name: Upload security reports + uses: actions/upload-artifact@v4 + with: + name: security-reports + path: | + bandit-report.json + safety-report.json +``` + +### Update Management + +```python +# check_updates.py +import aiohttp +import asyncio +from packaging import version +import pyoutlineapi + + +async def check_pyoutlineapi_updates(): + """Check for security updates.""" + + async with aiohttp.ClientSession() as session: + async with session.get('https://pypi.org/pypi/pyoutlineapi/json') as resp: + if resp.status != 200: + print("❌ Failed to check for updates") + return + + data = await resp.json() + latest = data['info']['version'] + current = pyoutlineapi.__version__ + + if version.parse(current) < version.parse(latest): + print(f"⚠️ Update available: {latest} (current: {current})") + + # Check for security releases + releases = data['releases'].get(latest, []) + for release in releases: + if 'security' in release.get('comment_text', '').lower(): + print("🚨 SECURITY UPDATE - Update immediately!") + return True + + print("ℹ️ Regular update available") + return True + + print(f"✅ Up to date: {current}") + return False + + +# Run check +if __name__ == "__main__": + asyncio.run(check_pyoutlineapi_updates()) +``` + +### Dependency Pinning + +**requirements.txt with hashes:** + +```text +# requirements.txt +# Generated with: pip-compile --generate-hashes requirements.in + +pyoutlineapi==0.4.0 \ + --hash=sha256:abc123... \ + --hash=sha256:def456... + +aiohttp==3.9.1 \ + --hash=sha256:123abc... \ + --hash=sha256:456def... + +pydantic==2.5.3 \ + --hash=sha256:789ghi... \ + --hash=sha256:012jkl... + +# Verify with: pip install --require-hashes -r requirements.txt +``` + +--- + +## Security Checklist + +### Pre-Deployment Security Checklist + +#### Configuration Security + +- [ ] ✅ Environment variables configured (no hardcoded credentials) +- [ ] ✅ `.env` file added to `.gitignore` +- [ ] ✅ Production config uses `ProductionConfig` preset +- [ ] ✅ HTTPS enforced (no HTTP connections) +- [ ] ✅ Certificate fingerprint verified and correct +- [ ] ✅ Logging disabled in production (`enable_logging=false`) +- [ ] ✅ Circuit breaker enabled (`enable_circuit_breaker=true`) +- [ ] ✅ Reasonable timeouts configured (5-30 seconds) +- [ ] ✅ Rate limiting configured (≤100 concurrent requests) + +#### Credential Management + +- [ ] ✅ Credentials stored in secrets manager (Vault/AWS/Docker secrets) +- [ ] ✅ Credentials rotated within last 90 days +- [ ] ✅ Separate credentials for each environment (dev/staging/prod) +- [ ] ✅ Credential rotation procedure documented +- [ ] ✅ Emergency credential revocation process in place + +#### Network Security + +- [ ] ✅ TLS 1.2+ enforced +- [ ] ✅ Certificate pinning enabled +- [ ] ✅ Firewall rules configured (allowlist only) +- [ ] ✅ Network isolation implemented (internal networks) +- [ ] ✅ DDoS protection configured (rate limiting) + +#### Access Control + +- [ ] ✅ Access keys have appropriate data limits +- [ ] ✅ Key naming conventions follow security guidelines +- [ ] ✅ Regular key rotation schedule established +- [ ] ✅ Unused keys identified and removed +- [ ] ✅ Key usage monitoring enabled + +#### Audit & Monitoring + +- [ ] ✅ Audit logging enabled in production +- [ ] ✅ Audit logs sent to SIEM/centralized logging +- [ ] ✅ Sensitive data filtering active +- [ ] ✅ Correlation IDs tracked for requests +- [ ] ✅ Security alerts configured +- [ ] ✅ Health monitoring enabled +- [ ] ✅ Circuit breaker metrics tracked + +#### Code Security + +- [ ] ✅ Dependencies scanned for vulnerabilities (`safety check`) +- [ ] ✅ Static analysis passed (`bandit -r pyoutlineapi/`) +- [ ] ✅ No secrets in code or version control +- [ ] ✅ Input validation enabled (automatic with Pydantic) +- [ ] ✅ Output sanitization enabled (automatic) +- [ ] ✅ Security tests in CI/CD pipeline + +#### Container/Deployment Security + +- [ ] ✅ Running as non-root user +- [ ] ✅ Read-only filesystem enabled +- [ ] ✅ Capabilities dropped (`cap_drop: ALL`) +- [ ] ✅ Security options configured (`no-new-privileges`) +- [ ] ✅ Resource limits set (CPU/memory) +- [ ] ✅ Health checks configured +- [ ] ✅ Secrets mounted securely (not in environment) + +### Runtime Security Checklist + +#### Connection Health + +- [ ] ✅ Health checks passing consistently +- [ ] ✅ Certificate validation successful +- [ ] ✅ No connection timeouts or errors +- [ ] ✅ Circuit breaker in CLOSED state +- [ ] ✅ Success rate >95% + +#### Access Key Management + +- [ ] ✅ Regular usage monitoring active +- [ ] ✅ Unused keys cleaned up monthly +- [ ] ✅ Data limits enforced +- [ ] ✅ No keys with unlimited access +- [ ] ✅ Key creation/deletion audited + +#### Monitoring & Alerting + +- [ ] ✅ Log monitoring active +- [ ] ✅ No sensitive data in logs +- [ ] ✅ Security alerts configured +- [ ] ✅ Anomaly detection enabled +- [ ] ✅ On-call rotation established + +#### Compliance + +- [ ] ✅ Audit logs retained per policy (90+ days) +- [ ] ✅ Access reviews completed quarterly +- [ ] ✅ Security training current +- [ ] ✅ Incident response plan tested +- [ ] ✅ Compliance certifications current + +--- + +## Incident Response + +### Incident Classification + +| Severity | Description | Response Time | Examples | +|-------------------|-------------------------|---------------|------------------------------------| +| **P0 - Critical** | Active security breach | Immediate | Credential compromise, data breach | +| **P1 - High** | Potential security risk | < 4 hours | Suspicious activity, failed logins | +| **P2 - Medium** | Security concern | < 24 hours | Configuration drift, outdated deps | +| **P3 - Low** | Minor security issue | < 7 days | Best practice violations | + +### Security Incident Response Plan + +**Phase 1: Detection & Assessment (0-15 minutes)** + +```python +async def detect_security_incident(client: AsyncOutlineClient): + """Automated security incident detection.""" + + # Check circuit breaker state + metrics = client.get_circuit_metrics() + if metrics and metrics['state'] == 'OPEN': + await alert_security_team( + severity="HIGH", + message="Circuit breaker opened - possible DoS or service failure" + ) + + # Check for unusual key creation patterns + keys = await client.get_access_keys() + recent_keys = [k for k in keys.access_keys + if is_recently_created(k, hours=1)] + + if len(recent_keys) > 10: # Threshold + await alert_security_team( + severity="HIGH", + message=f"Unusual key creation: {len(recent_keys)} keys in last hour" + ) + + # Check for excessive data usage + if client.is_connected: + metrics = await client.get_transfer_metrics() + for key_id, bytes_used in metrics.bytes_transferred_by_user_id.items(): + if bytes_used > 100 * 1024 ** 3: # 100 GB threshold + await alert_security_team( + severity="MEDIUM", + message=f"High data usage detected: Key {key_id}" + ) +``` + +**Phase 2: Containment (15-60 minutes)** + +```python +async def contain_security_incident(client: AsyncOutlineClient): + """Immediate containment actions.""" + + # 1. Rotate credentials + print("Step 1: Rotating credentials...") + await rotate_outline_credentials() + + # 2. Disable suspicious keys + print("Step 2: Disabling suspicious access keys...") + suspicious_keys = await identify_suspicious_keys(client) + for key_id in suspicious_keys: + await client.delete_access_key(key_id) + print(f" ✅ Disabled key: {key_id}") + + # 3. Enable additional monitoring + print("Step 3: Enhanced monitoring enabled...") + await enable_enhanced_monitoring() + + # 4. Notify stakeholders + print("Step 4: Notifying stakeholders...") + await notify_security_team({ + "incident": "Security incident contained", + "actions_taken": [ + "Credentials rotated", + f"Disabled {len(suspicious_keys)} suspicious keys", + "Enhanced monitoring enabled" + ] + }) +``` + +**Phase 3: Eradication (1-24 hours)** + +```python +async def eradicate_threat(client: AsyncOutlineClient): + """Remove threat completely.""" + + # 1. Full audit of all access keys + print("Conducting full access key audit...") + keys = await client.get_access_keys() + + audit_results = [] + for key in keys.access_keys: + status = await audit_access_key(key) + audit_results.append(status) + + if status['risk_level'] == 'HIGH': + await client.delete_access_key(key.id) + print(f" ⚠️ Removed high-risk key: {key.id}") + + # 2. Review and update security policies + await update_security_policies() + + # 3. Patch vulnerabilities + await apply_security_patches() + + # 4. Verify system integrity + integrity_check = await verify_system_integrity(client) + if not integrity_check: + raise SecurityError("System integrity compromised") +``` + +**Phase 4: Recovery (24-72 hours)** + +```python +async def recover_from_incident(): + """Restore normal operations securely.""" + + # 1. Restore from clean backup if needed + if backup_required: + await restore_from_backup() + + # 2. Recreate legitimate access keys + print("Recreating legitimate access keys...") + await recreate_access_keys_from_approved_list() + + # 3. Update documentation + await update_security_documentation() + + # 4. Conduct post-incident review + await schedule_post_incident_review() +``` + +**Phase 5: Lessons Learned (7 days)** + +```markdown +## Post-Incident Review Template + +### Incident Summary + +- **Date/Time**: +- **Duration**: +- **Severity**: +- **Impact**: + +### Timeline + +- **Detection**: +- **Containment**: +- **Eradication**: +- **Recovery**: + +### Root Cause Analysis + +1. What happened? +2. Why did it happen? +3. How was it detected? + +### Actions Taken + +- [ ] Immediate response +- [ ] Containment measures +- [ ] System recovery + +### Lessons Learned + +1. What worked well? +2. What could be improved? +3. What should we do differently? + +### Action Items + +- [ ] Update security policies +- [ ] Implement additional monitoring +- [ ] Security training +- [ ] Tool/process improvements + +### Follow-up + +- **Review Date**: +- **Owner**: +- **Status**: +``` + +### Emergency Contacts + +**Security Team Contacts:** + +```python +SECURITY_CONTACTS = { + "primary": { + "email": "security@company.com", + "phone": "+1-XXX-XXX-XXXX", + "pagerduty": "security-team" + }, + "escalation": { + "email": "ciso@company.com", + "phone": "+1-XXX-XXX-XXXX" + }, + "vendor": { + "email": "pytelemonbot@mail.ru", + "github": "https://github.com/orenlab/pyoutlineapi/security" + } +} +``` + +### Incident Communication Template + +```python +async def send_security_incident_notification( + severity: str, + title: str, + description: str, + actions_taken: list[str] +): + """Send standardized security incident notification.""" + + message = f""" +🚨 SECURITY INCIDENT - {severity} + +Title: {title} +Time: {datetime.now().isoformat()} +Severity: {severity} + +Description: +{description} + +Actions Taken: +{chr(10).join(f"- {action}" for action in actions_taken)} + +Status: Under Investigation + +Contact: security@company.com +Incident ID: INC-{datetime.now().strftime('%Y%m%d-%H%M%S')} + """ + + # Send via multiple channels + await send_email(to=SECURITY_CONTACTS['primary']['email'], body=message) + await send_slack_alert(channel='#security-incidents', message=message) + await create_pagerduty_incident(severity=severity, message=message) +``` + +--- + +## Additional Resources + +### Security Standards & Frameworks + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework) +- [CIS Controls](https://www.cisecurity.org/controls) +- [ISO 27001](https://www.iso.org/isoiec-27001-information-security.html) + +### Python Security + +- [Python Security Best Practices](https://python-security.readthedocs.io/) +- [Bandit Security Linter](https://bandit.readthedocs.io/) +- [Safety - Dependency Scanner](https://pyup.io/safety/) +- [OWASP Python Security Project](https://owasp.org/www-project-python-security/) + +### TLS & Certificate Security + +- [TLS Best Practices](https://wiki.mozilla.org/Security/Server_Side_TLS) +- [Certificate Pinning Guide](https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning) +- [Let's Encrypt Documentation](https://letsencrypt.org/docs/) + +### Outline VPN Security + +- [Outline Server Security](https://github.com/Jigsaw-Code/outline-server/blob/master/docs/security.md) +- [Outline Documentation](https://getoutline.org/support/) + +### Container Security + +- [Docker Security Best Practices](https://docs.docker.com/engine/security/) +- [Kubernetes Security Best Practices](https://kubernetes.io/docs/concepts/security/) +- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker) + +### Secrets Management + +- [HashiCorp Vault](https://www.vaultproject.io/) +- [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) +- [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) +- [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) + +--- + +## Version History + +| Version | Date | Changes | +|---------|------------|------------------------------------| +| 1.0.0 | 2026-01-30 | Initial security policy for v0.4.0 | + +--- + +**Last Updated**: 2026-01-30 +**Next Review**: 2026-04-30 (Quarterly review) + +For security questions or to report vulnerabilities, contact: `pytelemonbot@mail.ru` + +--- + +**Made with 🔒 by the PyOutlineAPI Security Team** + +*Protecting your Outline VPN infrastructure with enterprise-grade security* \ No newline at end of file diff --git a/codeclone.baseline.json b/codeclone.baseline.json new file mode 100644 index 0000000..9ababe7 --- /dev/null +++ b/codeclone.baseline.json @@ -0,0 +1,17 @@ +{ + "meta": { + "generator": { + "name": "codeclone", + "version": "1.4.0" + }, + "schema_version": "1.0", + "fingerprint_version": "1", + "python_tag": "cp313", + "created_at": "2026-02-13T09:32:08Z", + "payload_sha256": "07a383c1d0974593c83ac30430aec9b99d89fe50f640a9b3b433658e0bd029e8" + }, + "clones": { + "functions": [], + "blocks": [] + } +} diff --git a/docs/guides/README.md b/docs/guides/README.md new file mode 100644 index 0000000..9f82411 --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,14 @@ +# Guides + +Index of detailed documentation and examples. + +- [Access keys](access-keys.md) +- [Server management](server-management.md) +- [Metrics](metrics.md) +- [Enterprise features](enterprise-features.md) +- [Configuration](configuration.md) +- [Error handling](error-handling.md) +- [Advanced usage](advanced-usage.md) +- [Best practices](best-practices.md) +- [Docker](docker.md) +- [Production example](production-example.md) diff --git a/docs/guides/access-keys.md b/docs/guides/access-keys.md new file mode 100644 index 0000000..8fb1a83 --- /dev/null +++ b/docs/guides/access-keys.md @@ -0,0 +1,60 @@ +# Access Key Management + +Practical guide for creating and managing access keys. + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Create keys + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.models import DataLimit + +# Create key with data limit +key = await client.create_access_key( + name="Alice", + limit=DataLimit.from_gigabytes(10), +) + +# Create with custom settings +key = await client.create_access_key( + name="Bob", + port=8388, + method="chacha20-ietf-poly1305", + password="custom-password", +) + +# Create with specific ID +key = await client.create_access_key_with_id( + key_id="user-001", + name="Charlie", +) +``` + +## List and filter keys + +```python +keys = await client.get_access_keys() +limited_keys = keys.filter_with_limits() +unlimited_keys = keys.filter_without_limits() +alice_keys = keys.get_by_name("Alice") +key = keys.get_by_id("1") +``` + +## Update and delete keys + +```python +await client.rename_access_key("1", "Alice Smith") +await client.set_access_key_data_limit("1", DataLimit.from_gigabytes(5)) +await client.remove_access_key_data_limit("1") +await client.delete_access_key("1") +``` + + diff --git a/docs/guides/advanced-usage.md b/docs/guides/advanced-usage.md new file mode 100644 index 0000000..49fd1ba --- /dev/null +++ b/docs/guides/advanced-usage.md @@ -0,0 +1,89 @@ +# Advanced Usage + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Rate limiting & connection pooling + +```python +from pyoutlineapi import AsyncOutlineClient, OutlineClientConfig + +config = OutlineClientConfig.from_env( + rate_limit=50, + max_connections=20, +) + +async with AsyncOutlineClient(config) as client: + stats = client.get_rate_limiter_stats() + print(f"Active: {stats['active']}/{stats['limit']}") + print(f"Available: {stats['available']}") + + await client.set_rate_limit(100) + print(f"Active requests: {client.active_requests}") + print(f"Available slots: {client.available_slots}") +``` + +## Health checks & summary + +```python +async with AsyncOutlineClient.from_env() as client: + health = await client.health_check() + print(f"Healthy: {health['healthy']}") + print(f"Response time: {health.get('response_time_ms')}ms") + print(f"Circuit: {health['circuit_state']}") + + summary = await client.get_server_summary() + print(f"Server: {summary['server']['name']}") + print(f"Keys: {summary['access_keys_count']}") + print(f"Metrics enabled: {summary['server'].get('metricsEnabled')}") + + if summary.get("transfer_metrics"): + print(f"Total traffic: {summary['transfer_metrics']}") + + status = client.get_status() + print(f"Connected: {status['connected']}") + print(f"Circuit state: {status['circuit_state']}") + print(f"Active requests: {status['active_requests']}") + print(f"Rate limit: {status['rate_limit']}") +``` + +## JSON format + +```python +config = OutlineClientConfig.from_env(json_format=True) +async with AsyncOutlineClient(config) as client: + server_dict = await client.get_server_info() + print(server_dict["name"]) + +async with AsyncOutlineClient.from_env() as client: + server_dict = await client.get_server_info(as_json=True) + server_model = await client.get_server_info(as_json=False) +``` + +## Custom metrics collector + +```python +from pyoutlineapi.base_client import MetricsCollector + + +class PrometheusMetrics: + def increment(self, metric: str, *, tags: dict | None = None) -> None: + pass + + def timing(self, metric: str, value: float, *, tags: dict | None = None) -> None: + pass + + def gauge(self, metric: str, value: float, *, tags: dict | None = None) -> None: + pass + + +metrics = PrometheusMetrics() +async with AsyncOutlineClient.from_env(metrics=metrics) as client: + await client.get_server_info() +``` diff --git a/docs/guides/best-practices.md b/docs/guides/best-practices.md new file mode 100644 index 0000000..ae02d0a --- /dev/null +++ b/docs/guides/best-practices.md @@ -0,0 +1,98 @@ +# Best Practices + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Use environment variables + +```python +from pyoutlineapi import AsyncOutlineClient + +# Secure - credentials not in code +async with AsyncOutlineClient.from_env() as client: + pass + +# Avoid embedding secrets in code +client = AsyncOutlineClient( + api_url="https://...", + cert_sha256="a" * 64, +) +``` + +## Always use context managers + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + await client.get_server_info() + +client = AsyncOutlineClient.from_env() +await client.__aenter__() +try: + await client.get_server_info() +finally: + await client.shutdown() +``` + +## Handle specific exceptions + +```python +from pyoutlineapi.exceptions import APIError + +try: + key = await client.get_access_key("key-id") +except APIError as e: + if e.status_code == 404: + print("Key not found") + elif e.is_server_error: + print("Server error - retry") +``` + +## Enable production features + +```python +from pyoutlineapi import AsyncOutlineClient, DefaultAuditLogger, ProductionConfig + +config = ProductionConfig.from_env( + enable_circuit_breaker=True, + circuit_failure_threshold=5, + rate_limit=50, +) +audit = DefaultAuditLogger(enable_async=True, queue_size=5000) +client = AsyncOutlineClient(config, audit_logger=audit) +``` + +## Use batch operations for multiple items + +```python +from pyoutlineapi.batch_operations import BatchOperations + +batch = BatchOperations(client, max_concurrent=10) +configs = [{"name": f"User{i}"} for i in range(1, 101)] +result = await batch.create_multiple_keys(configs) + +for i in range(1, 101): + await client.create_access_key(name=f"User{i}") +``` + +## Monitor health & performance + +```python +from pyoutlineapi.health_monitoring import HealthMonitor + +monitor = HealthMonitor(client, cache_ttl=30) +health = await monitor.comprehensive_check() + +if not health.healthy: + logger.error(f"Service unhealthy: {health.failed_checks}") + +monitor.record_request(success=True, duration=0.5) +metrics = monitor.get_metrics() +``` diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md new file mode 100644 index 0000000..1d78e1d --- /dev/null +++ b/docs/guides/configuration.md @@ -0,0 +1,120 @@ +# Configuration + +Configuration options, environment variables, and safety controls. + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Environment variables + +```bash +# Required +OUTLINE_API_URL=https://server.com:12345/secret +OUTLINE_CERT_SHA256=your-certificate-fingerprint + +# Client settings +OUTLINE_TIMEOUT=10 +OUTLINE_RETRY_ATTEMPTS=2 +OUTLINE_MAX_CONNECTIONS=10 +OUTLINE_RATE_LIMIT=100 +OUTLINE_USER_AGENT=MyApp/1.0 + +# Feature flags +OUTLINE_ENABLE_CIRCUIT_BREAKER=true +OUTLINE_ENABLE_LOGGING=false +OUTLINE_JSON_FORMAT=false +OUTLINE_ALLOW_PRIVATE_NETWORKS=true +OUTLINE_RESOLVE_DNS_FOR_SSRF=false + +# Circuit breaker settings +OUTLINE_CIRCUIT_FAILURE_THRESHOLD=5 +OUTLINE_CIRCUIT_RECOVERY_TIMEOUT=60.0 +OUTLINE_CIRCUIT_SUCCESS_THRESHOLD=2 +OUTLINE_CIRCUIT_CALL_TIMEOUT=10.0 +``` + +```python +# Python usage +from pyoutlineapi import AsyncOutlineClient + +# Load from .env file +async with AsyncOutlineClient.from_env() as client: + await client.get_server_info() + +# Custom .env file +async with AsyncOutlineClient.from_env(env_file=".env.prod") as client: + await client.get_server_info() + +# Override specific settings +async with AsyncOutlineClient.from_env( + timeout=30, + enable_logging=True, + rate_limit=50, +) as client: + await client.get_server_info() +``` + +## Configuration object + +```python +from pyoutlineapi import AsyncOutlineClient, OutlineClientConfig +from pydantic import SecretStr + +config = OutlineClientConfig( + api_url="https://server.com:12345/secret", + cert_sha256=SecretStr("a" * 64), + timeout=30, + retry_attempts=5, + max_connections=20, + rate_limit=100, + enable_circuit_breaker=True, + enable_logging=True, + json_format=False, + allow_private_networks=True, + resolve_dns_for_ssrf=False, +) + +async with AsyncOutlineClient(config) as client: + await client.get_server_info() + +config = OutlineClientConfig.create_minimal( + api_url="https://server.com:12345/secret", + cert_sha256="a" * 64, + timeout=10, +) + +from pyoutlineapi import DevelopmentConfig, ProductionConfig + +dev_config = DevelopmentConfig.from_env() +prod_config = ProductionConfig.from_env() +``` + +## Configuration management + +```python +new_config = config.model_copy_immutable( + timeout=20, + enable_logging=True, +) + +safe_config = config.get_sanitized_config() +logger.info(f"Using config: {safe_config}") + +cert_value = config.get_cert_sha256() + +if config.circuit_config: + print(f"Threshold: {config.circuit_config.failure_threshold}") + print(f"Timeout: {config.circuit_config.recovery_timeout}") +``` + +## SSRF controls + +- `allow_private_networks=True` allows private/local IPs in the API URL. +- `resolve_dns_for_ssrf=True` resolves DNS and blocks if **any** A/AAAA record is private/reserved. + This protects against DNS rebinding and mixed public/private records. diff --git a/docs/guides/docker.md b/docs/guides/docker.md new file mode 100644 index 0000000..096a84b --- /dev/null +++ b/docs/guides/docker.md @@ -0,0 +1,132 @@ +# Docker + +## Setup + +Make sure `OUTLINE_API_URL` and `OUTLINE_CERT_SHA256` are provided via environment variables or `.env`. + +## Dockerfile + +```dockerfile +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENV OUTLINE_API_URL="" +ENV OUTLINE_CERT_SHA256="" +ENV OUTLINE_ENABLE_LOGGING="true" +ENV OUTLINE_ENABLE_CIRCUIT_BREAKER="true" +ENV OUTLINE_CIRCUIT_FAILURE_THRESHOLD="5" +ENV OUTLINE_RATE_LIMIT="50" + +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD python -c "import asyncio; from app import health_check; exit(0 if asyncio.run(health_check()) else 1)" + +CMD ["python", "app.py"] +``` + +## docker-compose.yml + +```yaml +version: '3.8' + +services: + outline-manager: + build: . + env_file: .env.prod + environment: + - OUTLINE_ENABLE_LOGGING=true + - OUTLINE_ENABLE_CIRCUIT_BREAKER=true + - OUTLINE_CIRCUIT_FAILURE_THRESHOLD=5 + - OUTLINE_RATE_LIMIT=50 + restart: unless-stopped + healthcheck: + test: [ "CMD", "python", "-c", "import asyncio; from app import health_check; exit(0 if asyncio.run(health_check()) else 1)" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - outline-net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + outline-net: + driver: bridge +``` + +## app.py + +```python +import asyncio +import logging + +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.health_monitoring import HealthMonitor + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +_client = None +_monitor = None + + +async def health_check() -> bool: + """Health check for Docker/Kubernetes.""" + global _client, _monitor + + if not _client: + _client = AsyncOutlineClient.from_env() + await _client.__aenter__() + _monitor = HealthMonitor(_client) + + try: + return await _monitor.quick_check() + except Exception as e: + logger.error("Health check failed: %s", e) + return False + + +async def main() -> int: + logger.info("Starting Outline VPN Manager...") + + async with AsyncOutlineClient.from_env() as client: + monitor = HealthMonitor(client) + if not await monitor.wait_for_healthy(timeout=60): + logger.error("Service not healthy after 60s") + return 1 + + logger.info("Service healthy - starting operations") + + server = await client.get_server_info() + logger.info("Managing server: %s", server.name) + + while True: + await asyncio.sleep(60) + health = await monitor.comprehensive_check() + if not health.healthy: + logger.warning("Health degraded: %s", health.failed_checks) + + return 0 + + +if __name__ == "__main__": + try: + exit(asyncio.run(main())) + except KeyboardInterrupt: + logger.info("Shutting down gracefully...") + finally: + if _client: + asyncio.run(_client.shutdown()) +``` diff --git a/docs/guides/enterprise-features.md b/docs/guides/enterprise-features.md new file mode 100644 index 0000000..7429aee --- /dev/null +++ b/docs/guides/enterprise-features.md @@ -0,0 +1,248 @@ +# Enterprise Features + +Production-grade patterns: circuit breaker, health monitoring, batch operations, metrics collection, and audit logging. + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Circuit breaker + +```python +from pyoutlineapi import AsyncOutlineClient, CircuitOpenError + +async with AsyncOutlineClient.from_env( + enable_circuit_breaker=True, + circuit_failure_threshold=5, + circuit_recovery_timeout=60.0, + circuit_success_threshold=2, +) as client: + try: + await client.get_server_info() + except CircuitOpenError as e: + print(f"Circuit open - retry after {e.retry_after}s") + + metrics = client.get_circuit_metrics() + if metrics: + print(f"State: {metrics['state']}") + print(f"Success rate: {metrics['success_rate']:.2%}") + print(f"Total calls: {metrics['total_calls']}") + print(f"Failed calls: {metrics['failed_calls']}") + + await client.reset_circuit_breaker() +``` + +Circuit states: +- CLOSED: normal operation +- OPEN: failures exceeded threshold +- HALF_OPEN: recovery probing + +## Health monitoring + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.health_monitoring import HealthMonitor + +async with AsyncOutlineClient.from_env() as client: + monitor = HealthMonitor(client, cache_ttl=30.0) + + is_healthy = await monitor.quick_check() + print(f"Quick check: {is_healthy}") + + health = await monitor.comprehensive_check() + print(f"Overall health: {health.healthy}") + print(f"Total checks: {health.total_checks}") + print(f"Passed: {health.passed_checks}") + print(f"Degraded: {health.is_degraded}") + + for check_name, result in health.checks.items(): + print(f"{check_name}: {result['status']}") + if "message" in result: + print(f" → {result['message']}") + + if not health.healthy: + print(f"Failed: {health.failed_checks}") + print(f"Warnings: {health.warning_checks}") + + print(f"Connectivity time: {health.metrics.get('connectivity_time', 0):.3f}s") + print(f"Success rate: {health.metrics.get('success_rate', 0):.2%}") + + async def check_disk_space(client): + return {"status": "healthy", "message": "Disk space OK"} + + monitor.add_custom_check("disk_space", check_disk_space) + health = await monitor.comprehensive_check(force_refresh=True) + + if await monitor.wait_for_healthy(timeout=120, check_interval=5): + print("Service recovered!") + else: + print("Service still unhealthy after 120s") + + monitor.record_request(success=True, duration=0.5) + monitor.record_request(success=False, duration=5.0) + + metrics = monitor.get_metrics() + print(f"Total requests: {metrics['total_requests']}") + print(f"Avg response time: {metrics['avg_response_time']:.3f}s") + print(f"Uptime: {metrics['uptime']:.1f}s") +``` + +## Batch operations + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.batch_operations import BatchOperations +from pyoutlineapi.models import DataLimit + +async with AsyncOutlineClient.from_env() as client: + batch = BatchOperations(client, max_concurrent=10) + + configs = [ + {"name": f"User{i}", "limit": DataLimit.from_gigabytes(5)} + for i in range(1, 101) + ] + result = await batch.create_multiple_keys(configs, fail_fast=False) + + print(f"Created: {result.successful}/{result.total}") + print(f"Failed: {result.failed}") + print(f"Success rate: {result.success_rate:.2%}") + + keys = result.get_successful_results() + + if result.has_validation_errors: + print(f"Validation errors: {result.validation_errors}") + + pairs = [(key.id, f"User-{i}") for i, key in enumerate(keys, 1)] + await batch.rename_multiple_keys(pairs) + + limits = [(key.id, DataLimit.from_gigabytes(10).bytes) for key in keys] + await batch.set_multiple_data_limits(limits) + + key_ids = [key.id for key in keys] + await batch.delete_multiple_keys(key_ids) + + operations = [ + lambda: client.get_access_key(key_id) + for key_id in key_ids + ] + await batch.execute_custom_operations(operations) + + await batch.set_concurrency(20) +``` + +## Metrics collection + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.metrics_collector import MetricsCollector +import asyncio +import time + +async with AsyncOutlineClient.from_env() as client: + collector = MetricsCollector( + client, + interval=60, + max_history=1440, + ) + + await collector.start() + await asyncio.sleep(3600) + + snapshot = collector.get_latest_snapshot() + if snapshot: + print(f"Keys: {snapshot.key_count}") + print(f"Total traffic: {snapshot.total_bytes_transferred}") + + stats = collector.get_usage_stats(period_minutes=60) + print(f"Period: {stats.duration:.1f}s") + print(f"Total: {stats.gigabytes_transferred:.2f} GB") + print(f"Rate: {stats.bytes_per_second / 1024:.2f} KB/s") + print(f"Peak: {stats.peak_bytes / 1024 ** 3:.2f} GB") + print(f"Active keys: {len(stats.active_keys)}") + print(f"Snapshots: {stats.snapshots_count}") + + key_usage = collector.get_key_usage("key-123", period_minutes=60) + print(f"Key 123 traffic: {key_usage['total_bytes'] / 1024 ** 3:.2f} GB") + print(f"Key 123 rate: {key_usage['bytes_per_second'] / 1024:.2f} KB/s") + + cutoff = time.time() - 3600 + recent_snapshots = collector.get_snapshots_after(cutoff) + + data = collector.export_to_dict() + print(f"Collection duration: {data['collection_end'] - data['collection_start']:.1f}s") + print(f"Snapshots: {data['snapshots_count']}") + + prometheus = collector.export_prometheus_format(include_per_key=True) + print(prometheus) + + summary = collector.export_prometheus_summary() + print(summary) + + print(f"Running: {collector.is_running}") + print(f"Uptime: {collector.uptime:.1f}s") + print(f"Snapshots collected: {collector.snapshots_count}") + + await collector.stop() + +async with AsyncOutlineClient.from_env() as client: + async with MetricsCollector(client, interval=60) as collector: + await asyncio.sleep(3600) + stats = collector.get_usage_stats() +``` + +## Audit logging + +```python +from pyoutlineapi import AsyncOutlineClient, DefaultAuditLogger, set_default_audit_logger +from pyoutlineapi import NoOpAuditLogger + +# Default audit logger (built-in) +audit_logger = DefaultAuditLogger( + enable_async=True, + queue_size=5000, +) + +async with AsyncOutlineClient.from_env(audit_logger=audit_logger) as client: + key = await client.create_access_key(name="Alice") + await client.rename_access_key(key.id, "Alice Smith") + await client.delete_access_key(key.id) + + try: + await client.delete_access_key("non-existent") + except Exception: + pass + + +class CustomAuditLogger: + def log_action(self, action: str, resource: str, **kwargs) -> None: + print(f"AUDIT: {action} on {resource} - {kwargs}") + + async def alog_action(self, action: str, resource: str, **kwargs) -> None: + self.log_action(action, resource, **kwargs) + + async def shutdown(self) -> None: + pass + + +set_default_audit_logger(DefaultAuditLogger(enable_async=True)) + +async with AsyncOutlineClient.from_env() as client1: + await client1.create_access_key(name="User1") + +async with AsyncOutlineClient.from_env() as client2: + await client2.create_access_key(name="User2") + +async with AsyncOutlineClient.from_env(audit_logger=NoOpAuditLogger()) as client: + await client.create_access_key(name="Test") +``` + +Audited operations: +- Access keys: create, delete, rename, set/remove data limit +- Server: rename, set hostname, set default port +- Data limits: set/remove global data limit +- Metrics: set metrics status diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md new file mode 100644 index 0000000..6a6f3f8 --- /dev/null +++ b/docs/guides/error-handling.md @@ -0,0 +1,98 @@ +# Error Handling + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Exception hierarchy + +```python +from pyoutlineapi.exceptions import ( + OutlineError, + APIError, + CircuitOpenError, + ConfigurationError, + ValidationError, + ConnectionError, + TimeoutError, +) +``` + +## Comprehensive handling + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.exceptions import ( + CircuitOpenError, + APIError, + ConnectionError, + TimeoutError, + get_retry_delay, + is_retryable, + get_safe_error_dict, +) + +async with AsyncOutlineClient.from_env() as client: + try: + server = await client.get_server_info() + + except CircuitOpenError as e: + print(f"Service failing - retry after {e.retry_after}s") + await asyncio.sleep(e.retry_after) + + except APIError as e: + print(f"Status: {e.status_code}") + print(f"Endpoint: {e.endpoint}") + + if e.is_client_error: + print("Client error - check request") + elif e.is_server_error: + print("Server error - retry may help") + elif e.is_rate_limit_error: + print("Rate limited") + + if e.is_retryable: + delay = get_retry_delay(e) + await asyncio.sleep(delay) + + except ConnectionError as e: + print(f"Failed to connect to {e.host}:{e.port}") + + except TimeoutError as e: + print(f"Timeout after {e.timeout}s on {e.operation}") + + except OutlineError as e: + safe_dict = get_safe_error_dict(e) + logger.error(f"Error: {safe_dict}") +``` + +## Retry strategy + +```python +import asyncio + +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.exceptions import APIError, is_retryable, get_retry_delay + + +async def robust_operation(max_attempts: int = 3): + for attempt in range(max_attempts): + try: + async with AsyncOutlineClient.from_env() as client: + return await client.get_server_info() + + except APIError as e: + if not is_retryable(e) or attempt == max_attempts - 1: + raise + + delay = get_retry_delay(e) or 1.0 + backoff_delay = delay * (2 ** attempt) + + print(f"Attempt {attempt + 1} failed, retrying in {backoff_delay}s") + await asyncio.sleep(backoff_delay) +``` diff --git a/docs/guides/metrics.md b/docs/guides/metrics.md new file mode 100644 index 0000000..a2946a9 --- /dev/null +++ b/docs/guides/metrics.md @@ -0,0 +1,53 @@ +# Metrics + +Guide to metrics management and transfer statistics. + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Enable / disable metrics + +```python +from pyoutlineapi import AsyncOutlineClient + +await client.set_metrics_status(True) +status = await client.get_metrics_status() +``` + +## Transfer metrics + +```python +from pyoutlineapi import AsyncOutlineClient + +metrics = await client.get_transfer_metrics() +print(f"Total: {metrics.total_gigabytes:.2f} GB") +print(f"Active keys: {metrics.user_count}") + +for key_id, bytes_used in metrics.top_users(5): + print(f"{key_id}: {bytes_used / 1024 ** 3:.2f} GB") + +usage = metrics.get_user_bytes("key-123") +print(f"Key 123: {usage / 1024 ** 3:.2f} GB") +``` + +## Experimental metrics + +```python +from pyoutlineapi import AsyncOutlineClient + +exp = await client.get_experimental_metrics("24h") +print(f"Server tunnel time: {exp.server.tunnel_time.hours:.1f} hours") +print(f"Server traffic: {exp.server.data_transferred.gigabytes:.2f} GB") +print(f"Peak bandwidth: {exp.server.bandwidth.peak.data.bytes} bytes") +print(f"Locations: {len(exp.server.locations)}") + +key_metric = exp.get_key_metric("key-123") +if key_metric: + print(f"Key tunnel time: {key_metric.tunnel_time.minutes:.1f} min") +``` diff --git a/docs/guides/production-example.md b/docs/guides/production-example.md new file mode 100644 index 0000000..158d241 --- /dev/null +++ b/docs/guides/production-example.md @@ -0,0 +1,192 @@ +# Production Example + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +```python +""" +Enterprise-grade Outline VPN management application. +Features: health monitoring, metrics collection, batch operations, audit logging. +""" +import asyncio +import logging +import signal +from typing import Optional + +from pyoutlineapi import AsyncOutlineClient, DefaultAuditLogger +from pyoutlineapi.health_monitoring import HealthMonitor +from pyoutlineapi.batch_operations import BatchOperations +from pyoutlineapi.metrics_collector import MetricsCollector +from pyoutlineapi.exceptions import OutlineError, CircuitOpenError +from pyoutlineapi.models import DataLimit + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('outline_manager.log') + ] +) +logger = logging.getLogger(__name__) + + +class OutlineManager: + """Production-ready Outline VPN manager.""" + + def __init__(self): + self.client: Optional[AsyncOutlineClient] = None + self.monitor: Optional[HealthMonitor] = None + self.collector: Optional[MetricsCollector] = None + self.shutdown_event = asyncio.Event() + + async def initialize(self) -> bool: + """Initialize all components.""" + try: + audit_logger = DefaultAuditLogger( + enable_async=True, + queue_size=5000, + ) + + self.client = AsyncOutlineClient.from_env( + audit_logger=audit_logger, + enable_circuit_breaker=True, + circuit_failure_threshold=5, + rate_limit=50, + ) + await self.client.__aenter__() + + self.monitor = HealthMonitor(self.client, cache_ttl=30) + + logger.info("Waiting for service to become healthy...") + if not await self.monitor.wait_for_healthy(timeout=120): + logger.error("Service not healthy after 120s") + return False + + logger.info("Service healthy!") + + self.collector = MetricsCollector( + self.client, + interval=60, + max_history=1440, + ) + await self.collector.start() + logger.info("Metrics collection started") + + return True + + except Exception as e: + logger.error(f"Initialization failed: {e}") + return False + + async def create_users(self, count: int) -> None: + """Create multiple users with batch operations.""" + logger.info(f"Creating {count} users...") + + batch = BatchOperations(self.client, max_concurrent=10) + configs = [ + { + "name": f"User{i:04d}", + "limit": DataLimit.from_gigabytes(10), + } + for i in range(1, count + 1) + ] + + result = await batch.create_multiple_keys(configs, fail_fast=False) + + logger.info(f"Created: {result.successful}/{result.total}") + logger.info(f"Success rate: {result.success_rate:.2%}") + + if result.has_errors: + logger.warning(f"Errors: {result.errors}") + + async def monitor_loop(self) -> None: + """Continuous health monitoring loop.""" + while not self.shutdown_event.is_set(): + try: + health = await self.monitor.comprehensive_check() + + if not health.healthy: + logger.warning(f"Health check failed: {health.failed_checks}") + + if health.is_degraded: + logger.warning(f"Service degraded: {health.warning_checks}") + + await asyncio.sleep(30) + + except Exception as e: + logger.error(f"Monitor loop error: {e}") + await asyncio.sleep(10) + + async def metrics_report(self) -> None: + """Periodic metrics reporting.""" + while not self.shutdown_event.is_set(): + try: + await asyncio.sleep(300) + + stats = self.collector.get_usage_stats(period_minutes=60) + logger.info( + "1h traffic: %.2f GB | active keys: %d", + stats.gigabytes_transferred, + len(stats.active_keys), + ) + + except Exception as e: + logger.error(f"Metrics report error: {e}") + + async def shutdown(self) -> None: + """Graceful shutdown.""" + self.shutdown_event.set() + + if self.collector: + await self.collector.stop() + + if self.client: + await self.client.shutdown() + + async def run(self) -> int: + if not await self.initialize(): + return 1 + + tasks = [ + asyncio.create_task(self.monitor_loop()), + asyncio.create_task(self.metrics_report()), + ] + + await self.shutdown_event.wait() + + for task in tasks: + task.cancel() + + await self.shutdown() + return 0 + + +def setup_signal_handlers(manager: OutlineManager) -> None: + def _handler(_sig, _frame): + logger.info("Shutdown signal received") + manager.shutdown_event.set() + + signal.signal(signal.SIGTERM, _handler) + signal.signal(signal.SIGINT, _handler) + + +async def main() -> int: + manager = OutlineManager() + setup_signal_handlers(manager) + return await manager.run() + + +if __name__ == "__main__": + try: + exit(asyncio.run(main())) + except KeyboardInterrupt: + logger.info("Shutting down gracefully...") +``` diff --git a/docs/guides/server-management.md b/docs/guides/server-management.md new file mode 100644 index 0000000..ed99a4b --- /dev/null +++ b/docs/guides/server-management.md @@ -0,0 +1,596 @@ +# Server Management Guide + +Complete guide to managing Outline VPN server configuration with PyOutlineAPI. + +## Setup + +```python +from pyoutlineapi import AsyncOutlineClient + +async with AsyncOutlineClient.from_env() as client: + pass +``` + +## Table of Contents + +- [Server Information](#server-information) +- [Server Configuration](#server-configuration) +- [Network Settings](#network-settings) +- [Global Data Limits](#global-data-limits) +- [Metrics Management](#metrics-management) +- [Best Practices](#best-practices) + +--- + +## Server Information + +### Get Server Info + +```python +from pyoutlineapi import AsyncOutlineClient +import asyncio + + +async def get_server_details(): + async with AsyncOutlineClient.from_env() as client: + server = await client.get_server_info() + + print(f"Server Name: {server.name}") + print(f"Server ID: {server.server_id}") + print(f"Version: {server.version}") + print(f"Port: {server.port_for_new_access_keys}") + print(f"Hostname: {server.hostname_for_access_keys}") + print(f"Metrics: {server.metrics_enabled}") + print(f"Created: {server.created_timestamp_seconds}") + + +asyncio.run(get_server_details()) +``` + +### Server Properties + +```python +async def analyze_server(): + async with AsyncOutlineClient.from_env() as client: + server = await client.get_server_info() + + # Check metrics status + if server.metrics_enabled: + print("✅ Metrics enabled") + else: + print("❌ Metrics disabled") + + # Check default port + if server.port_for_new_access_keys == 443: + print("✅ Using standard HTTPS port") + else: + print(f"ℹ️ Using custom port: {server.port_for_new_access_keys}") + + # Check hostname + if server.hostname_for_access_keys: + print(f"✅ Hostname configured: {server.hostname_for_access_keys}") + else: + print("⚠️ No hostname configured") + + +asyncio.run(analyze_server()) +``` + +--- + +## Server Configuration + +### Rename Server + +```python +async def rename_server(): + async with AsyncOutlineClient.from_env() as client: + # Update server name + await client.rename_server("Production VPN Server") + + # Verify + server = await client.get_server_info() + print(f"Server renamed to: {server.name}") + + +asyncio.run(rename_server()) +``` + +### Complete Server Setup + +```python +async def setup_server(): + """Complete server configuration.""" + async with AsyncOutlineClient.from_env() as client: + # Set server name + await client.rename_server("Company VPN") + print("✅ Server name set") + + # Configure hostname + await client.set_hostname("vpn.company.com") + print("✅ Hostname configured") + + # Set default port + await client.set_default_port(443) + print("✅ Port configured") + + # Enable metrics + await client.set_metrics_status(True) + print("✅ Metrics enabled") + + # Verify configuration + server = await client.get_server_info() + print(f"\n📋 Final configuration:") + print(f" Name: {server.name}") + print(f" Hostname: {server.hostname_for_access_keys}") + print(f" Port: {server.port_for_new_access_keys}") + print(f" Metrics: {server.metrics_enabled}") + + +asyncio.run(setup_server()) +``` + +--- + +## Network Settings + +### Set Hostname + +```python +async def configure_hostname(): + async with AsyncOutlineClient.from_env() as client: + # Set custom hostname + await client.set_hostname("vpn.example.com") + + # Verify + server = await client.get_server_info() + print(f"Hostname: {server.hostname_for_access_keys}") + + +asyncio.run(configure_hostname()) +``` + +### Set Default Port + +```python +async def configure_port(): + async with AsyncOutlineClient.from_env() as client: + # Change default port for new keys + await client.set_default_port(443) + + # Verify + server = await client.get_server_info() + print(f"Default port: {server.port_for_new_access_keys}") + + +asyncio.run(configure_port()) +``` + +### Update Port for Existing Keys + +```python +async def update_all_ports(): + """Update port for all existing keys.""" + async with AsyncOutlineClient.from_env() as client: + # Set new default port + new_port = 8443 + await client.set_default_port(new_port) + + # Get all keys + keys = await client.get_access_keys() + + # Note: Existing keys keep their ports + # New keys will use the new default + print(f"New default port: {new_port}") + print(f"Existing keys: {keys.count}") + print("Note: Existing keys retain their current ports") + + +asyncio.run(update_all_ports()) +``` + +--- + +## Global Data Limits + +### Set Global Limit + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.models import DataLimit + + +async def set_global_limit(): + async with AsyncOutlineClient.from_env() as client: + # Set 100 GB global limit + limit = DataLimit.from_gigabytes(100) + await client.set_global_data_limit(limit) + + print(f"✅ Global limit set: 100 GB") + print("This affects all keys without individual limits") + + +asyncio.run(set_global_limit()) +``` + +### Remove Global Limit + +```python +async def remove_global_limit(): + async with AsyncOutlineClient.from_env() as client: + # Remove global data limit + await client.remove_global_data_limit() + + print("✅ Global limit removed") + print("Keys without individual limits are now unlimited") + + +asyncio.run(remove_global_limit()) +``` + +### Check Global Limit Impact + +```python +async def analyze_limit_impact(): + async with AsyncOutlineClient.from_env() as client: + keys = await client.get_access_keys() + + # Count keys affected by global limit + without_individual_limits = keys.filter_without_limits() + with_limits = keys.filter_with_limits() + + print(f"Total keys: {keys.count}") + print(f"Keys with individual limits: {len(with_limits)}") + print(f"Keys affected by global limit: {len(without_individual_limits)}") + + +asyncio.run(analyze_limit_impact()) +``` + +--- + +## Metrics Management + +### Enable Metrics + +```python +async def enable_metrics(): + async with AsyncOutlineClient.from_env() as client: + # Enable metrics collection + await client.set_metrics_status(True) + + # Verify + status = await client.get_metrics_status() + print(f"Metrics enabled: {status.metrics_enabled}") + + +asyncio.run(enable_metrics()) +``` + +### Disable Metrics + +```python +async def disable_metrics(): + async with AsyncOutlineClient.from_env() as client: + # Disable metrics + await client.set_metrics_status(False) + + # Verify + status = await client.get_metrics_status() + print(f"Metrics enabled: {status.metrics_enabled}") + + +asyncio.run(disable_metrics()) +``` + +### Check Metrics Status + +```python +async def check_metrics(): + async with AsyncOutlineClient.from_env() as client: + status = await client.get_metrics_status() + + if status.metrics_enabled: + print("✅ Metrics collection is enabled") + print("You can view transfer metrics and analytics") + else: + print("❌ Metrics collection is disabled") + print("Enable metrics to track usage") + + +asyncio.run(check_metrics()) +``` + +--- + +## Best Practices + +### 1. Use Descriptive Names + +```python +# ❌ Bad: Generic name +await client.rename_server("Server1") + +# ✅ Good: Descriptive name +await client.rename_server("US-East Production VPN") +``` + +### 2. Configure Hostname + +```python +# Always set hostname for better user experience +await client.set_hostname("vpn.company.com") +``` + +### 3. Use Standard Ports + +```python +# Prefer standard ports for better compatibility +await client.set_default_port(443) # HTTPS +# or +await client.set_default_port(8080) # Common alternative +``` + +### 4. Enable Metrics + +```python +# Enable metrics for monitoring and analytics +await client.set_metrics_status(True) +``` + +### 5. Document Configuration + +```python +from dataclasses import dataclass +from datetime import datetime + + +@dataclass +class ServerConfig: + """Server configuration snapshot.""" + name: str + hostname: str + port: int + metrics_enabled: bool + timestamp: datetime + + +async def save_config(): + """Save current configuration.""" + async with AsyncOutlineClient.from_env() as client: + server = await client.get_server_info() + + config = ServerConfig( + name=server.name, + hostname=server.hostname_for_access_keys or "not-set", + port=server.port_for_new_access_keys, + metrics_enabled=server.metrics_enabled, + timestamp=datetime.now() + ) + + # Save to file/database + print(f"Configuration saved: {config}") + + +asyncio.run(save_config()) +``` + +### 6. Validate Changes + +```python +async def validate_server_config(): + """Validate server configuration.""" + async with AsyncOutlineClient.from_env() as client: + server = await client.get_server_info() + + issues = [] + + # Check name + if not server.name or server.name == "My Outline Server": + issues.append("Server name not customized") + + # Check hostname + if not server.hostname_for_access_keys: + issues.append("Hostname not configured") + + # Check port + if server.port_for_new_access_keys not in [443, 8080, 8443]: + issues.append(f"Non-standard port: {server.port_for_new_access_keys}") + + # Check metrics + if not server.metrics_enabled: + issues.append("Metrics disabled") + + if issues: + print("⚠️ Configuration issues:") + for issue in issues: + print(f" - {issue}") + else: + print("✅ Configuration looks good") + + +asyncio.run(validate_server_config()) +``` + +### 7. Configuration Template + +```python +from typing import TypedDict + + +class ServerTemplate(TypedDict): + """Server configuration template.""" + name: str + hostname: str + port: int + metrics_enabled: bool + global_limit_gb: int | None + + +# Production template +PRODUCTION_CONFIG: ServerTemplate = { + "name": "Production VPN", + "hostname": "vpn.company.com", + "port": 443, + "metrics_enabled": True, + "global_limit_gb": 100, +} + +# Development template +DEVELOPMENT_CONFIG: ServerTemplate = { + "name": "Development VPN", + "hostname": "vpn-dev.company.com", + "port": 8080, + "metrics_enabled": True, + "global_limit_gb": None, +} + + +async def apply_template(template: ServerTemplate): + """Apply configuration template.""" + async with AsyncOutlineClient.from_env() as client: + await client.rename_server(template["name"]) + await client.set_hostname(template["hostname"]) + await client.set_default_port(template["port"]) + await client.set_metrics_status(template["metrics_enabled"]) + + if template["global_limit_gb"]: + limit = DataLimit.from_gigabytes(template["global_limit_gb"]) + await client.set_global_data_limit(limit) + else: + await client.remove_global_data_limit() + + print(f"✅ Applied template: {template['name']}") + + +# Apply production configuration +asyncio.run(apply_template(PRODUCTION_CONFIG)) +``` + +--- + +## Complete Example + +```python +from pyoutlineapi import AsyncOutlineClient +from pyoutlineapi.models import DataLimit +from pyoutlineapi.exceptions import OutlineError +import asyncio +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class ServerManager: + """Server configuration manager.""" + + def __init__(self, client: AsyncOutlineClient): + self.client = client + + async def initial_setup( + self, + name: str, + hostname: str, + port: int = 443, + global_limit_gb: int | None = None, + ): + """Perform initial server setup.""" + try: + # Configure server + await self.client.rename_server(name) + logger.info(f"✅ Server name: {name}") + + await self.client.set_hostname(hostname) + logger.info(f"✅ Hostname: {hostname}") + + await self.client.set_default_port(port) + logger.info(f"✅ Port: {port}") + + await self.client.set_metrics_status(True) + logger.info("✅ Metrics enabled") + + # Set global limit if specified + if global_limit_gb: + limit = DataLimit.from_gigabytes(global_limit_gb) + await self.client.set_global_data_limit(limit) + logger.info(f"✅ Global limit: {global_limit_gb} GB") + + # Verify configuration + server = await self.client.get_server_info() + logger.info(f"\n📋 Server configured:") + logger.info(f" Name: {server.name}") + logger.info(f" Hostname: {server.hostname_for_access_keys}") + logger.info(f" Port: {server.port_for_new_access_keys}") + logger.info(f" Metrics: {server.metrics_enabled}") + + return True + + except OutlineError as e: + logger.error(f"❌ Setup failed: {e}") + return False + + async def get_status(self) -> dict: + """Get comprehensive server status.""" + server = await self.client.get_server_info() + keys = await self.client.get_access_keys() + + status = { + "server": { + "name": server.name, + "version": server.version, + "hostname": server.hostname_for_access_keys, + "port": server.port_for_new_access_keys, + "metrics_enabled": server.metrics_enabled, + }, + "keys": { + "total": keys.count, + "with_limits": len(keys.filter_with_limits()), + "without_limits": len(keys.filter_without_limits()), + } + } + + if server.metrics_enabled: + metrics = await self.client.get_transfer_metrics() + status["usage"] = { + "total_gb": metrics.total_gigabytes, + "active_keys": metrics.user_count, + } + + return status + + +async def main(): + """Main setup flow.""" + async with AsyncOutlineClient.from_env() as client: + manager = ServerManager(client) + + # Initial setup + success = await manager.initial_setup( + name="Company VPN Server", + hostname="vpn.company.com", + port=443, + global_limit_gb=100 + ) + + if success: + # Get status + status = await manager.get_status() + logger.info(f"\n📊 Status: {status}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +--- + +## See Also + +- [Access Key Management](access-keys.md) +- [Metrics & Monitoring](metrics-monitoring.md) +- [Configuration Guide](../getting-started/configuration.md) + +--- + +[← Back to Documentation](../README.md) diff --git a/docs/pyoutlineapi.html b/docs/pyoutlineapi.html index f4675f0..a6312fd 100644 --- a/docs/pyoutlineapi.html +++ b/docs/pyoutlineapi.html @@ -3,14 +3,14 @@ - + pyoutlineapi API documentation - + -
-
-

-pyoutlineapi

+ +
  • + DefaultAuditLogger + + +
  • +
  • + DevelopmentConfig + + +
  • +
  • + ErrorResponse + + +
  • +
  • + ExperimentalMetrics + + +
  • +
  • + HealthCheckResult + + +
  • +
  • + HostnameRequest + + +
  • +
  • + JsonDict +
  • +
  • + JsonPayload +
  • +
  • + LocationMetric + + +
  • +
  • + MetricsCollector + + +
  • +
  • + MetricsEnabledRequest + + +
  • +
  • + MetricsStatusResponse + + +
  • +
  • + MetricsTags +
  • +
  • + MultiServerManager + + +
  • +
  • + NoOpAuditLogger + + +
  • +
  • + NoOpMetrics + + +
  • +
  • + OutlineClientConfig + + +
  • +
  • + OutlineConnectionError + + +
  • +
  • + OutlineError + + +
  • +
  • + OutlineTimeoutError + + +
  • +
  • + PeakDeviceCount + + +
  • +
  • + PortRequest + + +
  • +
  • + ProductionConfig + + +
  • +
  • + QueryParams +
  • +
  • + ResponseData +
  • +
  • + ResponseParser + + +
  • +
  • + SecureIDGenerator + + +
  • +
  • + Server + + +
  • +
  • + ServerExperimentalMetric + + +
  • +
  • + ServerMetrics + + +
  • +
  • + ServerNameRequest + + +
  • +
  • + ServerSummary + + +
  • +
  • + TimestampMs +
  • +
  • + TimestampSec +
  • +
  • + TunnelTime + + +
  • +
  • + ValidationError + + +
  • +
  • + Validators + + +
  • +
  • + __author__ +
  • +
  • + __email__ +
  • +
  • + __license__ +
  • +
  • + __version__ +
  • +
  • + audited +
  • +
  • + build_config_overrides +
  • +
  • + correlation_id +
  • +
  • + create_client +
  • +
  • + create_env_template +
  • +
  • + create_multi_server_manager +
  • +
  • + format_error_chain +
  • +
  • + get_audit_logger +
  • +
  • + get_or_create_audit_logger +
  • +
  • + get_retry_delay +
  • +
  • + get_safe_error_dict +
  • +
  • + get_version +
  • +
  • + is_json_serializable +
  • +
  • + is_retryable +
  • +
  • + is_valid_bytes +
  • +
  • + is_valid_port +
  • +
  • + load_config +
  • +
  • + mask_sensitive_data +
  • +
  • + print_type_info +
  • +
  • + quick_setup +
  • +
  • + set_audit_logger +
  • + + + + + + built with pdocpdoc logo + + + +
    +
    +

    +pyoutlineapi

    + +

    PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    + +

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru +All rights reserved.

    + +

    This software is licensed under the MIT License.

    + +
    You can find the full license text at:
    + +
    +

    https://opensource.org/licenses/MIT

    +
    + +
    Source code repository:
    + +
    +

    https://github.com/orenlab/pyoutlineapi

    +
    + +

    Quick Start:

    + +
    +
    from pyoutlineapi import AsyncOutlineClient
    +
    +# From environment variables
    +async with AsyncOutlineClient.from_env() as client:
    +    server = await client.get_server_info()
    +    print(f"Server: {server.name}")
    +
    +# Prefer from_env for production usage
    +async with AsyncOutlineClient.from_env() as client:
    +    keys = await client.get_access_keys()
    +
    +
    + +

    Advanced Usage - Type Hints:

    + +
    +
    from pyoutlineapi import (
    +    AsyncOutlineClient,
    +    AuditLogger,
    +    AuditDetails,
    +    MetricsCollector,
    +    MetricsTags,
    +)
    +
    +class CustomAuditLogger:
    +    def log_action(
    +        self,
    +        action: str,
    +        resource: str,
    +        *,
    +        user: str | None = None,
    +        details: AuditDetails | None = None,
    +        correlation_id: str | None = None,
    +    ) -> None:
    +        print(f"[AUDIT] {action} on {resource}")
    +
    +async with AsyncOutlineClient.from_env(
    +    audit_logger=CustomAuditLogger(),
    +) as client:
    +    await client.create_access_key(name="test")
    +
    +
    +
    + + + + + +
      1"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.
    +  2
    +  3Copyright (c) 2025 Denis Rozhnovskiy <pytelemonbot@mail.ru>
    +  4All rights reserved.
    +  5
    +  6This software is licensed under the MIT License.
    +  7You can find the full license text at:
    +  8    https://opensource.org/licenses/MIT
    +  9
    + 10Source code repository:
    + 11    https://github.com/orenlab/pyoutlineapi
    + 12
    + 13Quick Start:
    + 14
    + 15```python
    + 16from pyoutlineapi import AsyncOutlineClient
    + 17
    + 18# From environment variables
    + 19async with AsyncOutlineClient.from_env() as client:
    + 20    server = await client.get_server_info()
    + 21    print(f"Server: {server.name}")
    + 22
    + 23# Prefer from_env for production usage
    + 24async with AsyncOutlineClient.from_env() as client:
    + 25    keys = await client.get_access_keys()
    + 26```
    + 27
    + 28Advanced Usage - Type Hints:
    + 29
    + 30```python
    + 31from pyoutlineapi import (
    + 32    AsyncOutlineClient,
    + 33    AuditLogger,
    + 34    AuditDetails,
    + 35    MetricsCollector,
    + 36    MetricsTags,
    + 37)
    + 38
    + 39class CustomAuditLogger:
    + 40    def log_action(
    + 41        self,
    + 42        action: str,
    + 43        resource: str,
    + 44        *,
    + 45        user: str | None = None,
    + 46        details: AuditDetails | None = None,
    + 47        correlation_id: str | None = None,
    + 48    ) -> None:
    + 49        print(f"[AUDIT] {action} on {resource}")
    + 50
    + 51async with AsyncOutlineClient.from_env(
    + 52    audit_logger=CustomAuditLogger(),
    + 53) as client:
    + 54    await client.create_access_key(name="test")
    + 55```
    + 56"""
    + 57
    + 58from __future__ import annotations
    + 59
    + 60from importlib import metadata
    + 61from typing import TYPE_CHECKING, Final, NoReturn
    + 62
    + 63# Core imports
    + 64from .audit import (
    + 65    AuditContext,
    + 66    AuditLogger,
    + 67    DefaultAuditLogger,
    + 68    NoOpAuditLogger,
    + 69    audited,
    + 70    get_audit_logger,
    + 71    get_or_create_audit_logger,
    + 72    set_audit_logger,
    + 73)
    + 74from .base_client import MetricsCollector, NoOpMetrics, correlation_id
    + 75from .circuit_breaker import CircuitConfig, CircuitMetrics, CircuitState
    + 76from .client import (
    + 77    AsyncOutlineClient,
    + 78    MultiServerManager,
    + 79    create_client,
    + 80    create_multi_server_manager,
    + 81)
    + 82from .common_types import (
    + 83    DEFAULT_SENSITIVE_KEYS,
    + 84    AuditDetails,
    + 85    ConfigOverrides,
    + 86    Constants,
    + 87    CredentialSanitizer,
    + 88    JsonPayload,
    + 89    MetricsTags,
    + 90    QueryParams,
    + 91    ResponseData,
    + 92    SecureIDGenerator,
    + 93    TimestampMs,
    + 94    TimestampSec,
    + 95    Validators,
    + 96    build_config_overrides,
    + 97    is_json_serializable,
    + 98    is_valid_bytes,
    + 99    is_valid_port,
    +100    mask_sensitive_data,
    +101)
    +102from .config import (
    +103    DevelopmentConfig,
    +104    OutlineClientConfig,
    +105    ProductionConfig,
    +106    create_env_template,
    +107    load_config,
    +108)
    +109from .exceptions import (
    +110    APIError,
    +111    CircuitOpenError,
    +112    ConfigurationError,
    +113    OutlineConnectionError,
    +114    OutlineError,
    +115    OutlineTimeoutError,
    +116    ValidationError,
    +117    format_error_chain,
    +118    get_retry_delay,
    +119    get_safe_error_dict,
    +120    is_retryable,
    +121)
    +122from .models import (
    +123    AccessKey,
    +124    AccessKeyCreateRequest,
    +125    AccessKeyList,
    +126    AccessKeyMetric,
    +127    AccessKeyNameRequest,
    +128    BandwidthData,
    +129    BandwidthDataValue,
    +130    BandwidthInfo,
    +131    ConnectionInfo,
    +132    DataLimit,
    +133    DataLimitRequest,
    +134    DataTransferred,
    +135    ErrorResponse,
    +136    ExperimentalMetrics,
    +137    HealthCheckResult,
    +138    HostnameRequest,
    +139    LocationMetric,
    +140    MetricsEnabledRequest,
    +141    MetricsStatusResponse,
    +142    PeakDeviceCount,
    +143    PortRequest,
    +144    Server,
    +145    ServerExperimentalMetric,
    +146    ServerMetrics,
    +147    ServerNameRequest,
    +148    ServerSummary,
    +149    TunnelTime,
    +150)
    +151from .response_parser import JsonDict, ResponseParser
    +152
    +153# Package metadata
    +154try:
    +155    __version__: str = metadata.version("pyoutlineapi")
    +156except metadata.PackageNotFoundError:
    +157    __version__ = "0.4.0-dev"
    +158
    +159__author__: Final[str] = "Denis Rozhnovskiy"
    +160__email__: Final[str] = "pytelemonbot@mail.ru"
    +161__license__: Final[str] = "MIT"
    +162
    +163# Public API
    +164__all__: Final[list[str]] = [
    +165    "DEFAULT_SENSITIVE_KEYS",
    +166    "APIError",
    +167    "AccessKey",
    +168    "AccessKeyCreateRequest",
    +169    "AccessKeyList",
    +170    "AccessKeyMetric",
    +171    "AccessKeyNameRequest",
    +172    "AsyncOutlineClient",
    +173    "AuditContext",
    +174    "AuditLogger",
    +175    "BandwidthData",
    +176    "BandwidthDataValue",
    +177    "BandwidthInfo",
    +178    "CircuitConfig",
    +179    "CircuitMetrics",
    +180    "CircuitOpenError",
    +181    "CircuitState",
    +182    "ConfigOverrides",
    +183    "ConfigurationError",
    +184    "Constants",
    +185    "CredentialSanitizer",
    +186    "DataLimit",
    +187    "DataLimitRequest",
    +188    "DataTransferred",
    +189    "DefaultAuditLogger",
    +190    "DevelopmentConfig",
    +191    "ErrorResponse",
    +192    "ExperimentalMetrics",
    +193    "HealthCheckResult",
    +194    "HostnameRequest",
    +195    "JsonDict",
    +196    "JsonPayload",
    +197    "LocationMetric",
    +198    "MetricsCollector",
    +199    "MetricsEnabledRequest",
    +200    "MetricsStatusResponse",
    +201    "MetricsTags",
    +202    "MultiServerManager",
    +203    "NoOpAuditLogger",
    +204    "NoOpMetrics",
    +205    "OutlineClientConfig",
    +206    "OutlineConnectionError",
    +207    "OutlineError",
    +208    "OutlineTimeoutError",
    +209    "PeakDeviceCount",
    +210    "PortRequest",
    +211    "ProductionConfig",
    +212    "QueryParams",
    +213    "ResponseData",
    +214    "ResponseParser",
    +215    "SecureIDGenerator",
    +216    "Server",
    +217    "ServerExperimentalMetric",
    +218    "ServerMetrics",
    +219    "ServerNameRequest",
    +220    "ServerSummary",
    +221    "TimestampMs",
    +222    "TimestampSec",
    +223    "TunnelTime",
    +224    "ValidationError",
    +225    "Validators",
    +226    "__author__",
    +227    "__email__",
    +228    "__license__",
    +229    "__version__",
    +230    "audited",
    +231    "build_config_overrides",
    +232    "correlation_id",
    +233    "create_client",
    +234    "create_env_template",
    +235    "create_multi_server_manager",
    +236    "format_error_chain",
    +237    "get_audit_logger",
    +238    "get_or_create_audit_logger",
    +239    "get_retry_delay",
    +240    "get_safe_error_dict",
    +241    "get_version",
    +242    "is_json_serializable",
    +243    "is_retryable",
    +244    "is_valid_bytes",
    +245    "is_valid_port",
    +246    "load_config",
    +247    "mask_sensitive_data",
    +248    "print_type_info",
    +249    "quick_setup",
    +250    "set_audit_logger",
    +251]
    +252
    +253
    +254# ===== Convenience Functions =====
    +255
    +256
    +257def get_version() -> str:
    +258    """Get package version string.
    +259
    +260    :return: Package version
    +261    """
    +262    return __version__
    +263
    +264
    +265def quick_setup() -> None:
    +266    """Create configuration template file for quick setup.
    +267
    +268    Creates `.env.example` file with all available configuration options.
    +269    """
    +270    create_env_template()
    +271    print("✅ Created .env.example")
    +272    print("📝 Edit the file with your server details")
    +273    print("🚀 Then use: AsyncOutlineClient.from_env()")
    +274
    +275
    +276def print_type_info() -> None:
    +277    """Print information about available type aliases for advanced usage."""
    +278    info = """
    +279🎯 PyOutlineAPI Type Aliases for Advanced Usage
    +280===============================================
    +281
    +282For creating custom AuditLogger:
    +283    from pyoutlineapi import AuditLogger, AuditDetails
    +284
    +285    class MyAuditLogger:
    +286        def log_action(
    +287            self,
    +288            action: str,
    +289            resource: str,
    +290            *,
    +291            details: AuditDetails | None = None,
    +292            ...
    +293        ) -> None: ...
    +294
    +295        async def alog_action(
    +296            self,
    +297            action: str,
    +298            resource: str,
    +299            *,
    +300            details: AuditDetails | None = None,
    +301            ...
    +302        ) -> None: ...
    +303
    +304For creating custom MetricsCollector:
    +305    from pyoutlineapi import MetricsCollector, MetricsTags
    +306
    +307    class MyMetrics:
    +308        def increment(
    +309            self,
    +310            metric: str,
    +311            *,
    +312            tags: MetricsTags | None = None
    +313        ) -> None: ...
    +314
    +315Available Type Aliases:
    +316    - TimestampMs, TimestampSec  # Unix timestamps
    +317    - JsonPayload, ResponseData  # JSON data types
    +318    - QueryParams                # URL query parameters
    +319    - AuditDetails               # Audit log details
    +320    - MetricsTags                # Metrics tags
    +321
    +322Constants and Validators:
    +323    from pyoutlineapi import Constants, Validators
    +324
    +325    # Access constants
    +326    Constants.RETRY_STATUS_CODES
    +327    Constants.MIN_PORT, Constants.MAX_PORT
    +328
    +329    # Use validators
    +330    Validators.validate_port(8080)
    +331    Validators.validate_key_id("my-key")
    +332
    +333Utility Classes:
    +334    from pyoutlineapi import (
    +335        CredentialSanitizer,
    +336        SecureIDGenerator,
    +337        ResponseParser,
    +338    )
    +339
    +340    # Sanitize sensitive data
    +341    safe_url = CredentialSanitizer.sanitize(url)
    +342
    +343    # Generate secure IDs
    +344    secure_id = SecureIDGenerator.generate()
    +345
    +346    # Parse API responses
    +347    parsed = ResponseParser.parse(data, Model)
    +348
    +349📖 Documentation: https://github.com/orenlab/pyoutlineapi
    +350    """
    +351    print(info)
    +352
    +353
    +354# ===== Better Error Messages =====
    +355
    +356
    +357def __getattr__(name: str) -> NoReturn:
    +358    """Provide helpful error messages for common mistakes.
    +359
    +360    :param name: Attribute name
    +361    :raises AttributeError: If attribute not found
    +362    """
    +363    mistakes = {
    +364        "OutlineClient": "Use 'AsyncOutlineClient' instead",
    +365        "OutlineSettings": "Use 'OutlineClientConfig' instead",
    +366        "create_resilient_client": (
    +367            "Use 'AsyncOutlineClient.from_env()' with 'enable_circuit_breaker=True'"
    +368        ),
    +369    }
    +370
    +371    if name in mistakes:
    +372        raise AttributeError(f"{name} not available. {mistakes[name]}")
    +373
    +374    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
    +375
    +376
    +377# ===== Interactive Help =====
    +378
    +379if TYPE_CHECKING:
    +380    import sys
    +381
    +382    if hasattr(sys, "ps1"):
    +383        # Show help in interactive mode
    +384        print(f"🚀 PyOutlineAPI v{__version__}")
    +385        print("💡 Quick start: pyoutlineapi.quick_setup()")
    +386        print("🎯 Type hints: pyoutlineapi.print_type_info()")
    +387        print("📚 Help: help(pyoutlineapi.AsyncOutlineClient)")
    +
    + + +
    +
    +
    + DEFAULT_SENSITIVE_KEYS = + + frozenset({'secret', 'accessUrl', 'cert_sha256', 'api_key', 'api_url', 'access_url', 'authorization', 'token', 'password', 'apikey', 'apiKey', 'apiUrl', 'certSha256'}) + + +
    + + + + +
    +
    + +
    + + class + APIError(pyoutlineapi.OutlineError): + + + +
    + +
    171class APIError(OutlineError):
    +172    """HTTP API request failure.
    +173
    +174    Automatically determines retry eligibility based on HTTP status code.
    +175
    +176    Attributes:
    +177        status_code: HTTP status code (if available)
    +178        endpoint: API endpoint that failed
    +179        response_data: Raw response data (may contain sensitive info)
    +180
    +181    Example:
    +182        >>> error = APIError("Not found", status_code=404, endpoint="/server")
    +183        >>> error.is_client_error  # True
    +184        >>> error.is_retryable  # False
    +185    """
    +186
    +187    __slots__ = ("endpoint", "response_data", "status_code")
    +188
    +189    def __init__(
    +190        self,
    +191        message: str,
    +192        *,
    +193        status_code: int | None = None,
    +194        endpoint: str | None = None,
    +195        response_data: dict[str, Any] | None = None,
    +196    ) -> None:
    +197        """Initialize API error with sanitized endpoint.
    +198
    +199        Args:
    +200            message: Error message
    +201            status_code: HTTP status code
    +202            endpoint: API endpoint (will be sanitized)
    +203            response_data: Response data (may contain sensitive info)
    +204        """
    +205        from .common_types import Validators
    +206
    +207        # Sanitize endpoint for safe logging
    +208        safe_endpoint = (
    +209            Validators.sanitize_endpoint_for_logging(endpoint) if endpoint else None
    +210        )
    +211
    +212        # Build safe details (optimization: avoid dict creation if all None)
    +213        safe_details: dict[str, Any] | None = None
    +214        if status_code is not None or safe_endpoint is not None:
    +215            safe_details = {}
    +216            if status_code is not None:
    +217                safe_details["status_code"] = status_code
    +218            if safe_endpoint is not None:
    +219                safe_details["endpoint"] = safe_endpoint
    +220
    +221        # Build internal details (optimization: avoid dict creation if all None)
    +222        details: dict[str, Any] | None = None
    +223        if status_code is not None or endpoint is not None:
    +224            details = {}
    +225            if status_code is not None:
    +226                details["status_code"] = status_code
    +227            if endpoint is not None:
    +228                details["endpoint"] = endpoint
    +229
    +230        super().__init__(message, details=details, safe_details=safe_details)
    +231
    +232        # Store attributes directly (faster access than dict lookups)
    +233        self.status_code = status_code
    +234        self.endpoint = endpoint
    +235        self.response_data = response_data
    +236
    +237    @property
    +238    def is_retryable(self) -> bool:
    +239        """Check if error is retryable based on status code."""
    +240        return self.status_code in Constants.RETRY_STATUS_CODES if self.status_code else False
    +241
    +242    @property
    +243    def is_client_error(self) -> bool:
    +244        """Check if error is a client error (4xx status).
    +245
    +246        Returns:
    +247            True if status code is 400-499
    +248        """
    +249        return self.status_code is not None and 400 <= self.status_code < 500
    +250
    +251    @property
    +252    def is_server_error(self) -> bool:
    +253        """Check if error is a server error (5xx status).
    +254
    +255        Returns:
    +256            True if status code is 500-599
    +257        """
    +258        return self.status_code is not None and 500 <= self.status_code < 600
    +259
    +260    @property
    +261    def is_rate_limit_error(self) -> bool:
    +262        """Check if error is a rate limit error (429 status).
    +263
    +264        Returns:
    +265            True if status code is 429
    +266        """
    +267        return self.status_code == 429
    +
    + + +

    HTTP API request failure.

    + +

    Automatically determines retry eligibility based on HTTP status code.

    + +
    Attributes:
    + +
      +
    • status_code: HTTP status code (if available)
    • +
    • endpoint: API endpoint that failed
    • +
    • response_data: Raw response data (may contain sensitive info)
    • +
    + +
    Example:
    + +
    +
    +
    >>> error = APIError("Not found", status_code=404, endpoint="/server")
    +>>> error.is_client_error  # True
    +>>> error.is_retryable  # False
    +
    +
    +
    +
    + + +
    + +
    + + APIError( message: str, *, status_code: int | None = None, endpoint: str | None = None, response_data: dict[str, typing.Any] | None = None) + + + +
    + +
    189    def __init__(
    +190        self,
    +191        message: str,
    +192        *,
    +193        status_code: int | None = None,
    +194        endpoint: str | None = None,
    +195        response_data: dict[str, Any] | None = None,
    +196    ) -> None:
    +197        """Initialize API error with sanitized endpoint.
    +198
    +199        Args:
    +200            message: Error message
    +201            status_code: HTTP status code
    +202            endpoint: API endpoint (will be sanitized)
    +203            response_data: Response data (may contain sensitive info)
    +204        """
    +205        from .common_types import Validators
    +206
    +207        # Sanitize endpoint for safe logging
    +208        safe_endpoint = (
    +209            Validators.sanitize_endpoint_for_logging(endpoint) if endpoint else None
    +210        )
    +211
    +212        # Build safe details (optimization: avoid dict creation if all None)
    +213        safe_details: dict[str, Any] | None = None
    +214        if status_code is not None or safe_endpoint is not None:
    +215            safe_details = {}
    +216            if status_code is not None:
    +217                safe_details["status_code"] = status_code
    +218            if safe_endpoint is not None:
    +219                safe_details["endpoint"] = safe_endpoint
    +220
    +221        # Build internal details (optimization: avoid dict creation if all None)
    +222        details: dict[str, Any] | None = None
    +223        if status_code is not None or endpoint is not None:
    +224            details = {}
    +225            if status_code is not None:
    +226                details["status_code"] = status_code
    +227            if endpoint is not None:
    +228                details["endpoint"] = endpoint
    +229
    +230        super().__init__(message, details=details, safe_details=safe_details)
    +231
    +232        # Store attributes directly (faster access than dict lookups)
    +233        self.status_code = status_code
    +234        self.endpoint = endpoint
    +235        self.response_data = response_data
    +
    + + +

    Initialize API error with sanitized endpoint.

    + +
    Arguments:
    + +
      +
    • message: Error message
    • +
    • status_code: HTTP status code
    • +
    • endpoint: API endpoint (will be sanitized)
    • +
    • response_data: Response data (may contain sensitive info)
    • +
    +
    + + +
    +
    +
    + status_code + + +
    + + + + +
    +
    +
    + endpoint + + +
    + + + + +
    +
    +
    + response_data + + +
    + + + + +
    +
    + +
    + is_retryable: bool + + + +
    + +
    237    @property
    +238    def is_retryable(self) -> bool:
    +239        """Check if error is retryable based on status code."""
    +240        return self.status_code in Constants.RETRY_STATUS_CODES if self.status_code else False
    +
    + + +

    Check if error is retryable based on status code.

    +
    + + +
    +
    + +
    + is_client_error: bool + + + +
    + +
    242    @property
    +243    def is_client_error(self) -> bool:
    +244        """Check if error is a client error (4xx status).
    +245
    +246        Returns:
    +247            True if status code is 400-499
    +248        """
    +249        return self.status_code is not None and 400 <= self.status_code < 500
    +
    + + +

    Check if error is a client error (4xx status).

    + +
    Returns:
    + +
    +

    True if status code is 400-499

    +
    +
    + + +
    +
    + +
    + is_server_error: bool + + + +
    + +
    251    @property
    +252    def is_server_error(self) -> bool:
    +253        """Check if error is a server error (5xx status).
    +254
    +255        Returns:
    +256            True if status code is 500-599
    +257        """
    +258        return self.status_code is not None and 500 <= self.status_code < 600
    +
    + + +

    Check if error is a server error (5xx status).

    + +
    Returns:
    + +
    +

    True if status code is 500-599

    +
    +
    + + +
    +
    + +
    + is_rate_limit_error: bool + + + +
    + +
    260    @property
    +261    def is_rate_limit_error(self) -> bool:
    +262        """Check if error is a rate limit error (429 status).
    +263
    +264        Returns:
    +265            True if status code is 429
    +266        """
    +267        return self.status_code == 429
    +
    + + +

    Check if error is a rate limit error (429 status).

    + +
    Returns:
    + +
    +

    True if status code is 429

    +
    +
    + + +
    +
    +
    + +
    + + class + AccessKey(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    136class AccessKey(BaseValidatedModel):
    +137    """Access key model matching API schema with optimized properties.
    +138
    +139    SCHEMA: Based on OpenAPI /access-keys endpoint
    +140    """
    +141
    +142    id: str
    +143    name: str | None = None
    +144    password: str
    +145    port: Port
    +146    method: str
    +147    access_url: str = Field(alias="accessUrl")
    +148    data_limit: DataLimit | None = Field(None, alias="dataLimit")
    +149
    +150    @field_validator("name", mode="before")
    +151    @classmethod
    +152    def validate_name(cls, v: str | None) -> str | None:
    +153        """Handle empty names from API.
    +154
    +155        :param v: Name value
    +156        :return: Validated name or None
    +157        """
    +158        if v is None:
    +159            return None
    +160        return Validators.validate_name(v)
    +161
    +162    @field_validator("id")
    +163    @classmethod
    +164    def validate_id(cls, v: str) -> str:
    +165        """Validate key ID.
    +166
    +167        :param v: Key ID
    +168        :return: Validated key ID
    +169        :raises ValueError: If ID is invalid
    +170        """
    +171        return Validators.validate_key_id(v)
    +172
    +173    @property
    +174    def has_data_limit(self) -> bool:
    +175        """Check if key has data limit (optimized None check).
    +176
    +177        :return: True if data limit exists
    +178        """
    +179        return self.data_limit is not None
    +180
    +181    @property
    +182    def display_name(self) -> str:
    +183        """Get display name with optimized conditional.
    +184
    +185        :return: Display name
    +186        """
    +187        return self.name if self.name else f"Key-{self.id}"
    +
    + + +

    Access key model matching API schema with optimized properties.

    + +

    SCHEMA: Based on OpenAPI /access-keys endpoint

    +
    + + +
    +
    + id: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + name: str | None = +None + + +
    + + + + +
    +
    +
    + password: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + port: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])] = +PydanticUndefined + + +
    + + +

    Port number (1-65535)

    +
    + + +
    +
    +
    + method: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + access_url: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + data_limit: DataLimit | None = +None + + +
    + + + + +
    +
    + +
    +
    @field_validator('name', mode='before')
    +
    @classmethod
    + + def + validate_name(cls, v: str | None) -> str | None: + + + +
    + +
    150    @field_validator("name", mode="before")
    +151    @classmethod
    +152    def validate_name(cls, v: str | None) -> str | None:
    +153        """Handle empty names from API.
    +154
    +155        :param v: Name value
    +156        :return: Validated name or None
    +157        """
    +158        if v is None:
    +159            return None
    +160        return Validators.validate_name(v)
    +
    + + +

    Handle empty names from API.

    + +
    Parameters
    + +
      +
    • v: Name value
    • +
    + +
    Returns
    + +
    +

    Validated name or None

    +
    +
    + + +
    +
    + +
    +
    @field_validator('id')
    +
    @classmethod
    + + def + validate_id(cls, v: str) -> str: + + + +
    + +
    162    @field_validator("id")
    +163    @classmethod
    +164    def validate_id(cls, v: str) -> str:
    +165        """Validate key ID.
    +166
    +167        :param v: Key ID
    +168        :return: Validated key ID
    +169        :raises ValueError: If ID is invalid
    +170        """
    +171        return Validators.validate_key_id(v)
    +
    + + +

    Validate key ID.

    + +
    Parameters
    + +
      +
    • v: Key ID
    • +
    + +
    Returns
    + +
    +

    Validated key ID

    +
    + +
    Raises
    + +
      +
    • ValueError: If ID is invalid
    • +
    +
    + + +
    +
    + +
    + has_data_limit: bool + + + +
    + +
    173    @property
    +174    def has_data_limit(self) -> bool:
    +175        """Check if key has data limit (optimized None check).
    +176
    +177        :return: True if data limit exists
    +178        """
    +179        return self.data_limit is not None
    +
    + + +

    Check if key has data limit (optimized None check).

    + +
    Returns
    + +
    +

    True if data limit exists

    +
    +
    + + +
    +
    + +
    + display_name: str + + + +
    + +
    181    @property
    +182    def display_name(self) -> str:
    +183        """Get display name with optimized conditional.
    +184
    +185        :return: Display name
    +186        """
    +187        return self.name if self.name else f"Key-{self.id}"
    +
    + + +

    Get display name with optimized conditional.

    + +
    Returns
    + +
    +

    Display name

    +
    +
    + + +
    +
    +
    + +
    + + class + AccessKeyCreateRequest(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    484class AccessKeyCreateRequest(BaseValidatedModel):
    +485    """Request model for creating access keys.
    +486
    +487    SCHEMA: Based on POST /access-keys request body
    +488    """
    +489
    +490    name: str | None = Field(default=None, min_length=1, max_length=255)
    +491    method: str | None = None
    +492    password: str | None = None
    +493    port: Port | None = None
    +494    limit: DataLimit | None = None
    +
    + + +

    Request model for creating access keys.

    + +

    SCHEMA: Based on POST /access-keys request body

    +
    + + +
    +
    + name: str | None = +None + + +
    + + + + +
    +
    +
    + method: str | None = +None + + +
    + + + + +
    +
    +
    + password: str | None = +None + + +
    + + + + +
    +
    +
    + port: Optional[Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])]] = +None + + +
    + + + + +
    +
    +
    + limit: DataLimit | None = +None + + +
    + + + + +
    +
    +
    + +
    + + class + AccessKeyList(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    190class AccessKeyList(BaseValidatedModel):
    +191    """List of access keys with optimized utility methods.
    +192
    +193    SCHEMA: Based on GET /access-keys response
    +194    """
    +195
    +196    access_keys: list[AccessKey] = Field(alias="accessKeys")
    +197
    +198    @cached_property
    +199    def count(self) -> int:
    +200        """Get number of access keys (cached).
    +201
    +202        NOTE: Cached because list is immutable after creation
    +203
    +204        :return: Key count
    +205        """
    +206        return len(self.access_keys)
    +207
    +208    @property
    +209    def is_empty(self) -> bool:
    +210        """Check if list is empty (uses cached count).
    +211
    +212        :return: True if no keys
    +213        """
    +214        return self.count == 0
    +215
    +216    def get_by_id(self, key_id: str) -> AccessKey | None:
    +217        """Get key by ID with early return optimization.
    +218
    +219        :param key_id: Access key ID
    +220        :return: Access key or None if not found
    +221        """
    +222        for key in self.access_keys:
    +223            if key.id == key_id:
    +224                return key
    +225        return None
    +226
    +227    def get_by_name(self, name: str) -> list[AccessKey]:
    +228        """Get keys by name with optimized list comprehension.
    +229
    +230        :param name: Key name
    +231        :return: List of matching keys (may be multiple)
    +232        """
    +233        return [key for key in self.access_keys if key.name == name]
    +234
    +235    def filter_with_limits(self) -> list[AccessKey]:
    +236        """Get keys with data limits (optimized comprehension).
    +237
    +238        :return: List of keys with limits
    +239        """
    +240        return [key for key in self.access_keys if key.has_data_limit]
    +241
    +242    def filter_without_limits(self) -> list[AccessKey]:
    +243        """Get keys without data limits (optimized comprehension).
    +244
    +245        :return: List of keys without limits
    +246        """
    +247        return [key for key in self.access_keys if not key.has_data_limit]
    +
    + + +

    List of access keys with optimized utility methods.

    + +

    SCHEMA: Based on GET /access-keys response

    +
    + + +
    +
    + access_keys: list[AccessKey] = +PydanticUndefined + + +
    + + + + +
    +
    + +
    + count: int + + + +
    + +
    198    @cached_property
    +199    def count(self) -> int:
    +200        """Get number of access keys (cached).
    +201
    +202        NOTE: Cached because list is immutable after creation
    +203
    +204        :return: Key count
    +205        """
    +206        return len(self.access_keys)
    +
    + + +

    Get number of access keys (cached).

    + +

    NOTE: Cached because list is immutable after creation

    + +
    Returns
    + +
    +

    Key count

    +
    +
    + + +
    +
    + +
    + is_empty: bool + + + +
    + +
    208    @property
    +209    def is_empty(self) -> bool:
    +210        """Check if list is empty (uses cached count).
    +211
    +212        :return: True if no keys
    +213        """
    +214        return self.count == 0
    +
    + + +

    Check if list is empty (uses cached count).

    + +
    Returns
    + +
    +

    True if no keys

    +
    +
    + + +
    +
    + +
    + + def + get_by_id(self, key_id: str) -> AccessKey | None: + + + +
    + +
    216    def get_by_id(self, key_id: str) -> AccessKey | None:
    +217        """Get key by ID with early return optimization.
    +218
    +219        :param key_id: Access key ID
    +220        :return: Access key or None if not found
    +221        """
    +222        for key in self.access_keys:
    +223            if key.id == key_id:
    +224                return key
    +225        return None
    +
    + + +

    Get key by ID with early return optimization.

    + +
    Parameters
    + +
      +
    • key_id: Access key ID
    • +
    + +
    Returns
    + +
    +

    Access key or None if not found

    +
    +
    + + +
    +
    + +
    + + def + get_by_name(self, name: str) -> list[AccessKey]: + + + +
    + +
    227    def get_by_name(self, name: str) -> list[AccessKey]:
    +228        """Get keys by name with optimized list comprehension.
    +229
    +230        :param name: Key name
    +231        :return: List of matching keys (may be multiple)
    +232        """
    +233        return [key for key in self.access_keys if key.name == name]
    +
    + + +

    Get keys by name with optimized list comprehension.

    + +
    Parameters
    + +
      +
    • name: Key name
    • +
    + +
    Returns
    + +
    +

    List of matching keys (may be multiple)

    +
    +
    + + +
    +
    + +
    + + def + filter_with_limits(self) -> list[AccessKey]: + + + +
    + +
    235    def filter_with_limits(self) -> list[AccessKey]:
    +236        """Get keys with data limits (optimized comprehension).
    +237
    +238        :return: List of keys with limits
    +239        """
    +240        return [key for key in self.access_keys if key.has_data_limit]
    +
    + + +

    Get keys with data limits (optimized comprehension).

    + +
    Returns
    + +
    +

    List of keys with limits

    +
    +
    + + +
    +
    + +
    + + def + filter_without_limits(self) -> list[AccessKey]: + + + +
    + +
    242    def filter_without_limits(self) -> list[AccessKey]:
    +243        """Get keys without data limits (optimized comprehension).
    +244
    +245        :return: List of keys without limits
    +246        """
    +247        return [key for key in self.access_keys if not key.has_data_limit]
    +
    + + +

    Get keys without data limits (optimized comprehension).

    + +
    Returns
    + +
    +

    List of keys without limits

    +
    +
    + + +
    +
    +
    + +
    + + class + AccessKeyMetric(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    436class AccessKeyMetric(BaseValidatedModel):
    +437    """Per-key experimental metrics.
    +438
    +439    SCHEMA: Based on experimental metrics accessKeys array item
    +440    """
    +441
    +442    access_key_id: str = Field(alias="accessKeyId")
    +443    tunnel_time: TunnelTime = Field(alias="tunnelTime")
    +444    data_transferred: DataTransferred = Field(alias="dataTransferred")
    +445    connection: ConnectionInfo
    +
    + + +

    Per-key experimental metrics.

    + +

    SCHEMA: Based on experimental metrics accessKeys array item

    +
    + + +
    +
    + access_key_id: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + tunnel_time: TunnelTime = +PydanticUndefined + + +
    + + + + +
    +
    +
    + data_transferred: DataTransferred = +PydanticUndefined + + +
    + + + + +
    +
    +
    + connection: pyoutlineapi.models.ConnectionInfo = +PydanticUndefined + + +
    + + + + +
    +
    +
    + +
    + + class + AccessKeyNameRequest(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    524class AccessKeyNameRequest(BaseValidatedModel):
    +525    """Request model for renaming access key.
    +526
    +527    SCHEMA: Based on PUT /access-keys/{id}/name request body
    +528    """
    +529
    +530    name: str = Field(min_length=1, max_length=255)
    +
    + + +

    Request model for renaming access key.

    + +

    SCHEMA: Based on PUT /access-keys/{id}/name request body

    +
    + + +
    +
    + name: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + +
    + + class + AsyncOutlineClient(pyoutlineapi.base_client.BaseHTTPClient, pyoutlineapi.api_mixins.ServerMixin, pyoutlineapi.api_mixins.AccessKeyMixin, pyoutlineapi.api_mixins.DataLimitMixin, pyoutlineapi.api_mixins.MetricsMixin): + + + +
    + +
     46class AsyncOutlineClient(
    + 47    BaseHTTPClient,
    + 48    ServerMixin,
    + 49    AccessKeyMixin,
    + 50    DataLimitMixin,
    + 51    MetricsMixin,
    + 52):
    + 53    """High-performance async client for Outline VPN Server API."""
    + 54
    + 55    __slots__ = (
    + 56        "_audit_logger_instance",
    + 57        "_config",
    + 58        "_default_json_format",
    + 59    )
    + 60
    + 61    def __init__(
    + 62        self,
    + 63        config: OutlineClientConfig | None = None,
    + 64        *,
    + 65        api_url: str | None = None,
    + 66        cert_sha256: str | None = None,
    + 67        audit_logger: AuditLogger | None = None,
    + 68        metrics: MetricsCollector | None = None,
    + 69        **overrides: Unpack[ConfigOverrides],
    + 70    ) -> None:
    + 71        """Initialize Outline client with modern configuration approach.
    + 72
    + 73        Uses structural pattern matching for configuration resolution.
    + 74
    + 75        :param config: Client configuration object
    + 76        :param api_url: API URL (alternative to config)
    + 77        :param cert_sha256: Certificate fingerprint (alternative to config)
    + 78        :param audit_logger: Custom audit logger
    + 79        :param metrics: Custom metrics collector
    + 80        :param overrides: Configuration overrides (timeout, retry_attempts, etc.)
    + 81        :raises ConfigurationError: If configuration is invalid
    + 82
    + 83        Example:
    + 84            >>> async with AsyncOutlineClient.from_env() as client:
    + 85            ...     info = await client.get_server_info()
    + 86        """
    + 87        # Build config_kwargs using utility function (DRY)
    + 88        config_kwargs = build_config_overrides(**overrides)
    + 89
    + 90        # Validate configuration using pattern matching
    + 91        resolved_config = self._resolve_configuration(
    + 92            config, api_url, cert_sha256, config_kwargs
    + 93        )
    + 94
    + 95        self._config = resolved_config
    + 96        self._audit_logger_instance = audit_logger
    + 97        self._default_json_format = resolved_config.json_format
    + 98
    + 99        # Initialize base HTTP client
    +100        super().__init__(
    +101            api_url=resolved_config.api_url,
    +102            cert_sha256=resolved_config.cert_sha256,
    +103            timeout=resolved_config.timeout,
    +104            retry_attempts=resolved_config.retry_attempts,
    +105            max_connections=resolved_config.max_connections,
    +106            user_agent=resolved_config.user_agent,
    +107            enable_logging=resolved_config.enable_logging,
    +108            circuit_config=resolved_config.circuit_config,
    +109            rate_limit=resolved_config.rate_limit,
    +110            allow_private_networks=resolved_config.allow_private_networks,
    +111            resolve_dns_for_ssrf=resolved_config.resolve_dns_for_ssrf,
    +112            audit_logger=audit_logger,
    +113            metrics=metrics,
    +114        )
    +115
    +116        # Cache instance for weak reference tracking (automatic cleanup)
    +117        _client_cache[id(self)] = self
    +118
    +119        if resolved_config.enable_logging and logger.isEnabledFor(logging.INFO):
    +120            safe_url = Validators.sanitize_url_for_logging(self.api_url)
    +121            logger.info("Client initialized for %s", safe_url)
    +122
    +123    @staticmethod
    +124    def _resolve_configuration(
    +125        config: OutlineClientConfig | None,
    +126        api_url: str | None,
    +127        cert_sha256: str | None,
    +128        kwargs: dict[str, Any],
    +129    ) -> OutlineClientConfig:
    +130        """Resolve and validate configuration using pattern matching.
    +131
    +132        :param config: Configuration object
    +133        :param api_url: Direct API URL
    +134        :param cert_sha256: Direct certificate
    +135        :param kwargs: Additional kwargs
    +136        :return: Resolved configuration
    +137        :raises ConfigurationError: If configuration is invalid
    +138        """
    +139        match config, api_url, cert_sha256:
    +140            # Pattern 1: Direct parameters provided (most common case)
    +141            case None, str(url), str(cert) if url and cert:
    +142                return OutlineClientConfig.create_minimal(url, cert, **kwargs)
    +143
    +144            # Pattern 2: Config object provided
    +145            case OutlineClientConfig() as cfg, None, None:
    +146                return cfg
    +147
    +148            # Pattern 3: Missing required parameters
    +149            case None, None, _:
    +150                raise ConfigurationError(
    +151                    "Missing required 'api_url'",
    +152                    field="api_url",
    +153                    security_issue=False,
    +154                )
    +155            case None, _, None:
    +156                raise ConfigurationError(
    +157                    "Missing required 'cert_sha256'",
    +158                    field="cert_sha256",
    +159                    security_issue=True,
    +160                )
    +161            case None, None, None:
    +162                raise ConfigurationError(
    +163                    "Either provide 'config' or both 'api_url' and 'cert_sha256'"
    +164                )
    +165
    +166            # Pattern 4: Conflicting parameters
    +167            case OutlineClientConfig(), str() | None, str() | None:
    +168                raise ConfigurationError(
    +169                    "Cannot specify both 'config' and direct parameters"
    +170                )
    +171
    +172            # Pattern 5: Invalid combination (catch-all)
    +173            case _:
    +174                raise ConfigurationError("Invalid parameter combination")
    +175
    +176    @property
    +177    def config(self) -> OutlineClientConfig:
    +178        """Get immutable copy of configuration.
    +179
    +180        :return: Deep copy of configuration
    +181        """
    +182        return self._config.model_copy_immutable()
    +183
    +184    @property
    +185    def get_sanitized_config(self) -> dict[str, Any]:
    +186        """Delegate to config's sanitized representation.
    +187
    +188        See: OutlineClientConfig.get_sanitized_config().
    +189
    +190        :return: Sanitized configuration from underlying config object
    +191        """
    +192        return self._config.get_sanitized_config
    +193
    +194    @property
    +195    def json_format(self) -> bool:
    +196        """Get JSON format preference.
    +197
    +198        :return: True if raw JSON format is preferred
    +199        """
    +200        return self._default_json_format
    +201
    +202    # ===== Factory Methods =====
    +203
    +204    @classmethod
    +205    @asynccontextmanager
    +206    async def create(
    +207        cls,
    +208        api_url: str | None = None,
    +209        cert_sha256: str | None = None,
    +210        *,
    +211        config: OutlineClientConfig | None = None,
    +212        audit_logger: AuditLogger | None = None,
    +213        metrics: MetricsCollector | None = None,
    +214        **overrides: Unpack[ConfigOverrides],
    +215    ) -> AsyncGenerator[AsyncOutlineClient, None]:
    +216        """Create and initialize client as async context manager.
    +217
    +218        Automatically handles initialization and cleanup.
    +219        Recommended way to create clients in async contexts.
    +220
    +221        :param api_url: API URL
    +222        :param cert_sha256: Certificate fingerprint
    +223        :param config: Configuration object
    +224        :param audit_logger: Custom audit logger
    +225        :param metrics: Custom metrics collector
    +226        :param overrides: Configuration overrides (timeout, retry_attempts, etc.)
    +227        :yield: Initialized client instance
    +228        :raises ConfigurationError: If configuration is invalid
    +229
    +230        Example:
    +231            >>> async with AsyncOutlineClient.from_env() as client:
    +232            ...     keys = await client.get_access_keys()
    +233        """
    +234        if config is not None:
    +235            client = cls(config=config, audit_logger=audit_logger, metrics=metrics)
    +236        else:
    +237            client = cls(
    +238                api_url=api_url,
    +239                cert_sha256=cert_sha256,
    +240                audit_logger=audit_logger,
    +241                metrics=metrics,
    +242                **overrides,
    +243            )
    +244
    +245        async with client:
    +246            yield client
    +247
    +248    @classmethod
    +249    def from_env(
    +250        cls,
    +251        *,
    +252        env_file: str | Path | None = None,
    +253        audit_logger: AuditLogger | None = None,
    +254        metrics: MetricsCollector | None = None,
    +255        **overrides: Unpack[ConfigOverrides],
    +256    ) -> AsyncOutlineClient:
    +257        """Create client from environment variables.
    +258
    +259        Reads configuration from environment or .env file.
    +260        Modern approach using **overrides for runtime configuration.
    +261
    +262        :param env_file: Path to environment file (.env)
    +263        :param audit_logger: Custom audit logger
    +264        :param metrics: Custom metrics collector
    +265        :param overrides: Configuration overrides (timeout, enable_logging, etc.)
    +266        :return: Configured client instance
    +267        :raises ConfigurationError: If environment configuration is invalid
    +268
    +269        Example:
    +270            >>> async with AsyncOutlineClient.from_env(
    +271            ...     env_file=".env.production",
    +272            ...     timeout=20,
    +273            ... ) as client:
    +274            ...     info = await client.get_server_info()
    +275        """
    +276        config = OutlineClientConfig.from_env(env_file=env_file, **overrides)
    +277        return cls(config=config, audit_logger=audit_logger, metrics=metrics)
    +278
    +279    # ===== Context Manager Methods =====
    +280
    +281    async def __aexit__(
    +282        self,
    +283        exc_type: type[BaseException] | None,
    +284        exc_val: BaseException | None,
    +285        exc_tb: object | None,
    +286    ) -> None:
    +287        """Async context manager exit with comprehensive cleanup.
    +288
    +289        Ensures graceful shutdown even on exceptions. Uses ordered cleanup
    +290        sequence for proper resource deallocation.
    +291
    +292        Cleanup order:
    +293        1. Audit logger shutdown (drain queue)
    +294        2. HTTP client shutdown (close connections)
    +295        3. Emergency cleanup if steps 1-2 failed
    +296
    +297        :param exc_type: Exception type if error occurred
    +298        :param exc_val: Exception instance if error occurred
    +299        :param exc_tb: Exception traceback
    +300        :return: False to propagate exceptions
    +301        """
    +302        cleanup_errors: list[str] = []
    +303
    +304        # Step 1: Graceful audit logger shutdown
    +305        if self._audit_logger_instance is not None:
    +306            try:
    +307                if hasattr(self._audit_logger_instance, "shutdown"):
    +308                    shutdown_method = self._audit_logger_instance.shutdown
    +309                    if asyncio.iscoroutinefunction(shutdown_method):
    +310                        await shutdown_method()
    +311            except Exception as e:
    +312                error_msg = f"Audit logger shutdown error: {e}"
    +313                cleanup_errors.append(error_msg)
    +314                if logger.isEnabledFor(logging.WARNING):
    +315                    logger.warning(error_msg)
    +316
    +317        # Step 2: Shutdown HTTP client
    +318        try:
    +319            await self.shutdown(timeout=30.0)
    +320        except Exception as e:
    +321            error_msg = f"HTTP client shutdown error: {e}"
    +322            cleanup_errors.append(error_msg)
    +323            if logger.isEnabledFor(logging.ERROR):
    +324                logger.error(error_msg)
    +325
    +326        # Step 3: Emergency cleanup if shutdown failed
    +327        if cleanup_errors and hasattr(self, "_session"):
    +328            try:
    +329                if self._session and not self._session.closed:
    +330                    await self._session.close()
    +331                    if logger.isEnabledFor(logging.DEBUG):
    +332                        logger.debug("Emergency session cleanup completed")
    +333            except Exception as e:
    +334                if logger.isEnabledFor(logging.DEBUG):
    +335                    logger.debug("Emergency cleanup error: %s", e)
    +336
    +337        # Log summary of cleanup issues
    +338        if cleanup_errors and logger.isEnabledFor(logging.WARNING):
    +339            logger.warning(
    +340                "Cleanup completed with %d error(s): %s",
    +341                len(cleanup_errors),
    +342                "; ".join(cleanup_errors),
    +343            )
    +344
    +345        # Always propagate the original exception
    +346        return None
    +347
    +348    # ===== Utility Methods =====
    +349
    +350    async def health_check(self) -> dict[str, Any]:
    +351        """Perform basic health check.
    +352
    +353        Non-intrusive check that tests server connectivity without
    +354        modifying any state. Returns comprehensive health metrics.
    +355
    +356        :return: Health check result dictionary with response time
    +357
    +358        Example result:
    +359            {
    +360                "timestamp": 1234567890.123,
    +361                "healthy": True,
    +362                "response_time_ms": 45.2,
    +363                "connected": True,
    +364                "circuit_state": "closed",
    +365                "active_requests": 2,
    +366                "rate_limit_available": 98
    +367            }
    +368        """
    +369        import time
    +370
    +371        health_data: dict[str, Any] = {
    +372            "timestamp": time.time(),
    +373            "connected": self.is_connected,
    +374            "circuit_state": self.circuit_state,
    +375            "active_requests": self.active_requests,
    +376            "rate_limit_available": self.available_slots,
    +377        }
    +378
    +379        try:
    +380            start_time = time.monotonic()
    +381            await self.get_server_info()
    +382            duration = time.monotonic() - start_time
    +383
    +384            health_data["healthy"] = True
    +385            health_data["response_time_ms"] = round(duration * 1000, 2)
    +386
    +387        except Exception as e:
    +388            health_data["healthy"] = False
    +389            health_data["error"] = str(e)
    +390            health_data["error_type"] = type(e).__name__
    +391
    +392        return health_data
    +393
    +394    async def get_server_summary(self) -> dict[str, Any]:
    +395        """Get comprehensive server overview.
    +396
    +397        Aggregates multiple API calls into a single summary.
    +398        Continues on partial failures to return maximum information.
    +399        Executes non-dependent calls concurrently for performance.
    +400
    +401        :return: Server summary dictionary with aggregated data
    +402
    +403        Example result:
    +404            {
    +405                "timestamp": 1234567890.123,
    +406                "healthy": True,
    +407                "server": {...},
    +408                "access_keys_count": 10,
    +409                "metrics_enabled": True,
    +410                "transfer_metrics": {...},
    +411                "client_status": {...},
    +412                "errors": []
    +413            }
    +414        """
    +415        import time
    +416
    +417        summary: dict[str, Any] = {
    +418            "timestamp": time.time(),
    +419            "healthy": True,
    +420            "errors": [],
    +421        }
    +422
    +423        server_task = self.get_server_info(as_json=True)
    +424        keys_task = self.get_access_keys(as_json=True)
    +425        metrics_status_task = self.get_metrics_status(as_json=True)
    +426
    +427        server_result, keys_result, metrics_status_result = await asyncio.gather(
    +428            server_task, keys_task, metrics_status_task, return_exceptions=True
    +429        )
    +430
    +431        # Process server info
    +432        if isinstance(server_result, Exception):
    +433            summary["healthy"] = False
    +434            summary["errors"].append(f"Server info error: {server_result}")
    +435            if logger.isEnabledFor(logging.DEBUG):
    +436                logger.debug("Failed to fetch server info: %s", server_result)
    +437        else:
    +438            summary["server"] = server_result
    +439
    +440        # Process access keys
    +441        if isinstance(keys_result, Exception):
    +442            summary["healthy"] = False
    +443            summary["errors"].append(f"Access keys error: {keys_result}")
    +444            if logger.isEnabledFor(logging.DEBUG):
    +445                logger.debug("Failed to fetch access keys: %s", keys_result)
    +446        elif isinstance(keys_result, dict):
    +447            keys_list = keys_result.get("accessKeys", [])
    +448            summary["access_keys_count"] = (
    +449                len(keys_list) if isinstance(keys_list, list) else 0
    +450            )
    +451        elif isinstance(keys_result, AccessKeyList):
    +452            summary["access_keys_count"] = len(keys_result.access_keys)
    +453        else:
    +454            summary["access_keys_count"] = 0
    +455
    +456        # Process metrics status
    +457        if isinstance(metrics_status_result, Exception):
    +458            summary["errors"].append(f"Metrics status error: {metrics_status_result}")
    +459            if logger.isEnabledFor(logging.DEBUG):
    +460                logger.debug(
    +461                    "Failed to fetch metrics status: %s", metrics_status_result
    +462                )
    +463        elif isinstance(metrics_status_result, dict):
    +464            metrics_enabled = bool(metrics_status_result.get("metricsEnabled", False))
    +465            summary["metrics_enabled"] = metrics_enabled
    +466
    +467            # Fetch transfer metrics if enabled (dependent call - sequential)
    +468            if metrics_enabled:
    +469                try:
    +470                    transfer = await self.get_transfer_metrics(as_json=True)
    +471                    summary["transfer_metrics"] = transfer
    +472                except Exception as e:
    +473                    summary["errors"].append(f"Transfer metrics error: {e}")
    +474                    if logger.isEnabledFor(logging.DEBUG):
    +475                        logger.debug("Failed to fetch transfer metrics: %s", e)
    +476        elif isinstance(metrics_status_result, MetricsStatusResponse):
    +477            summary["metrics_enabled"] = metrics_status_result.metrics_enabled
    +478            if metrics_status_result.metrics_enabled:
    +479                try:
    +480                    transfer = await self.get_transfer_metrics(as_json=True)
    +481                    summary["transfer_metrics"] = transfer
    +482                except Exception as e:
    +483                    summary["errors"].append(f"Transfer metrics error: {e}")
    +484                    if logger.isEnabledFor(logging.DEBUG):
    +485                        logger.debug("Failed to fetch transfer metrics: %s", e)
    +486        else:
    +487            summary["metrics_enabled"] = False
    +488
    +489        # Add client status (synchronous, no API call)
    +490        summary["client_status"] = {
    +491            "connected": self.is_connected,
    +492            "circuit_state": self.circuit_state,
    +493            "active_requests": self.active_requests,
    +494            "rate_limit": {
    +495                "limit": self.rate_limit,
    +496                "available": self.available_slots,
    +497            },
    +498        }
    +499
    +500        return summary
    +501
    +502    def get_status(self) -> dict[str, Any]:
    +503        """Get current client status (synchronous).
    +504
    +505        Returns immediate status without making API calls.
    +506        Useful for monitoring and debugging.
    +507
    +508        :return: Status dictionary with all client metrics
    +509
    +510        Example result:
    +511            {
    +512                "connected": True,
    +513                "circuit_state": "closed",
    +514                "active_requests": 2,
    +515                "rate_limit": {
    +516                    "limit": 100,
    +517                    "available": 98,
    +518                    "active": 2
    +519                },
    +520                "circuit_metrics": {...}
    +521            }
    +522        """
    +523        return {
    +524            "connected": self.is_connected,
    +525            "circuit_state": self.circuit_state,
    +526            "active_requests": self.active_requests,
    +527            "rate_limit": {
    +528                "limit": self.rate_limit,
    +529                "available": self.available_slots,
    +530                "active": self.active_requests,
    +531            },
    +532            "circuit_metrics": self.get_circuit_metrics(),
    +533        }
    +534
    +535    def __repr__(self) -> str:
    +536        """Safe string representation without secrets.
    +537
    +538        Does not expose any sensitive information (URLs, certificates, tokens).
    +539
    +540        :return: String representation
    +541        """
    +542        status = "connected" if self.is_connected else "disconnected"
    +543        parts = [f"status={status}"]
    +544
    +545        if self.circuit_state:
    +546            parts.append(f"circuit={self.circuit_state}")
    +547
    +548        if self.active_requests:
    +549            parts.append(f"requests={self.active_requests}")
    +550
    +551        return f"AsyncOutlineClient({', '.join(parts)})"
    +
    + + +

    High-performance async client for Outline VPN Server API.

    +
    + + +
    + +
    + + AsyncOutlineClient( config: OutlineClientConfig | None = None, *, api_url: str | None = None, cert_sha256: str | None = None, audit_logger: AuditLogger | None = None, metrics: MetricsCollector | None = None, **overrides: Unpack[ConfigOverrides]) + + + +
    + +
     61    def __init__(
    + 62        self,
    + 63        config: OutlineClientConfig | None = None,
    + 64        *,
    + 65        api_url: str | None = None,
    + 66        cert_sha256: str | None = None,
    + 67        audit_logger: AuditLogger | None = None,
    + 68        metrics: MetricsCollector | None = None,
    + 69        **overrides: Unpack[ConfigOverrides],
    + 70    ) -> None:
    + 71        """Initialize Outline client with modern configuration approach.
    + 72
    + 73        Uses structural pattern matching for configuration resolution.
    + 74
    + 75        :param config: Client configuration object
    + 76        :param api_url: API URL (alternative to config)
    + 77        :param cert_sha256: Certificate fingerprint (alternative to config)
    + 78        :param audit_logger: Custom audit logger
    + 79        :param metrics: Custom metrics collector
    + 80        :param overrides: Configuration overrides (timeout, retry_attempts, etc.)
    + 81        :raises ConfigurationError: If configuration is invalid
    + 82
    + 83        Example:
    + 84            >>> async with AsyncOutlineClient.from_env() as client:
    + 85            ...     info = await client.get_server_info()
    + 86        """
    + 87        # Build config_kwargs using utility function (DRY)
    + 88        config_kwargs = build_config_overrides(**overrides)
    + 89
    + 90        # Validate configuration using pattern matching
    + 91        resolved_config = self._resolve_configuration(
    + 92            config, api_url, cert_sha256, config_kwargs
    + 93        )
    + 94
    + 95        self._config = resolved_config
    + 96        self._audit_logger_instance = audit_logger
    + 97        self._default_json_format = resolved_config.json_format
    + 98
    + 99        # Initialize base HTTP client
    +100        super().__init__(
    +101            api_url=resolved_config.api_url,
    +102            cert_sha256=resolved_config.cert_sha256,
    +103            timeout=resolved_config.timeout,
    +104            retry_attempts=resolved_config.retry_attempts,
    +105            max_connections=resolved_config.max_connections,
    +106            user_agent=resolved_config.user_agent,
    +107            enable_logging=resolved_config.enable_logging,
    +108            circuit_config=resolved_config.circuit_config,
    +109            rate_limit=resolved_config.rate_limit,
    +110            allow_private_networks=resolved_config.allow_private_networks,
    +111            resolve_dns_for_ssrf=resolved_config.resolve_dns_for_ssrf,
    +112            audit_logger=audit_logger,
    +113            metrics=metrics,
    +114        )
    +115
    +116        # Cache instance for weak reference tracking (automatic cleanup)
    +117        _client_cache[id(self)] = self
    +118
    +119        if resolved_config.enable_logging and logger.isEnabledFor(logging.INFO):
    +120            safe_url = Validators.sanitize_url_for_logging(self.api_url)
    +121            logger.info("Client initialized for %s", safe_url)
    +
    + + +

    Initialize Outline client with modern configuration approach.

    + +

    Uses structural pattern matching for configuration resolution.

    + +
    Parameters
    + +
      +
    • config: Client configuration object
    • +
    • api_url: API URL (alternative to config)
    • +
    • cert_sha256: Certificate fingerprint (alternative to config)
    • +
    • audit_logger: Custom audit logger
    • +
    • metrics: Custom metrics collector
    • +
    • overrides: Configuration overrides (timeout, retry_attempts, etc.)
    • +
    + +
    Raises
    + +
      +
    • ConfigurationError: If configuration is invalid
    • +
    + +
    Example:
    + +
    +
    +
    >>> async with AsyncOutlineClient.from_env() as client:
    +...     info = await client.get_server_info()
    +
    +
    +
    +
    + + +
    +
    + +
    + config: OutlineClientConfig + + + +
    + +
    176    @property
    +177    def config(self) -> OutlineClientConfig:
    +178        """Get immutable copy of configuration.
    +179
    +180        :return: Deep copy of configuration
    +181        """
    +182        return self._config.model_copy_immutable()
    +
    + + +

    Get immutable copy of configuration.

    + +
    Returns
    + +
    +

    Deep copy of configuration

    +
    +
    + + +
    +
    + +
    + get_sanitized_config: dict[str, typing.Any] + + + +
    + +
    184    @property
    +185    def get_sanitized_config(self) -> dict[str, Any]:
    +186        """Delegate to config's sanitized representation.
    +187
    +188        See: OutlineClientConfig.get_sanitized_config().
    +189
    +190        :return: Sanitized configuration from underlying config object
    +191        """
    +192        return self._config.get_sanitized_config
    +
    + + +

    Delegate to config's sanitized representation.

    + +

    See: OutlineClientConfig.get_sanitized_config().

    + +
    Returns
    + +
    +

    Sanitized configuration from underlying config object

    +
    +
    + + +
    +
    + +
    + json_format: bool + + + +
    + +
    194    @property
    +195    def json_format(self) -> bool:
    +196        """Get JSON format preference.
    +197
    +198        :return: True if raw JSON format is preferred
    +199        """
    +200        return self._default_json_format
    +
    + + +

    Get JSON format preference.

    + +
    Returns
    + +
    +

    True if raw JSON format is preferred

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    +
    @asynccontextmanager
    + + def + create( cls, api_url: str | None = None, cert_sha256: str | None = None, *, config: OutlineClientConfig | None = None, audit_logger: AuditLogger | None = None, metrics: MetricsCollector | None = None, **overrides: Unpack[ConfigOverrides]) -> AsyncGenerator[AsyncOutlineClient, None]: + + + +
    + +
    204    @classmethod
    +205    @asynccontextmanager
    +206    async def create(
    +207        cls,
    +208        api_url: str | None = None,
    +209        cert_sha256: str | None = None,
    +210        *,
    +211        config: OutlineClientConfig | None = None,
    +212        audit_logger: AuditLogger | None = None,
    +213        metrics: MetricsCollector | None = None,
    +214        **overrides: Unpack[ConfigOverrides],
    +215    ) -> AsyncGenerator[AsyncOutlineClient, None]:
    +216        """Create and initialize client as async context manager.
    +217
    +218        Automatically handles initialization and cleanup.
    +219        Recommended way to create clients in async contexts.
    +220
    +221        :param api_url: API URL
    +222        :param cert_sha256: Certificate fingerprint
    +223        :param config: Configuration object
    +224        :param audit_logger: Custom audit logger
    +225        :param metrics: Custom metrics collector
    +226        :param overrides: Configuration overrides (timeout, retry_attempts, etc.)
    +227        :yield: Initialized client instance
    +228        :raises ConfigurationError: If configuration is invalid
    +229
    +230        Example:
    +231            >>> async with AsyncOutlineClient.from_env() as client:
    +232            ...     keys = await client.get_access_keys()
    +233        """
    +234        if config is not None:
    +235            client = cls(config=config, audit_logger=audit_logger, metrics=metrics)
    +236        else:
    +237            client = cls(
    +238                api_url=api_url,
    +239                cert_sha256=cert_sha256,
    +240                audit_logger=audit_logger,
    +241                metrics=metrics,
    +242                **overrides,
    +243            )
    +244
    +245        async with client:
    +246            yield client
    +
    + + +

    Create and initialize client as async context manager.

    + +

    Automatically handles initialization and cleanup. +Recommended way to create clients in async contexts.

    + +
    Parameters
    + +
      +
    • api_url: API URL
    • +
    • cert_sha256: Certificate fingerprint
    • +
    • config: Configuration object
    • +
    • audit_logger: Custom audit logger
    • +
    • metrics: Custom metrics collector
    • +
    • overrides: Configuration overrides (timeout, retry_attempts, etc.) +:yield: Initialized client instance
    • +
    + +
    Raises
    + +
      +
    • ConfigurationError: If configuration is invalid
    • +
    + +
    Example:
    + +
    +
    +
    >>> async with AsyncOutlineClient.from_env() as client:
    +...     keys = await client.get_access_keys()
    +
    +
    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_env( cls, *, env_file: str | pathlib._local.Path | None = None, audit_logger: AuditLogger | None = None, metrics: MetricsCollector | None = None, **overrides: Unpack[ConfigOverrides]) -> AsyncOutlineClient: + + + +
    + +
    248    @classmethod
    +249    def from_env(
    +250        cls,
    +251        *,
    +252        env_file: str | Path | None = None,
    +253        audit_logger: AuditLogger | None = None,
    +254        metrics: MetricsCollector | None = None,
    +255        **overrides: Unpack[ConfigOverrides],
    +256    ) -> AsyncOutlineClient:
    +257        """Create client from environment variables.
    +258
    +259        Reads configuration from environment or .env file.
    +260        Modern approach using **overrides for runtime configuration.
    +261
    +262        :param env_file: Path to environment file (.env)
    +263        :param audit_logger: Custom audit logger
    +264        :param metrics: Custom metrics collector
    +265        :param overrides: Configuration overrides (timeout, enable_logging, etc.)
    +266        :return: Configured client instance
    +267        :raises ConfigurationError: If environment configuration is invalid
    +268
    +269        Example:
    +270            >>> async with AsyncOutlineClient.from_env(
    +271            ...     env_file=".env.production",
    +272            ...     timeout=20,
    +273            ... ) as client:
    +274            ...     info = await client.get_server_info()
    +275        """
    +276        config = OutlineClientConfig.from_env(env_file=env_file, **overrides)
    +277        return cls(config=config, audit_logger=audit_logger, metrics=metrics)
    +
    + + +

    Create client from environment variables.

    + +

    Reads configuration from environment or .env file. +Modern approach using **overrides for runtime configuration.

    + +
    Parameters
    + +
      +
    • env_file: Path to environment file (.env)
    • +
    • audit_logger: Custom audit logger
    • +
    • metrics: Custom metrics collector
    • +
    • overrides: Configuration overrides (timeout, enable_logging, etc.)
    • +
    + +
    Returns
    + +
    +

    Configured client instance

    +
    + +
    Raises
    + +
      +
    • ConfigurationError: If environment configuration is invalid
    • +
    + +
    Example:
    + +
    +
    +
    >>> async with AsyncOutlineClient.from_env(
    +...     env_file=".env.production",
    +...     timeout=20,
    +... ) as client:
    +...     info = await client.get_server_info()
    +
    +
    +
    +
    + + +
    +
    + +
    + + async def + health_check(self) -> dict[str, typing.Any]: + + + +
    + +
    350    async def health_check(self) -> dict[str, Any]:
    +351        """Perform basic health check.
    +352
    +353        Non-intrusive check that tests server connectivity without
    +354        modifying any state. Returns comprehensive health metrics.
    +355
    +356        :return: Health check result dictionary with response time
    +357
    +358        Example result:
    +359            {
    +360                "timestamp": 1234567890.123,
    +361                "healthy": True,
    +362                "response_time_ms": 45.2,
    +363                "connected": True,
    +364                "circuit_state": "closed",
    +365                "active_requests": 2,
    +366                "rate_limit_available": 98
    +367            }
    +368        """
    +369        import time
    +370
    +371        health_data: dict[str, Any] = {
    +372            "timestamp": time.time(),
    +373            "connected": self.is_connected,
    +374            "circuit_state": self.circuit_state,
    +375            "active_requests": self.active_requests,
    +376            "rate_limit_available": self.available_slots,
    +377        }
    +378
    +379        try:
    +380            start_time = time.monotonic()
    +381            await self.get_server_info()
    +382            duration = time.monotonic() - start_time
    +383
    +384            health_data["healthy"] = True
    +385            health_data["response_time_ms"] = round(duration * 1000, 2)
    +386
    +387        except Exception as e:
    +388            health_data["healthy"] = False
    +389            health_data["error"] = str(e)
    +390            health_data["error_type"] = type(e).__name__
    +391
    +392        return health_data
    +
    + + +

    Perform basic health check.

    + +

    Non-intrusive check that tests server connectivity without +modifying any state. Returns comprehensive health metrics.

    + +
    Returns
    + +
    +

    Health check result dictionary with response time

    +
    + +
    Example result:
    + +
    +

    { + "timestamp": 1234567890.123, + "healthy": True, + "response_time_ms": 45.2, + "connected": True, + "circuit_state": "closed", + "active_requests": 2, + "rate_limit_available": 98 + }

    +
    +
    + + +
    +
    + +
    + + async def + get_server_summary(self) -> dict[str, typing.Any]: + + + +
    + +
    394    async def get_server_summary(self) -> dict[str, Any]:
    +395        """Get comprehensive server overview.
    +396
    +397        Aggregates multiple API calls into a single summary.
    +398        Continues on partial failures to return maximum information.
    +399        Executes non-dependent calls concurrently for performance.
    +400
    +401        :return: Server summary dictionary with aggregated data
    +402
    +403        Example result:
    +404            {
    +405                "timestamp": 1234567890.123,
    +406                "healthy": True,
    +407                "server": {...},
    +408                "access_keys_count": 10,
    +409                "metrics_enabled": True,
    +410                "transfer_metrics": {...},
    +411                "client_status": {...},
    +412                "errors": []
    +413            }
    +414        """
    +415        import time
    +416
    +417        summary: dict[str, Any] = {
    +418            "timestamp": time.time(),
    +419            "healthy": True,
    +420            "errors": [],
    +421        }
    +422
    +423        server_task = self.get_server_info(as_json=True)
    +424        keys_task = self.get_access_keys(as_json=True)
    +425        metrics_status_task = self.get_metrics_status(as_json=True)
    +426
    +427        server_result, keys_result, metrics_status_result = await asyncio.gather(
    +428            server_task, keys_task, metrics_status_task, return_exceptions=True
    +429        )
    +430
    +431        # Process server info
    +432        if isinstance(server_result, Exception):
    +433            summary["healthy"] = False
    +434            summary["errors"].append(f"Server info error: {server_result}")
    +435            if logger.isEnabledFor(logging.DEBUG):
    +436                logger.debug("Failed to fetch server info: %s", server_result)
    +437        else:
    +438            summary["server"] = server_result
    +439
    +440        # Process access keys
    +441        if isinstance(keys_result, Exception):
    +442            summary["healthy"] = False
    +443            summary["errors"].append(f"Access keys error: {keys_result}")
    +444            if logger.isEnabledFor(logging.DEBUG):
    +445                logger.debug("Failed to fetch access keys: %s", keys_result)
    +446        elif isinstance(keys_result, dict):
    +447            keys_list = keys_result.get("accessKeys", [])
    +448            summary["access_keys_count"] = (
    +449                len(keys_list) if isinstance(keys_list, list) else 0
    +450            )
    +451        elif isinstance(keys_result, AccessKeyList):
    +452            summary["access_keys_count"] = len(keys_result.access_keys)
    +453        else:
    +454            summary["access_keys_count"] = 0
    +455
    +456        # Process metrics status
    +457        if isinstance(metrics_status_result, Exception):
    +458            summary["errors"].append(f"Metrics status error: {metrics_status_result}")
    +459            if logger.isEnabledFor(logging.DEBUG):
    +460                logger.debug(
    +461                    "Failed to fetch metrics status: %s", metrics_status_result
    +462                )
    +463        elif isinstance(metrics_status_result, dict):
    +464            metrics_enabled = bool(metrics_status_result.get("metricsEnabled", False))
    +465            summary["metrics_enabled"] = metrics_enabled
    +466
    +467            # Fetch transfer metrics if enabled (dependent call - sequential)
    +468            if metrics_enabled:
    +469                try:
    +470                    transfer = await self.get_transfer_metrics(as_json=True)
    +471                    summary["transfer_metrics"] = transfer
    +472                except Exception as e:
    +473                    summary["errors"].append(f"Transfer metrics error: {e}")
    +474                    if logger.isEnabledFor(logging.DEBUG):
    +475                        logger.debug("Failed to fetch transfer metrics: %s", e)
    +476        elif isinstance(metrics_status_result, MetricsStatusResponse):
    +477            summary["metrics_enabled"] = metrics_status_result.metrics_enabled
    +478            if metrics_status_result.metrics_enabled:
    +479                try:
    +480                    transfer = await self.get_transfer_metrics(as_json=True)
    +481                    summary["transfer_metrics"] = transfer
    +482                except Exception as e:
    +483                    summary["errors"].append(f"Transfer metrics error: {e}")
    +484                    if logger.isEnabledFor(logging.DEBUG):
    +485                        logger.debug("Failed to fetch transfer metrics: %s", e)
    +486        else:
    +487            summary["metrics_enabled"] = False
    +488
    +489        # Add client status (synchronous, no API call)
    +490        summary["client_status"] = {
    +491            "connected": self.is_connected,
    +492            "circuit_state": self.circuit_state,
    +493            "active_requests": self.active_requests,
    +494            "rate_limit": {
    +495                "limit": self.rate_limit,
    +496                "available": self.available_slots,
    +497            },
    +498        }
    +499
    +500        return summary
    +
    + + +

    Get comprehensive server overview.

    + +

    Aggregates multiple API calls into a single summary. +Continues on partial failures to return maximum information. +Executes non-dependent calls concurrently for performance.

    + +
    Returns
    + +
    +

    Server summary dictionary with aggregated data

    +
    + +
    Example result:
    + +
    +

    { + "timestamp": 1234567890.123, + "healthy": True, + "server": {...}, + "access_keys_count": 10, + "metrics_enabled": True, + "transfer_metrics": {...}, + "client_status": {...}, + "errors": [] + }

    +
    +
    + + +
    +
    + +
    + + def + get_status(self) -> dict[str, typing.Any]: + + + +
    + +
    502    def get_status(self) -> dict[str, Any]:
    +503        """Get current client status (synchronous).
    +504
    +505        Returns immediate status without making API calls.
    +506        Useful for monitoring and debugging.
    +507
    +508        :return: Status dictionary with all client metrics
    +509
    +510        Example result:
    +511            {
    +512                "connected": True,
    +513                "circuit_state": "closed",
    +514                "active_requests": 2,
    +515                "rate_limit": {
    +516                    "limit": 100,
    +517                    "available": 98,
    +518                    "active": 2
    +519                },
    +520                "circuit_metrics": {...}
    +521            }
    +522        """
    +523        return {
    +524            "connected": self.is_connected,
    +525            "circuit_state": self.circuit_state,
    +526            "active_requests": self.active_requests,
    +527            "rate_limit": {
    +528                "limit": self.rate_limit,
    +529                "available": self.available_slots,
    +530                "active": self.active_requests,
    +531            },
    +532            "circuit_metrics": self.get_circuit_metrics(),
    +533        }
    +
    + + +

    Get current client status (synchronous).

    + +

    Returns immediate status without making API calls. +Useful for monitoring and debugging.

    + +
    Returns
    + +
    +

    Status dictionary with all client metrics

    +
    + +
    Example result:
    + +
    +

    { + "connected": True, + "circuit_state": "closed", + "active_requests": 2, + "rate_limit": { + "limit": 100, + "available": 98, + "active": 2 + }, + "circuit_metrics": {...} + }

    +
    +
    + + +
    +
    +
    + +
    +
    @dataclass(slots=True, frozen=True)
    + + class + AuditContext: + + + +
    + +
     57@dataclass(slots=True, frozen=True)
    + 58class AuditContext:
    + 59    """Immutable audit context extracted from function call.
    + 60
    + 61    Uses structural pattern matching and signature inspection for smart extraction.
    + 62    """
    + 63
    + 64    action: str
    + 65    resource: str
    + 66    success: bool
    + 67    details: dict[str, Any] = field(default_factory=dict)
    + 68    correlation_id: str | None = None
    + 69
    + 70    @classmethod
    + 71    def from_call(
    + 72        cls,
    + 73        func: Callable[..., Any],
    + 74        instance: object,
    + 75        args: tuple[Any, ...],
    + 76        kwargs: dict[str, Any],
    + 77        result: object = None,
    + 78        exception: Exception | None = None,
    + 79    ) -> AuditContext:
    + 80        """Build audit context from function call with intelligent extraction.
    + 81
    + 82        :param func: Function being audited
    + 83        :param instance: Instance (self) for methods
    + 84        :param args: Positional arguments
    + 85        :param kwargs: Keyword arguments
    + 86        :param result: Function result (if successful)
    + 87        :param exception: Exception (if failed)
    + 88        :return: Complete audit context
    + 89        """
    + 90        success = exception is None
    + 91
    + 92        # Extract action from function name (snake_case -> action)
    + 93        action = func.__name__
    + 94
    + 95        # Smart resource extraction
    + 96        resource = cls._extract_resource(func, args, kwargs, result, success)
    + 97
    + 98        # Smart details extraction with automatic sanitization
    + 99        details = cls._extract_details(func, args, kwargs, result, exception, success)
    +100
    +101        # Correlation ID from instance if available
    +102        correlation_id = getattr(instance, "_correlation_id", None)
    +103
    +104        return cls(
    +105            action=action,
    +106            resource=resource,
    +107            success=success,
    +108            details=details,
    +109            correlation_id=correlation_id,
    +110        )
    +111
    +112    @staticmethod
    +113    def _extract_resource(
    +114        func: Callable[..., Any],
    +115        args: tuple[Any, ...],
    +116        kwargs: dict[str, Any],
    +117        result: object,
    +118        success: bool,
    +119    ) -> str:
    +120        """Smart resource extraction using structural pattern matching.
    +121
    +122        Priority:
    +123        1. result.id (for create operations)
    +124        2. Known resource parameter names (key_id, id, resource_id)
    +125        3. First meaningful argument
    +126        4. Function name analysis
    +127        5. 'unknown' fallback
    +128
    +129        :param func: Function being audited
    +130        :param args: Positional arguments
    +131        :param kwargs: Keyword arguments
    +132        :param result: Function result
    +133        :param success: Whether operation succeeded
    +134        :return: Resource identifier
    +135        """
    +136        # Pattern 1: Extract from successful result
    +137        if success and result is not None:
    +138            match result:
    +139                case _ if hasattr(result, "id"):
    +140                    return str(result.id)
    +141                case dict() if "id" in result:
    +142                    return str(result["id"])
    +143
    +144        # Pattern 2: Extract from known parameter names
    +145        sig = inspect.signature(func)
    +146        params = list(sig.parameters.keys())
    +147
    +148        # Skip 'self' and 'cls'
    +149        params = [p for p in params if p not in ("self", "cls")]
    +150
    +151        # Try common resource identifiers in priority order
    +152        for resource_param in ("key_id", "id", "resource_id", "user_id", "name"):
    +153            if resource_param in kwargs:
    +154                return str(kwargs[resource_param])
    +155
    +156        # Pattern 3: First meaningful parameter
    +157        if params and params[0] in kwargs:
    +158            return str(kwargs[params[0]])
    +159
    +160        # Pattern 4: First positional argument (after self)
    +161        if args:
    +162            return str(args[0])
    +163
    +164        # Pattern 5: Analyze function name for hints
    +165        func_name = func.__name__.lower()
    +166        if any(keyword in func_name for keyword in ("server", "global", "system")):
    +167            return "server"
    +168
    +169        return "unknown"
    +170
    +171    @staticmethod
    +172    def _extract_details(
    +173        func: Callable[..., Any],
    +174        args: tuple[Any, ...],
    +175        kwargs: dict[str, Any],
    +176        result: object,
    +177        exception: Exception | None,
    +178        success: bool,
    +179    ) -> dict[str, Any]:
    +180        """Smart details extraction using signature introspection.
    +181
    +182        Only includes meaningful parameters (excludes technical ones and None values).
    +183        Automatically sanitizes sensitive data.
    +184
    +185        :param func: Function being audited
    +186        :param args: Positional arguments
    +187        :param kwargs: Keyword arguments
    +188        :param result: Function result
    +189        :param exception: Exception if failed
    +190        :param success: Whether operation succeeded
    +191        :return: Sanitized details dictionary
    +192        """
    +193        details: dict[str, Any] = {"success": success}
    +194
    +195        # Signature-based extraction
    +196        sig = inspect.signature(func)
    +197
    +198        # Parameters to exclude from details
    +199        excluded = {"self", "cls", "as_json", "return_raw"}
    +200
    +201        for param_name, param in sig.parameters.items():
    +202            if param_name in excluded:
    +203                continue
    +204
    +205            # Get actual value
    +206            value = kwargs.get(param_name)
    +207
    +208            # Only include meaningful values (not None, not default)
    +209            if value is not None and value != param.default:
    +210                # Convert complex objects to simple representations
    +211                match value:
    +212                    case _ if hasattr(value, "model_dump"):
    +213                        # Pydantic models
    +214                        details[param_name] = value.model_dump(exclude_none=True)
    +215                    case dict():
    +216                        details[param_name] = value
    +217                    case list() | tuple():
    +218                        details[param_name] = len(value)  # Count, not content
    +219                    case _:
    +220                        details[param_name] = value
    +221
    +222        # Add error information if present
    +223        if exception:
    +224            details["error"] = str(exception)
    +225            details["error_type"] = type(exception).__name__
    +226
    +227        # Sanitize sensitive data
    +228        return _sanitize_details(details)
    +
    + + +

    Immutable audit context extracted from function call.

    + +

    Uses structural pattern matching and signature inspection for smart extraction.

    +
    + + +
    +
    + + AuditContext( action: str, resource: str, success: bool, details: dict[str, typing.Any] = <factory>, correlation_id: str | None = None) + + +
    + + + + +
    +
    +
    + action: str + + +
    + + + + +
    +
    +
    + resource: str + + +
    + + + + +
    +
    +
    + success: bool + + +
    + + + + +
    +
    +
    + details: dict[str, typing.Any] + + +
    + + + + +
    +
    +
    + correlation_id: str | None + + +
    + + + + +
    +
    + +
    +
    @classmethod
    + + def + from_call( cls, func: Callable[..., typing.Any], instance: object, args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any], result: object = None, exception: Exception | None = None) -> AuditContext: + + + +
    + +
     70    @classmethod
    + 71    def from_call(
    + 72        cls,
    + 73        func: Callable[..., Any],
    + 74        instance: object,
    + 75        args: tuple[Any, ...],
    + 76        kwargs: dict[str, Any],
    + 77        result: object = None,
    + 78        exception: Exception | None = None,
    + 79    ) -> AuditContext:
    + 80        """Build audit context from function call with intelligent extraction.
    + 81
    + 82        :param func: Function being audited
    + 83        :param instance: Instance (self) for methods
    + 84        :param args: Positional arguments
    + 85        :param kwargs: Keyword arguments
    + 86        :param result: Function result (if successful)
    + 87        :param exception: Exception (if failed)
    + 88        :return: Complete audit context
    + 89        """
    + 90        success = exception is None
    + 91
    + 92        # Extract action from function name (snake_case -> action)
    + 93        action = func.__name__
    + 94
    + 95        # Smart resource extraction
    + 96        resource = cls._extract_resource(func, args, kwargs, result, success)
    + 97
    + 98        # Smart details extraction with automatic sanitization
    + 99        details = cls._extract_details(func, args, kwargs, result, exception, success)
    +100
    +101        # Correlation ID from instance if available
    +102        correlation_id = getattr(instance, "_correlation_id", None)
    +103
    +104        return cls(
    +105            action=action,
    +106            resource=resource,
    +107            success=success,
    +108            details=details,
    +109            correlation_id=correlation_id,
    +110        )
    +
    + + +

    Build audit context from function call with intelligent extraction.

    + +
    Parameters
    + +
      +
    • func: Function being audited
    • +
    • instance: Instance (self) for methods
    • +
    • args: Positional arguments
    • +
    • kwargs: Keyword arguments
    • +
    • result: Function result (if successful)
    • +
    • exception: Exception (if failed)
    • +
    + +
    Returns
    + +
    +

    Complete audit context

    +
    +
    + + +
    +
    +
    + +
    +
    @runtime_checkable
    + + class + AuditLogger(typing.Protocol): + + + +
    + +
    234@runtime_checkable
    +235class AuditLogger(Protocol):
    +236    """Protocol for audit logging implementations.
    +237
    +238    Designed for async-first applications with sync fallback support.
    +239    """
    +240
    +241    async def alog_action(
    +242        self,
    +243        action: str,
    +244        resource: str,
    +245        *,
    +246        user: str | None = None,
    +247        details: dict[str, Any] | None = None,
    +248        correlation_id: str | None = None,
    +249    ) -> None:
    +250        """Log auditable action asynchronously (primary method)."""
    +251        ...
    +252
    +253    def log_action(
    +254        self,
    +255        action: str,
    +256        resource: str,
    +257        *,
    +258        user: str | None = None,
    +259        details: dict[str, Any] | None = None,
    +260        correlation_id: str | None = None,
    +261    ) -> None:
    +262        """Log auditable action synchronously (fallback method)."""
    +263        ...
    +264
    +265    async def shutdown(self) -> None:
    +266        """Gracefully shutdown logger."""
    +267        ...
    +
    + + +

    Protocol for audit logging implementations.

    + +

    Designed for async-first applications with sync fallback support.

    +
    + + +
    + +
    + + AuditLogger(*args, **kwargs) + + + +
    + +
    1957def _no_init_or_replace_init(self, *args, **kwargs):
    +1958    cls = type(self)
    +1959
    +1960    if cls._is_protocol:
    +1961        raise TypeError('Protocols cannot be instantiated')
    +1962
    +1963    # Already using a custom `__init__`. No need to calculate correct
    +1964    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
    +1965    if cls.__init__ is not _no_init_or_replace_init:
    +1966        return
    +1967
    +1968    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
    +1969    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
    +1970    # searches for a proper new `__init__` in the MRO. The new `__init__`
    +1971    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
    +1972    # instantiation of the protocol subclass will thus use the new
    +1973    # `__init__` and no longer call `_no_init_or_replace_init`.
    +1974    for base in cls.__mro__:
    +1975        init = base.__dict__.get('__init__', _no_init_or_replace_init)
    +1976        if init is not _no_init_or_replace_init:
    +1977            cls.__init__ = init
    +1978            break
    +1979    else:
    +1980        # should not happen
    +1981        cls.__init__ = object.__init__
    +1982
    +1983    cls.__init__(self, *args, **kwargs)
    +
    + + + + +
    +
    + +
    + + async def + alog_action( self, action: str, resource: str, *, user: str | None = None, details: dict[str, typing.Any] | None = None, correlation_id: str | None = None) -> None: + + + +
    + +
    241    async def alog_action(
    +242        self,
    +243        action: str,
    +244        resource: str,
    +245        *,
    +246        user: str | None = None,
    +247        details: dict[str, Any] | None = None,
    +248        correlation_id: str | None = None,
    +249    ) -> None:
    +250        """Log auditable action asynchronously (primary method)."""
    +251        ...
    +
    + + +

    Log auditable action asynchronously (primary method).

    +
    + + +
    +
    + +
    + + def + log_action( self, action: str, resource: str, *, user: str | None = None, details: dict[str, typing.Any] | None = None, correlation_id: str | None = None) -> None: + + + +
    + +
    253    def log_action(
    +254        self,
    +255        action: str,
    +256        resource: str,
    +257        *,
    +258        user: str | None = None,
    +259        details: dict[str, Any] | None = None,
    +260        correlation_id: str | None = None,
    +261    ) -> None:
    +262        """Log auditable action synchronously (fallback method)."""
    +263        ...
    +
    + + +

    Log auditable action synchronously (fallback method).

    +
    + + +
    +
    + +
    + + async def + shutdown(self) -> None: + + + +
    + +
    265    async def shutdown(self) -> None:
    +266        """Gracefully shutdown logger."""
    +267        ...
    +
    + + +

    Gracefully shutdown logger.

    +
    + + +
    +
    +
    + +
    + + class + BandwidthData(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    383class BandwidthData(BaseValidatedModel):
    +384    """Bandwidth measurement data.
    +385
    +386    SCHEMA: Based on experimental metrics bandwidth current/peak object
    +387    """
    +388
    +389    data: BandwidthDataValue
    +390    timestamp: TimestampSec | None = None
    +
    + + +

    Bandwidth measurement data.

    + +

    SCHEMA: Based on experimental metrics bandwidth current/peak object

    +
    + + +
    +
    + data: BandwidthDataValue = +PydanticUndefined + + +
    + + + + +
    +
    +
    + timestamp: Optional[Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in seconds', metadata=[Ge(ge=0)])]] = +None + + +
    + + + + +
    +
    +
    + +
    + + class + BandwidthDataValue(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    374class BandwidthDataValue(BaseValidatedModel):
    +375    """Bandwidth data value.
    +376
    +377    SCHEMA: Based on experimental metrics bandwidth data object
    +378    """
    +379
    +380    bytes: int
    +
    + + +

    Bandwidth data value.

    + +

    SCHEMA: Based on experimental metrics bandwidth data object

    +
    + + +
    +
    + bytes: int = +PydanticUndefined + + +
    + + + + +
    +
    +
    + +
    + + class + BandwidthInfo(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    393class BandwidthInfo(BaseValidatedModel):
    +394    """Current and peak bandwidth information.
    +395
    +396    SCHEMA: Based on experimental metrics bandwidth object
    +397    """
    +398
    +399    current: BandwidthData
    +400    peak: BandwidthData
    +
    + + +

    Current and peak bandwidth information.

    + +

    SCHEMA: Based on experimental metrics bandwidth object

    +
    + + +
    +
    + current: BandwidthData = +PydanticUndefined + + +
    + + + + +
    +
    +
    + peak: BandwidthData = +PydanticUndefined + + +
    + + + + +
    +
    +
    + +
    +
    @dataclass(frozen=True, slots=True)
    + + class + CircuitConfig: + + + +
    + +
    49@dataclass(frozen=True, slots=True)
    +50class CircuitConfig:
    +51    """Circuit breaker configuration with validation.
    +52
    +53    Immutable configuration to prevent runtime modification.
    +54    Uses slots for memory efficiency (~40 bytes per instance).
    +55    """
    +56
    +57    failure_threshold: int = 5
    +58    recovery_timeout: float = 60.0
    +59    success_threshold: int = 2
    +60    call_timeout: float = 10.0
    +61
    +62    def __post_init__(self) -> None:
    +63        """Validate configuration at creation time.
    +64
    +65        :raises ValueError: If any configuration value is invalid
    +66        """
    +67        if self.failure_threshold < 1:
    +68            raise ValueError("failure_threshold must be >= 1")
    +69        if self.recovery_timeout < 1.0:
    +70            raise ValueError("recovery_timeout must be >= 1.0")
    +71        if self.success_threshold < 1:
    +72            raise ValueError("success_threshold must be >= 1")
    +73        if self.call_timeout < 0.1:
    +74            raise ValueError("call_timeout must be >= 0.1")
    +
    + + +

    Circuit breaker configuration with validation.

    + +

    Immutable configuration to prevent runtime modification. +Uses slots for memory efficiency (~40 bytes per instance).

    +
    + + +
    +
    + + CircuitConfig( failure_threshold: int = 5, recovery_timeout: float = 60.0, success_threshold: int = 2, call_timeout: float = 10.0) + + +
    + + + + +
    +
    +
    + failure_threshold: int + + +
    + + + + +
    +
    +
    + recovery_timeout: float + + +
    + + + + +
    +
    +
    + success_threshold: int + + +
    + + + + +
    +
    +
    + call_timeout: float + + +
    + + + + +
    +
    +
    + +
    +
    @dataclass(slots=True)
    + + class + CircuitMetrics: + + + +
    + +
     77@dataclass(slots=True)
    + 78class CircuitMetrics:
    + 79    """Circuit breaker metrics with efficient storage.
    + 80
    + 81    Uses slots for memory efficiency (~80 bytes per instance).
    + 82    All calculations are O(1) with no allocations.
    + 83    """
    + 84
    + 85    total_calls: int = 0
    + 86    successful_calls: int = 0
    + 87    failed_calls: int = 0
    + 88    state_changes: int = 0
    + 89    last_failure_time: float = 0.0
    + 90    last_success_time: float = 0.0
    + 91
    + 92    @property
    + 93    def success_rate(self) -> float:
    + 94        """Calculate success rate (O(1), no allocations).
    + 95
    + 96        :return: Success rate as decimal (0.0 to 1.0)
    + 97        """
    + 98        if self.total_calls == 0:
    + 99            return 1.0
    +100        return self.successful_calls / self.total_calls
    +101
    +102    @property
    +103    def failure_rate(self) -> float:
    +104        """Calculate failure rate (O(1), no allocations).
    +105
    +106        :return: Failure rate as decimal (0.0 to 1.0)
    +107        """
    +108        return 1.0 - self.success_rate
    +109
    +110    def to_dict(self) -> dict[str, int | float]:
    +111        """Convert metrics to dictionary for serialization.
    +112
    +113        Pre-computes rates to avoid repeated calculations.
    +114
    +115        :return: Dictionary representation
    +116        """
    +117        success_rate = self.success_rate  # Calculate once
    +118        return {
    +119            "total_calls": self.total_calls,
    +120            "successful_calls": self.successful_calls,
    +121            "failed_calls": self.failed_calls,
    +122            "state_changes": self.state_changes,
    +123            "success_rate": success_rate,
    +124            "failure_rate": 1.0 - success_rate,  # Reuse calculation
    +125            "last_failure_time": self.last_failure_time,
    +126            "last_success_time": self.last_success_time,
    +127        }
    +
    + + +

    Circuit breaker metrics with efficient storage.

    + +

    Uses slots for memory efficiency (~80 bytes per instance). +All calculations are O(1) with no allocations.

    +
    + + +
    +
    + + CircuitMetrics( total_calls: int = 0, successful_calls: int = 0, failed_calls: int = 0, state_changes: int = 0, last_failure_time: float = 0.0, last_success_time: float = 0.0) + + +
    + + + + +
    +
    +
    + total_calls: int + + +
    + + + + +
    +
    +
    + successful_calls: int + + +
    + + + + +
    +
    +
    + failed_calls: int + + +
    + + + + +
    +
    +
    + state_changes: int + + +
    + + + + +
    +
    +
    + last_failure_time: float + + +
    + + + + +
    +
    +
    + last_success_time: float + + +
    + + + + +
    +
    + +
    + success_rate: float + + + +
    + +
     92    @property
    + 93    def success_rate(self) -> float:
    + 94        """Calculate success rate (O(1), no allocations).
    + 95
    + 96        :return: Success rate as decimal (0.0 to 1.0)
    + 97        """
    + 98        if self.total_calls == 0:
    + 99            return 1.0
    +100        return self.successful_calls / self.total_calls
    +
    + + +

    Calculate success rate (O(1), no allocations).

    + +
    Returns
    + +
    +

    Success rate as decimal (0.0 to 1.0)

    +
    +
    + + +
    +
    + +
    + failure_rate: float + + + +
    + +
    102    @property
    +103    def failure_rate(self) -> float:
    +104        """Calculate failure rate (O(1), no allocations).
    +105
    +106        :return: Failure rate as decimal (0.0 to 1.0)
    +107        """
    +108        return 1.0 - self.success_rate
    +
    + + +

    Calculate failure rate (O(1), no allocations).

    + +
    Returns
    + +
    +

    Failure rate as decimal (0.0 to 1.0)

    +
    +
    + + +
    +
    + +
    + + def + to_dict(self) -> dict[str, int | float]: + + + +
    + +
    110    def to_dict(self) -> dict[str, int | float]:
    +111        """Convert metrics to dictionary for serialization.
    +112
    +113        Pre-computes rates to avoid repeated calculations.
    +114
    +115        :return: Dictionary representation
    +116        """
    +117        success_rate = self.success_rate  # Calculate once
    +118        return {
    +119            "total_calls": self.total_calls,
    +120            "successful_calls": self.successful_calls,
    +121            "failed_calls": self.failed_calls,
    +122            "state_changes": self.state_changes,
    +123            "success_rate": success_rate,
    +124            "failure_rate": 1.0 - success_rate,  # Reuse calculation
    +125            "last_failure_time": self.last_failure_time,
    +126            "last_success_time": self.last_success_time,
    +127        }
    +
    + + +

    Convert metrics to dictionary for serialization.

    + +

    Pre-computes rates to avoid repeated calculations.

    + +
    Returns
    + +
    +

    Dictionary representation

    +
    +
    + + +
    +
    +
    + +
    + + class + CircuitOpenError(pyoutlineapi.OutlineError): + + + +
    + +
    270class CircuitOpenError(OutlineError):
    +271    """Circuit breaker is open due to repeated failures.
    +272
    +273    Indicates temporary service unavailability. Clients should wait
    +274    for ``retry_after`` seconds before retrying.
    +275
    +276    Attributes:
    +277        retry_after: Seconds to wait before retry
    +278
    +279    Example:
    +280        >>> error = CircuitOpenError("Circuit open", retry_after=60.0)
    +281        >>> error.is_retryable  # True
    +282        >>> error.retry_after  # 60.0
    +283    """
    +284
    +285    __slots__ = ("retry_after",)
    +286
    +287    _is_retryable: ClassVar[bool] = True
    +288
    +289    def __init__(self, message: str, *, retry_after: float = 60.0) -> None:
    +290        """Initialize circuit open error.
    +291
    +292        Args:
    +293            message: Error message
    +294            retry_after: Seconds to wait before retry
    +295
    +296        Raises:
    +297            ValueError: If retry_after is negative
    +298        """
    +299        if retry_after < 0:
    +300            raise ValueError("retry_after must be non-negative")
    +301
    +302        # Pre-round for safe_details (avoid repeated rounding)
    +303        rounded_retry = round(retry_after, 2)
    +304        safe_details = {"retry_after": rounded_retry}
    +305        super().__init__(message, safe_details=safe_details)
    +306
    +307        self.retry_after = retry_after
    +308
    +309    @property
    +310    def default_retry_delay(self) -> float:
    +311        """Suggested delay before retry."""
    +312        return self.retry_after
    +
    + + +

    Circuit breaker is open due to repeated failures.

    + +

    Indicates temporary service unavailability. Clients should wait +for retry_after seconds before retrying.

    + +
    Attributes:
    + +
      +
    • retry_after: Seconds to wait before retry
    • +
    + +
    Example:
    + +
    +
    +
    >>> error = CircuitOpenError("Circuit open", retry_after=60.0)
    +>>> error.is_retryable  # True
    +>>> error.retry_after  # 60.0
    +
    +
    +
    +
    + + +
    + +
    + + CircuitOpenError(message: str, *, retry_after: float = 60.0) + + + +
    + +
    289    def __init__(self, message: str, *, retry_after: float = 60.0) -> None:
    +290        """Initialize circuit open error.
    +291
    +292        Args:
    +293            message: Error message
    +294            retry_after: Seconds to wait before retry
    +295
    +296        Raises:
    +297            ValueError: If retry_after is negative
    +298        """
    +299        if retry_after < 0:
    +300            raise ValueError("retry_after must be non-negative")
    +301
    +302        # Pre-round for safe_details (avoid repeated rounding)
    +303        rounded_retry = round(retry_after, 2)
    +304        safe_details = {"retry_after": rounded_retry}
    +305        super().__init__(message, safe_details=safe_details)
    +306
    +307        self.retry_after = retry_after
    +
    + + +

    Initialize circuit open error.

    + +
    Arguments:
    + +
      +
    • message: Error message
    • +
    • retry_after: Seconds to wait before retry
    • +
    + +
    Raises:
    + +
      +
    • ValueError: If retry_after is negative
    • +
    +
    + + +
    +
    +
    + retry_after + + +
    + + + + +
    +
    + +
    + default_retry_delay: float + + + +
    + +
    309    @property
    +310    def default_retry_delay(self) -> float:
    +311        """Suggested delay before retry."""
    +312        return self.retry_after
    +
    + + +

    Suggested delay before retry.

    +
    + + +
    +
    +
    + +
    + + class + CircuitState(enum.Enum): + + + +
    + +
    36class CircuitState(Enum):
    +37    """Circuit breaker states.
    +38
    +39    CLOSED: Normal operation, requests pass through (hot path)
    +40    OPEN: Failures exceeded threshold, requests blocked
    +41    HALF_OPEN: Testing recovery, limited requests allowed
    +42    """
    +43
    +44    CLOSED = auto()
    +45    OPEN = auto()
    +46    HALF_OPEN = auto()
    +
    + + +

    Circuit breaker states.

    + +

    CLOSED: Normal operation, requests pass through (hot path) +OPEN: Failures exceeded threshold, requests blocked +HALF_OPEN: Testing recovery, limited requests allowed

    +
    + + +
    +
    + CLOSED = +<CircuitState.CLOSED: 1> + + +
    + + + + +
    +
    +
    + OPEN = +<CircuitState.OPEN: 2> + + +
    + + + + +
    +
    +
    + HALF_OPEN = +<CircuitState.HALF_OPEN: 3> + + +
    + + + + +
    +
    +
    + +
    + + class + ConfigOverrides(typing.TypedDict): + + + +
    + +
    686class ConfigOverrides(TypedDict, total=False):
    +687    """Type-safe configuration overrides.
    +688
    +689    All fields are optional, allowing selective parameter overriding
    +690    while maintaining type safety.
    +691    """
    +692
    +693    timeout: int
    +694    retry_attempts: int
    +695    max_connections: int
    +696    rate_limit: int
    +697    user_agent: str
    +698    enable_circuit_breaker: bool
    +699    circuit_failure_threshold: int
    +700    circuit_recovery_timeout: float
    +701    circuit_success_threshold: int
    +702    circuit_call_timeout: float
    +703    enable_logging: bool
    +704    json_format: bool
    +705    allow_private_networks: bool
    +706    resolve_dns_for_ssrf: bool
    +
    + + +

    Type-safe configuration overrides.

    + +

    All fields are optional, allowing selective parameter overriding +while maintaining type safety.

    +
    + + +
    +
    + timeout: int + + +
    + + + + +
    +
    +
    + retry_attempts: int + + +
    + + + + +
    +
    +
    + max_connections: int + + +
    + + + + +
    +
    +
    + rate_limit: int + + +
    + + + + +
    +
    +
    + user_agent: str + + +
    + + + + +
    +
    +
    + enable_circuit_breaker: bool + + +
    + + + + +
    +
    +
    + circuit_failure_threshold: int + + +
    + + + + +
    +
    +
    + circuit_recovery_timeout: float + + +
    + + + + +
    +
    +
    + circuit_success_threshold: int + + +
    + + + + +
    +
    +
    + circuit_call_timeout: float + + +
    + + + + +
    +
    +
    + enable_logging: bool + + +
    + + + + +
    +
    +
    + json_format: bool + + +
    + + + + +
    +
    +
    + allow_private_networks: bool + + +
    + + + + +
    +
    +
    + resolve_dns_for_ssrf: bool + + +
    + + + + +
    +
    +
    + +
    + + class + ConfigurationError(pyoutlineapi.OutlineError): + + + +
    + +
    315class ConfigurationError(OutlineError):
    +316    """Invalid or missing configuration.
    +317
    +318    Attributes:
    +319        field: Configuration field name that failed
    +320        security_issue: Whether this is a security-related issue
    +321
    +322    Example:
    +323        >>> error = ConfigurationError(
    +324        ...     "Missing API URL", field="api_url", security_issue=True
    +325        ... )
    +326    """
    +327
    +328    __slots__ = ("field", "security_issue")
    +329
    +330    def __init__(
    +331        self,
    +332        message: str,
    +333        *,
    +334        field: str | None = None,
    +335        security_issue: bool = False,
    +336    ) -> None:
    +337        """Initialize configuration error.
    +338
    +339        Args:
    +340            message: Error message
    +341            field: Configuration field name
    +342            security_issue: Whether this is a security issue
    +343        """
    +344        safe_details: dict[str, Any] | None = None
    +345        if field or security_issue:
    +346            safe_details = {}
    +347            if field:
    +348                safe_details["field"] = field
    +349            if security_issue:
    +350                safe_details["security_issue"] = True
    +351
    +352        super().__init__(message, safe_details=safe_details)
    +353
    +354        self.field = field
    +355        self.security_issue = security_issue
    +
    + + +

    Invalid or missing configuration.

    + +
    Attributes:
    + +
      +
    • field: Configuration field name that failed
    • +
    • security_issue: Whether this is a security-related issue
    • +
    + +
    Example:
    + +
    +
    +
    >>> error = ConfigurationError(
    +...     "Missing API URL", field="api_url", security_issue=True
    +... )
    +
    +
    +
    +
    + + +
    + +
    + + ConfigurationError( message: str, *, field: str | None = None, security_issue: bool = False) + + + +
    + +
    330    def __init__(
    +331        self,
    +332        message: str,
    +333        *,
    +334        field: str | None = None,
    +335        security_issue: bool = False,
    +336    ) -> None:
    +337        """Initialize configuration error.
    +338
    +339        Args:
    +340            message: Error message
    +341            field: Configuration field name
    +342            security_issue: Whether this is a security issue
    +343        """
    +344        safe_details: dict[str, Any] | None = None
    +345        if field or security_issue:
    +346            safe_details = {}
    +347            if field:
    +348                safe_details["field"] = field
    +349            if security_issue:
    +350                safe_details["security_issue"] = True
    +351
    +352        super().__init__(message, safe_details=safe_details)
    +353
    +354        self.field = field
    +355        self.security_issue = security_issue
    +
    + + +

    Initialize configuration error.

    + +
    Arguments:
    + +
      +
    • message: Error message
    • +
    • field: Configuration field name
    • +
    • security_issue: Whether this is a security issue
    • +
    +
    + + +
    +
    +
    + field + + +
    + + + + +
    +
    +
    + security_issue + + +
    + + + + +
    +
    +
    + +
    + + class + Constants: + + + +
    + +
     85class Constants:
    + 86    """Application-wide constants with security limits."""
    + 87
    + 88    # Port constraints
    + 89    MIN_PORT: Final[int] = 1
    + 90    MAX_PORT: Final[int] = 65535
    + 91
    + 92    # Length limits
    + 93    MAX_NAME_LENGTH: Final[int] = 255
    + 94    CERT_FINGERPRINT_LENGTH: Final[int] = 64
    + 95    MAX_KEY_ID_LENGTH: Final[int] = 255
    + 96    MAX_URL_LENGTH: Final[int] = 2048
    + 97
    + 98    # Network defaults
    + 99    DEFAULT_TIMEOUT: Final[int] = 10
    +100    DEFAULT_RETRY_ATTEMPTS: Final[int] = 2
    +101    DEFAULT_MIN_CONNECTIONS: Final[int] = 1
    +102    DEFAULT_MAX_CONNECTIONS: Final[int] = 100
    +103    DEFAULT_RETRY_DELAY: Final[float] = 1.0
    +104    DEFAULT_MIN_TIMEOUT: Final[int] = 1
    +105    DEFAULT_MAX_TIMEOUT: Final[int] = 300
    +106    DEFAULT_USER_AGENT: Final[str] = "PyOutlineAPI/0.4.0"
    +107    _MIN_RATE_LIMIT: Final[int] = 1
    +108    _MAX_RATE_LIMIT: Final[int] = 1000
    +109    _SAFETY_MARGIN: Final[float] = 10.0
    +110
    +111    # Resource limits
    +112    MAX_RECURSION_DEPTH: Final[int] = 10
    +113    MAX_SNAPSHOT_SIZE_MB: Final[int] = 10
    +114
    +115    # HTTP retry codes
    +116    RETRY_STATUS_CODES: Final[frozenset[int]] = frozenset(
    +117        {408, 429, 500, 502, 503, 504}
    +118    )
    +119
    +120    # Logging levels
    +121    LOG_LEVEL_DEBUG: Final[int] = logging.DEBUG
    +122    LOG_LEVEL_INFO: Final[int] = logging.INFO
    +123    LOG_LEVEL_WARNING: Final[int] = logging.WARNING
    +124    LOG_LEVEL_ERROR: Final[int] = logging.ERROR
    +125
    +126    # ===== Security limits =====
    +127
    +128    # Response size protection (DoS prevention)
    +129    MAX_RESPONSE_SIZE: Final[int] = 10 * 1024 * 1024  # 10 MB
    +130    MAX_RESPONSE_CHUNK_SIZE: Final[int] = 8192  # 8 KB chunks
    +131
    +132    # Rate limiting defaults
    +133    DEFAULT_RATE_LIMIT_RPS: Final[float] = 100.0  # Requests per second
    +134    DEFAULT_RATE_LIMIT_BURST: Final[int] = 200  # Burst capacity
    +135    DEFAULT_RATE_LIMIT: Final[int] = 100  # Concurrent requests
    +136
    +137    # Connection limits
    +138    MAX_CONNECTIONS_PER_HOST: Final[int] = 50
    +139    DNS_CACHE_TTL: Final[int] = 300  # 5 minutes
    +140
    +141    # Timeout strategies
    +142    TIMEOUT_WARNING_RATIO: Final[float] = 0.8  # Warn at 80% of timeout
    +143    MAX_TIMEOUT: Final[int] = 300  # 5 minutes absolute max
    +
    + + +

    Application-wide constants with security limits.

    +
    + + +
    +
    + MIN_PORT: Final[int] = +1 + + +
    + + + + +
    +
    +
    + MAX_PORT: Final[int] = +65535 + + +
    + + + + +
    +
    +
    + MAX_NAME_LENGTH: Final[int] = +255 + + +
    + + + + +
    +
    +
    + CERT_FINGERPRINT_LENGTH: Final[int] = +64 + + +
    + + + + +
    +
    +
    + MAX_KEY_ID_LENGTH: Final[int] = +255 + + +
    + + + + +
    +
    +
    + MAX_URL_LENGTH: Final[int] = +2048 + + +
    + + + + +
    +
    +
    + DEFAULT_TIMEOUT: Final[int] = +10 + + +
    + + + + +
    +
    +
    + DEFAULT_RETRY_ATTEMPTS: Final[int] = +2 + + +
    + + + + +
    +
    +
    + DEFAULT_MIN_CONNECTIONS: Final[int] = +1 + + +
    + + + + +
    +
    +
    + DEFAULT_MAX_CONNECTIONS: Final[int] = +100 + + +
    + + + + +
    +
    +
    + DEFAULT_RETRY_DELAY: Final[float] = +1.0 + + +
    + + + + +
    +
    +
    + DEFAULT_MIN_TIMEOUT: Final[int] = +1 + + +
    + + + + +
    +
    +
    + DEFAULT_MAX_TIMEOUT: Final[int] = +300 + + +
    + + + + +
    +
    +
    + DEFAULT_USER_AGENT: Final[str] = +'PyOutlineAPI/0.4.0' + + +
    + + + + +
    +
    +
    + MAX_RECURSION_DEPTH: Final[int] = +10 + + +
    + + + + +
    +
    +
    + MAX_SNAPSHOT_SIZE_MB: Final[int] = +10 + + +
    + + + + +
    +
    +
    + RETRY_STATUS_CODES: Final[frozenset[int]] = +frozenset({500, 408, 502, 503, 504, 429}) + + +
    + + + + +
    +
    +
    + LOG_LEVEL_DEBUG: Final[int] = +10 + + +
    + + + + +
    +
    +
    + LOG_LEVEL_INFO: Final[int] = +20 + + +
    + + + + +
    +
    +
    + LOG_LEVEL_WARNING: Final[int] = +30 + + +
    + + + + +
    +
    +
    + LOG_LEVEL_ERROR: Final[int] = +40 + + +
    + + + + +
    +
    +
    + MAX_RESPONSE_SIZE: Final[int] = +10485760 + + +
    + + + + +
    +
    +
    + MAX_RESPONSE_CHUNK_SIZE: Final[int] = +8192 + + +
    + + + + +
    +
    +
    + DEFAULT_RATE_LIMIT_RPS: Final[float] = +100.0 + + +
    + + + + +
    +
    +
    + DEFAULT_RATE_LIMIT_BURST: Final[int] = +200 + + +
    + + + + +
    +
    +
    + DEFAULT_RATE_LIMIT: Final[int] = +100 + + +
    + + + + +
    +
    +
    + MAX_CONNECTIONS_PER_HOST: Final[int] = +50 + + +
    + + + + +
    +
    +
    + DNS_CACHE_TTL: Final[int] = +300 + + +
    + + + + +
    +
    +
    + TIMEOUT_WARNING_RATIO: Final[float] = +0.8 + + +
    + + + + +
    +
    +
    + MAX_TIMEOUT: Final[int] = +300 + + +
    + + + + +
    +
    +
    + +
    + + class + CredentialSanitizer: + + + +
    + +
    242class CredentialSanitizer:
    +243    """Sanitize credentials from strings and exceptions."""
    +244
    +245    # Patterns for detecting credentials
    +246    PATTERNS: Final[list[tuple[re.Pattern[str], str]]] = [
    +247        (
    +248            re.compile(
    +249                r'api[_-]?key["\']?\s*[:=]\s*["\']?([a-zA-Z0-9]{20,})',
    +250                re.IGNORECASE,
    +251            ),
    +252            "***API_KEY***",
    +253        ),
    +254        (
    +255            re.compile(r'token["\']?\s*[:=]\s*["\']?([a-zA-Z0-9]{20,})', re.IGNORECASE),
    +256            "***TOKEN***",
    +257        ),
    +258        (
    +259            re.compile(r'password["\']?\s*[:=]\s*["\']?([^\s"\']+)', re.IGNORECASE),
    +260            "***PASSWORD***",
    +261        ),
    +262        (
    +263            re.compile(
    +264                r'cert[_-]?sha256["\']?\s*[:=]\s*["\']?([a-f0-9]{64})', re.IGNORECASE
    +265            ),
    +266            "***CERT***",
    +267        ),
    +268        (
    +269            re.compile(r"bearer\s+([a-zA-Z0-9\-._~+/]+=*)", re.IGNORECASE),
    +270            "Bearer ***TOKEN***",
    +271        ),
    +272        (
    +273            re.compile(r"access_url['\"]?\s*[:=]\s*['\"]?([^\s'\"]+)", re.IGNORECASE),
    +274            "***ACCESS_URL***",
    +275        ),
    +276    ]
    +277
    +278    @classmethod
    +279    @lru_cache(maxsize=512)
    +280    def sanitize(cls, text: str) -> str:
    +281        """Remove credentials from string.
    +282
    +283        :param text: Text that may contain credentials
    +284        :return: Sanitized text
    +285        """
    +286        if not text:
    +287            return text
    +288
    +289        sanitized = text
    +290        for pattern, replacement in cls.PATTERNS:
    +291            sanitized = pattern.sub(replacement, sanitized)
    +292        return sanitized
    +
    + + +

    Sanitize credentials from strings and exceptions.

    +
    + + +
    +
    + PATTERNS: Final[list[tuple[re.Pattern[str], str]]] = + + [(re.compile('api[_-]?key["\\\']?\\s*[:=]\\s*["\\\']?([a-zA-Z0-9]{20,})', re.IGNORECASE), '***API_KEY***'), (re.compile('token["\\\']?\\s*[:=]\\s*["\\\']?([a-zA-Z0-9]{20,})', re.IGNORECASE), '***TOKEN***'), (re.compile('password["\\\']?\\s*[:=]\\s*["\\\']?([^\\s"\\\']+)', re.IGNORECASE), '***PASSWORD***'), (re.compile('cert[_-]?sha256["\\\']?\\s*[:=]\\s*["\\\']?([a-f0-9]{64})', re.IGNORECASE), '***CERT***'), (re.compile('bearer\\s+([a-zA-Z0-9\\-._~+/]+=*)', re.IGNORECASE), 'Bearer ***TOKEN***'), (re.compile('access_url[\'\\"]?\\s*[:=]\\s*[\'\\"]?([^\\s\'\\"]+)', re.IGNORECASE), '***ACCESS_URL***')] + + +
    + + + + +
    +
    + +
    +
    @classmethod
    +
    @lru_cache(maxsize=512)
    + + def + sanitize(cls, text: str) -> str: + + + +
    + +
    278    @classmethod
    +279    @lru_cache(maxsize=512)
    +280    def sanitize(cls, text: str) -> str:
    +281        """Remove credentials from string.
    +282
    +283        :param text: Text that may contain credentials
    +284        :return: Sanitized text
    +285        """
    +286        if not text:
    +287            return text
    +288
    +289        sanitized = text
    +290        for pattern, replacement in cls.PATTERNS:
    +291            sanitized = pattern.sub(replacement, sanitized)
    +292        return sanitized
    +
    + + +

    Remove credentials from string.

    + +
    Parameters
    + +
      +
    • text: Text that may contain credentials
    • +
    + +
    Returns
    + +
    +

    Sanitized text

    +
    +
    + + +
    +
    +
    + +
    + + class + DataLimit(pyoutlineapi.common_types.BaseValidatedModel, pyoutlineapi.models.ByteConversionMixin): + + + +
    + +
    103class DataLimit(BaseValidatedModel, ByteConversionMixin):
    +104    """Data transfer limit in bytes with unit conversions."""
    +105
    +106    bytes: Bytes
    +107
    +108    @classmethod
    +109    def from_kilobytes(cls, kb: float) -> Self:
    +110        """Create DataLimit from kilobytes.
    +111
    +112        :param kb: Size in kilobytes
    +113        :return: DataLimit instance
    +114        """
    +115        return cls(bytes=int(kb * _BYTES_IN_KB))
    +116
    +117    @classmethod
    +118    def from_megabytes(cls, mb: float) -> Self:
    +119        """Create DataLimit from megabytes.
    +120
    +121        :param mb: Size in megabytes
    +122        :return: DataLimit instance
    +123        """
    +124        return cls(bytes=int(mb * _BYTES_IN_MB))
    +125
    +126    @classmethod
    +127    def from_gigabytes(cls, gb: float) -> Self:
    +128        """Create DataLimit from gigabytes.
    +129
    +130        :param gb: Size in gigabytes
    +131        :return: DataLimit instance
    +132        """
    +133        return cls(bytes=int(gb * _BYTES_IN_GB))
    +
    + + +

    Data transfer limit in bytes with unit conversions.

    +
    + + +
    +
    + bytes: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Size in bytes', metadata=[Ge(ge=0)])] = +PydanticUndefined + + +
    + + +

    Size in bytes

    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_kilobytes(cls, kb: float) -> Self: + + + +
    + +
    108    @classmethod
    +109    def from_kilobytes(cls, kb: float) -> Self:
    +110        """Create DataLimit from kilobytes.
    +111
    +112        :param kb: Size in kilobytes
    +113        :return: DataLimit instance
    +114        """
    +115        return cls(bytes=int(kb * _BYTES_IN_KB))
    +
    + + +

    Create DataLimit from kilobytes.

    + +
    Parameters
    + +
      +
    • kb: Size in kilobytes
    • +
    + +
    Returns
    + +
    +

    DataLimit instance

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_megabytes(cls, mb: float) -> Self: + + + +
    + +
    117    @classmethod
    +118    def from_megabytes(cls, mb: float) -> Self:
    +119        """Create DataLimit from megabytes.
    +120
    +121        :param mb: Size in megabytes
    +122        :return: DataLimit instance
    +123        """
    +124        return cls(bytes=int(mb * _BYTES_IN_MB))
    +
    + + +

    Create DataLimit from megabytes.

    + +
    Parameters
    + +
      +
    • mb: Size in megabytes
    • +
    + +
    Returns
    + +
    +

    DataLimit instance

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_gigabytes(cls, gb: float) -> Self: + + + +
    + +
    126    @classmethod
    +127    def from_gigabytes(cls, gb: float) -> Self:
    +128        """Create DataLimit from gigabytes.
    +129
    +130        :param gb: Size in gigabytes
    +131        :return: DataLimit instance
    +132        """
    +133        return cls(bytes=int(gb * _BYTES_IN_GB))
    +
    + + +

    Create DataLimit from gigabytes.

    + +
    Parameters
    + +
      +
    • gb: Size in gigabytes
    • +
    + +
    Returns
    + +
    +

    DataLimit instance

    +
    +
    + + +
    +
    +
    + +
    + + class + DataLimitRequest(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    533class DataLimitRequest(BaseValidatedModel):
    +534    """Request model for setting data limit.
    +535
    +536    Note:
    +537        The API expects the DataLimit object directly.
    +538        Use to_payload() to produce the correct request body.
    +539    """
    +540
    +541    limit: DataLimit
    +542
    +543    def to_payload(self) -> dict[str, int]:
    +544        """Convert to API request payload.
    +545
    +546        :return: Payload dict with bytes field
    +547        """
    +548        return cast(dict[str, int], self.limit.model_dump(by_alias=True))
    +
    + + +

    Request model for setting data limit.

    + +
    Note:
    + +
    +

    The API expects the DataLimit object directly. + Use to_payload() to produce the correct request body.

    +
    +
    + + +
    +
    + limit: DataLimit = +PydanticUndefined + + +
    + + + + +
    +
    + +
    + + def + to_payload(self) -> dict[str, int]: + + + +
    + +
    543    def to_payload(self) -> dict[str, int]:
    +544        """Convert to API request payload.
    +545
    +546        :return: Payload dict with bytes field
    +547        """
    +548        return cast(dict[str, int], self.limit.model_dump(by_alias=True))
    +
    + + +

    Convert to API request payload.

    + +
    Returns
    + +
    +

    Payload dict with bytes field

    +
    +
    + + +
    +
    +
    + +
    + + class + DataTransferred(pyoutlineapi.common_types.BaseValidatedModel, pyoutlineapi.models.ByteConversionMixin): + + + +
    + +
    365class DataTransferred(BaseValidatedModel, ByteConversionMixin):
    +366    """Data transfer metric with byte conversions.
    +367
    +368    SCHEMA: Based on experimental metrics dataTransferred object
    +369    """
    +370
    +371    bytes: Bytes
    +
    + + +

    Data transfer metric with byte conversions.

    + +

    SCHEMA: Based on experimental metrics dataTransferred object

    +
    + + +
    +
    + bytes: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Size in bytes', metadata=[Ge(ge=0)])] = +PydanticUndefined + + +
    + + +

    Size in bytes

    +
    + + +
    +
    +
    + +
    + + class + DefaultAuditLogger: + + + +
    + +
    273class DefaultAuditLogger:
    +274    """Async audit logger with batching and backpressure handling."""
    +275
    +276    __slots__ = (
    +277        "_batch_size",
    +278        "_batch_timeout",
    +279        "_lock",
    +280        "_queue",
    +281        "_queue_size",
    +282        "_shutdown_event",
    +283        "_task",
    +284    )
    +285
    +286    def __init__(
    +287        self,
    +288        *,
    +289        queue_size: int = 10000,
    +290        batch_size: int = 100,
    +291        batch_timeout: float = 1.0,
    +292    ) -> None:
    +293        """Initialize audit logger with batching support.
    +294
    +295        :param queue_size: Maximum queue size (backpressure protection)
    +296        :param batch_size: Maximum batch size for processing
    +297        :param batch_timeout: Maximum time to wait for batch completion (seconds)
    +298        """
    +299        self._queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=queue_size)
    +300        self._queue_size = queue_size
    +301        self._batch_size = batch_size
    +302        self._batch_timeout = batch_timeout
    +303        self._task: asyncio.Task[None] | None = None
    +304        self._shutdown_event = asyncio.Event()
    +305        self._lock = asyncio.Lock()
    +306
    +307    async def alog_action(
    +308        self,
    +309        action: str,
    +310        resource: str,
    +311        *,
    +312        user: str | None = None,
    +313        details: dict[str, Any] | None = None,
    +314        correlation_id: str | None = None,
    +315    ) -> None:
    +316        """Log auditable action asynchronously with automatic batching.
    +317
    +318        :param action: Action being performed
    +319        :param resource: Resource identifier
    +320        :param user: User performing the action (optional)
    +321        :param details: Additional structured details (optional)
    +322        :param correlation_id: Request correlation ID (optional)
    +323        """
    +324        if self._shutdown_event.is_set():
    +325            # Fallback to sync logging during shutdown
    +326            return self.log_action(
    +327                action,
    +328                resource,
    +329                user=user,
    +330                details=details,
    +331                correlation_id=correlation_id,
    +332            )
    +333
    +334        # Ensure background task is running
    +335        await self._ensure_task_running()
    +336
    +337        # Build log entry
    +338        entry = self._build_entry(action, resource, user, details, correlation_id)
    +339
    +340        # Try to enqueue, handle backpressure
    +341        try:
    +342            self._queue.put_nowait(entry)
    +343        except asyncio.QueueFull:
    +344            # Backpressure: log warning and use sync fallback
    +345            if logger.isEnabledFor(logging.WARNING):
    +346                logger.warning(
    +347                    "[AUDIT] Queue full (%d items), using sync fallback",
    +348                    self._queue_size,
    +349                )
    +350            self.log_action(
    +351                action,
    +352                resource,
    +353                user=user,
    +354                details=details,
    +355                correlation_id=correlation_id,
    +356            )
    +357
    +358    def log_action(
    +359        self,
    +360        action: str,
    +361        resource: str,
    +362        *,
    +363        user: str | None = None,
    +364        details: dict[str, Any] | None = None,
    +365        correlation_id: str | None = None,
    +366    ) -> None:
    +367        """Log auditable action synchronously (fallback method).
    +368
    +369        :param action: Action being performed
    +370        :param resource: Resource identifier
    +371        :param user: User performing the action (optional)
    +372        :param details: Additional structured details (optional)
    +373        :param correlation_id: Request correlation ID (optional)
    +374        """
    +375        entry = self._build_entry(action, resource, user, details, correlation_id)
    +376        self._write_log(entry)
    +377
    +378    async def _ensure_task_running(self) -> None:
    +379        """Ensure background processing task is running (lazy start with lock)."""
    +380        if self._task is not None and not self._task.done():
    +381            return
    +382
    +383        async with self._lock:
    +384            # Double-check after acquiring lock
    +385            if self._task is None or self._task.done():
    +386                self._task = asyncio.create_task(
    +387                    self._process_queue(), name="audit-logger"
    +388                )
    +389
    +390    async def _process_queue(self) -> None:
    +391        """Background task for processing audit logs in batches.
    +392
    +393        Uses batching for improved throughput and reduced I/O overhead.
    +394        """
    +395        batch: list[dict[str, Any]] = []
    +396
    +397        try:
    +398            while not self._shutdown_event.is_set():
    +399                try:
    +400                    # Wait for item with timeout for batch processing
    +401                    entry = await asyncio.wait_for(
    +402                        self._queue.get(), timeout=self._batch_timeout
    +403                    )
    +404                    batch.append(entry)
    +405
    +406                    # Process batch when size reached or queue empty
    +407                    if len(batch) >= self._batch_size or self._queue.empty():
    +408                        self._write_batch(batch)
    +409                        batch.clear()
    +410
    +411                    self._queue.task_done()
    +412
    +413                except asyncio.TimeoutError:
    +414                    # Timeout: flush partial batch if any
    +415                    if batch:
    +416                        self._write_batch(batch)
    +417                        batch.clear()
    +418
    +419        except asyncio.CancelledError:
    +420            # Flush remaining batch on cancellation
    +421            if batch:
    +422                self._write_batch(batch)
    +423            raise
    +424        finally:
    +425            if logger.isEnabledFor(logging.DEBUG):
    +426                logger.debug("[AUDIT] Queue processor stopped")
    +427
    +428    def _write_batch(self, batch: list[dict[str, Any]]) -> None:
    +429        """Write batch of log entries efficiently.
    +430
    +431        :param batch: Batch of log entries to write
    +432        """
    +433        for entry in batch:
    +434            self._write_log(entry)
    +435
    +436    def _write_log(self, entry: dict[str, Any]) -> None:
    +437        """Write single log entry to logger.
    +438
    +439        :param entry: Log entry to write
    +440        """
    +441        message = self._format_message(entry)
    +442        logger.info(message, extra=entry)
    +443
    +444    @staticmethod
    +445    def _build_entry(
    +446        action: str,
    +447        resource: str,
    +448        user: str | None,
    +449        details: dict[str, Any] | None,
    +450        correlation_id: str | None,
    +451    ) -> dict[str, Any]:
    +452        """Build structured log entry with sanitization.
    +453
    +454        :param action: Action being performed
    +455        :param resource: Resource identifier
    +456        :param user: User performing action
    +457        :param details: Additional details
    +458        :param correlation_id: Correlation ID
    +459        :return: Structured log entry
    +460        """
    +461        entry: dict[str, Any] = {
    +462            "action": action,
    +463            "resource": resource,
    +464            "timestamp": time.time(),
    +465            "is_audit": True,
    +466        }
    +467
    +468        if user is not None:
    +469            entry["user"] = user
    +470        if correlation_id is not None:
    +471            entry["correlation_id"] = correlation_id
    +472        if details is not None:
    +473            entry["details"] = _sanitize_details(details)
    +474
    +475        return entry
    +476
    +477    @staticmethod
    +478    def _format_message(entry: dict[str, Any]) -> str:
    +479        """Format audit log message for human readability.
    +480
    +481        :param entry: Log entry
    +482        :return: Formatted message
    +483        """
    +484        action = entry["action"]
    +485        resource = entry["resource"]
    +486        user = entry.get("user")
    +487        correlation_id = entry.get("correlation_id")
    +488
    +489        parts = ["[AUDIT]", action, "on", resource]
    +490
    +491        if user:
    +492            parts.extend(["by", user])
    +493        if correlation_id:
    +494            parts.append(f"[{correlation_id}]")
    +495
    +496        return " ".join(parts)
    +497
    +498    async def shutdown(self, *, timeout: float = 5.0) -> None:
    +499        """Gracefully shutdown audit logger with queue draining.
    +500
    +501        :param timeout: Maximum time to wait for queue to drain (seconds)
    +502        """
    +503        async with self._lock:
    +504            if self._shutdown_event.is_set():
    +505                return
    +506
    +507            self._shutdown_event.set()
    +508
    +509            if logger.isEnabledFor(logging.DEBUG):
    +510                logger.debug("[AUDIT] Shutting down, draining queue")
    +511
    +512            # Wait for queue to drain
    +513            try:
    +514                await asyncio.wait_for(self._queue.join(), timeout=timeout)
    +515            except asyncio.TimeoutError:
    +516                remaining = self._queue.qsize()
    +517                if logger.isEnabledFor(logging.WARNING):
    +518                    logger.warning(
    +519                        "[AUDIT] Queue did not drain within %ss, %d items remaining",
    +520                        timeout,
    +521                        remaining,
    +522                    )
    +523
    +524            # Cancel processing task
    +525            if self._task and not self._task.done():
    +526                self._task.cancel()
    +527                with suppress(asyncio.CancelledError):
    +528                    await self._task
    +529
    +530            if logger.isEnabledFor(logging.DEBUG):
    +531                logger.debug("[AUDIT] Shutdown complete")
    +
    + + +

    Async audit logger with batching and backpressure handling.

    +
    + + +
    + +
    + + DefaultAuditLogger( *, queue_size: int = 10000, batch_size: int = 100, batch_timeout: float = 1.0) + + + +
    + +
    286    def __init__(
    +287        self,
    +288        *,
    +289        queue_size: int = 10000,
    +290        batch_size: int = 100,
    +291        batch_timeout: float = 1.0,
    +292    ) -> None:
    +293        """Initialize audit logger with batching support.
    +294
    +295        :param queue_size: Maximum queue size (backpressure protection)
    +296        :param batch_size: Maximum batch size for processing
    +297        :param batch_timeout: Maximum time to wait for batch completion (seconds)
    +298        """
    +299        self._queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=queue_size)
    +300        self._queue_size = queue_size
    +301        self._batch_size = batch_size
    +302        self._batch_timeout = batch_timeout
    +303        self._task: asyncio.Task[None] | None = None
    +304        self._shutdown_event = asyncio.Event()
    +305        self._lock = asyncio.Lock()
    +
    + + +

    Initialize audit logger with batching support.

    + +
    Parameters
    + +
      +
    • queue_size: Maximum queue size (backpressure protection)
    • +
    • batch_size: Maximum batch size for processing
    • +
    • batch_timeout: Maximum time to wait for batch completion (seconds)
    • +
    +
    + + +
    +
    + +
    + + async def + alog_action( self, action: str, resource: str, *, user: str | None = None, details: dict[str, typing.Any] | None = None, correlation_id: str | None = None) -> None: + + + +
    + +
    307    async def alog_action(
    +308        self,
    +309        action: str,
    +310        resource: str,
    +311        *,
    +312        user: str | None = None,
    +313        details: dict[str, Any] | None = None,
    +314        correlation_id: str | None = None,
    +315    ) -> None:
    +316        """Log auditable action asynchronously with automatic batching.
    +317
    +318        :param action: Action being performed
    +319        :param resource: Resource identifier
    +320        :param user: User performing the action (optional)
    +321        :param details: Additional structured details (optional)
    +322        :param correlation_id: Request correlation ID (optional)
    +323        """
    +324        if self._shutdown_event.is_set():
    +325            # Fallback to sync logging during shutdown
    +326            return self.log_action(
    +327                action,
    +328                resource,
    +329                user=user,
    +330                details=details,
    +331                correlation_id=correlation_id,
    +332            )
    +333
    +334        # Ensure background task is running
    +335        await self._ensure_task_running()
    +336
    +337        # Build log entry
    +338        entry = self._build_entry(action, resource, user, details, correlation_id)
    +339
    +340        # Try to enqueue, handle backpressure
    +341        try:
    +342            self._queue.put_nowait(entry)
    +343        except asyncio.QueueFull:
    +344            # Backpressure: log warning and use sync fallback
    +345            if logger.isEnabledFor(logging.WARNING):
    +346                logger.warning(
    +347                    "[AUDIT] Queue full (%d items), using sync fallback",
    +348                    self._queue_size,
    +349                )
    +350            self.log_action(
    +351                action,
    +352                resource,
    +353                user=user,
    +354                details=details,
    +355                correlation_id=correlation_id,
    +356            )
    +
    + + +

    Log auditable action asynchronously with automatic batching.

    + +
    Parameters
    + +
      +
    • action: Action being performed
    • +
    • resource: Resource identifier
    • +
    • user: User performing the action (optional)
    • +
    • details: Additional structured details (optional)
    • +
    • correlation_id: Request correlation ID (optional)
    • +
    +
    + + +
    +
    + +
    + + def + log_action( self, action: str, resource: str, *, user: str | None = None, details: dict[str, typing.Any] | None = None, correlation_id: str | None = None) -> None: + + + +
    + +
    358    def log_action(
    +359        self,
    +360        action: str,
    +361        resource: str,
    +362        *,
    +363        user: str | None = None,
    +364        details: dict[str, Any] | None = None,
    +365        correlation_id: str | None = None,
    +366    ) -> None:
    +367        """Log auditable action synchronously (fallback method).
    +368
    +369        :param action: Action being performed
    +370        :param resource: Resource identifier
    +371        :param user: User performing the action (optional)
    +372        :param details: Additional structured details (optional)
    +373        :param correlation_id: Request correlation ID (optional)
    +374        """
    +375        entry = self._build_entry(action, resource, user, details, correlation_id)
    +376        self._write_log(entry)
    +
    + + +

    Log auditable action synchronously (fallback method).

    + +
    Parameters
    + +
      +
    • action: Action being performed
    • +
    • resource: Resource identifier
    • +
    • user: User performing the action (optional)
    • +
    • details: Additional structured details (optional)
    • +
    • correlation_id: Request correlation ID (optional)
    • +
    +
    + + +
    +
    + +
    + + async def + shutdown(self, *, timeout: float = 5.0) -> None: + + + +
    + +
    498    async def shutdown(self, *, timeout: float = 5.0) -> None:
    +499        """Gracefully shutdown audit logger with queue draining.
    +500
    +501        :param timeout: Maximum time to wait for queue to drain (seconds)
    +502        """
    +503        async with self._lock:
    +504            if self._shutdown_event.is_set():
    +505                return
    +506
    +507            self._shutdown_event.set()
    +508
    +509            if logger.isEnabledFor(logging.DEBUG):
    +510                logger.debug("[AUDIT] Shutting down, draining queue")
    +511
    +512            # Wait for queue to drain
    +513            try:
    +514                await asyncio.wait_for(self._queue.join(), timeout=timeout)
    +515            except asyncio.TimeoutError:
    +516                remaining = self._queue.qsize()
    +517                if logger.isEnabledFor(logging.WARNING):
    +518                    logger.warning(
    +519                        "[AUDIT] Queue did not drain within %ss, %d items remaining",
    +520                        timeout,
    +521                        remaining,
    +522                    )
    +523
    +524            # Cancel processing task
    +525            if self._task and not self._task.done():
    +526                self._task.cancel()
    +527                with suppress(asyncio.CancelledError):
    +528                    await self._task
    +529
    +530            if logger.isEnabledFor(logging.DEBUG):
    +531                logger.debug("[AUDIT] Shutdown complete")
    +
    + + +

    Gracefully shutdown audit logger with queue draining.

    + +
    Parameters
    + +
      +
    • timeout: Maximum time to wait for queue to drain (seconds)
    • +
    +
    + + +
    +
    +
    + +
    + + class + DevelopmentConfig(pyoutlineapi.OutlineClientConfig): + + + +
    + +
    463class DevelopmentConfig(OutlineClientConfig):
    +464    """Development configuration with relaxed security.
    +465
    +466    Optimized for local development and testing with:
    +467    - Extended timeouts for debugging
    +468    - Detailed logging enabled by default
    +469    - Circuit breaker disabled for easier testing
    +470    """
    +471
    +472    model_config = SettingsConfigDict(
    +473        env_prefix=_DEV_ENV_PREFIX,
    +474        env_file=".env.dev",
    +475        case_sensitive=False,
    +476        extra="forbid",
    +477    )
    +478
    +479    enable_logging: bool = True
    +480    enable_circuit_breaker: bool = False
    +481    timeout: int = 30
    +
    + + +

    Development configuration with relaxed security.

    + +

    Optimized for local development and testing with:

    + +
      +
    • Extended timeouts for debugging
    • +
    • Detailed logging enabled by default
    • +
    • Circuit breaker disabled for easier testing
    • +
    +
    + + +
    +
    + enable_logging: bool = +True + + +
    + + + + +
    +
    +
    + enable_circuit_breaker: bool = +False + + +
    + + + + +
    +
    +
    + timeout: int = +30 + + +
    + + + + +
    +
    +
    + +
    + + class + ErrorResponse(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    573class ErrorResponse(BaseValidatedModel):
    +574    """Error response with optimized string formatting.
    +575
    +576    SCHEMA: Based on API error response format
    +577    """
    +578
    +579    code: str
    +580    message: str
    +581
    +582    def __str__(self) -> str:
    +583        """Format error as string (optimized f-string).
    +584
    +585        :return: Formatted error message
    +586        """
    +587        return f"{self.code}: {self.message}"
    +
    + + +

    Error response with optimized string formatting.

    + +

    SCHEMA: Based on API error response format

    +
    + + +
    +
    + code: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + message: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + +
    + + class + ExperimentalMetrics(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    460class ExperimentalMetrics(BaseValidatedModel):
    +461    """Experimental metrics with optimized lookup.
    +462
    +463    SCHEMA: Based on GET /experimental/server/metrics response
    +464    """
    +465
    +466    server: ServerExperimentalMetric
    +467    access_keys: list[AccessKeyMetric] = Field(alias="accessKeys")
    +468
    +469    def get_key_metric(self, key_id: str) -> AccessKeyMetric | None:
    +470        """Get metrics for specific key with early return.
    +471
    +472        :param key_id: Access key ID
    +473        :return: Key metrics or None if not found
    +474        """
    +475        for metric in self.access_keys:
    +476            if metric.access_key_id == key_id:
    +477                return metric  # Early return
    +478        return None
    +
    + + +

    Experimental metrics with optimized lookup.

    + +

    SCHEMA: Based on GET /experimental/server/metrics response

    +
    + + +
    +
    + server: ServerExperimentalMetric = +PydanticUndefined + + +
    + + + + +
    +
    +
    + access_keys: list[AccessKeyMetric] = +PydanticUndefined + + +
    + + + + +
    +
    + +
    + + def + get_key_metric(self, key_id: str) -> AccessKeyMetric | None: + + + +
    + +
    469    def get_key_metric(self, key_id: str) -> AccessKeyMetric | None:
    +470        """Get metrics for specific key with early return.
    +471
    +472        :param key_id: Access key ID
    +473        :return: Key metrics or None if not found
    +474        """
    +475        for metric in self.access_keys:
    +476            if metric.access_key_id == key_id:
    +477                return metric  # Early return
    +478        return None
    +
    + + +

    Get metrics for specific key with early return.

    + +
    Parameters
    + +
      +
    • key_id: Access key ID
    • +
    + +
    Returns
    + +
    +

    Key metrics or None if not found

    +
    +
    + + +
    +
    +
    + +
    + + class + HealthCheckResult(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    593class HealthCheckResult(BaseValidatedModel):
    +594    """Health check result with optimized diagnostics."""
    +595
    +596    healthy: bool
    +597    timestamp: float
    +598    checks: ChecksDict
    +599
    +600    @cached_property
    +601    def failed_checks(self) -> list[str]:
    +602        """Get failed checks (cached for repeated access).
    +603
    +604        :return: List of failed check names
    +605        """
    +606        return [
    +607            name
    +608            for name, result in self.checks.items()
    +609            if result.get("status") != "healthy"
    +610        ]
    +611
    +612    @property
    +613    def success_rate(self) -> float:
    +614        """Calculate success rate (uses cached failed_checks).
    +615
    +616        :return: Success rate (0.0 to 1.0)
    +617        """
    +618        if not self.checks:
    +619            return 1.0  # Early return
    +620
    +621        total = len(self.checks)
    +622        passed = total - len(self.failed_checks)  # Uses cached property
    +623        return passed / total
    +
    + + +

    Health check result with optimized diagnostics.

    +
    + + +
    +
    + healthy: bool = +PydanticUndefined + + +
    + + + + +
    +
    +
    + timestamp: float = +PydanticUndefined + + +
    + + + + +
    +
    +
    + checks: dict[str, dict[str, typing.Any]] = +PydanticUndefined + + +
    + + + + +
    +
    + +
    + failed_checks: list[str] + + + +
    + +
    600    @cached_property
    +601    def failed_checks(self) -> list[str]:
    +602        """Get failed checks (cached for repeated access).
    +603
    +604        :return: List of failed check names
    +605        """
    +606        return [
    +607            name
    +608            for name, result in self.checks.items()
    +609            if result.get("status") != "healthy"
    +610        ]
    +
    + + +

    Get failed checks (cached for repeated access).

    + +
    Returns
    + +
    +

    List of failed check names

    +
    +
    + + +
    +
    + +
    + success_rate: float + + + +
    + +
    612    @property
    +613    def success_rate(self) -> float:
    +614        """Calculate success rate (uses cached failed_checks).
    +615
    +616        :return: Success rate (0.0 to 1.0)
    +617        """
    +618        if not self.checks:
    +619            return 1.0  # Early return
    +620
    +621        total = len(self.checks)
    +622        passed = total - len(self.failed_checks)  # Uses cached property
    +623        return passed / total
    +
    + + +

    Calculate success rate (uses cached failed_checks).

    + +
    Returns
    + +
    +

    Success rate (0.0 to 1.0)

    +
    +
    + + +
    +
    +
    + +
    + + class + HostnameRequest(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    506class HostnameRequest(BaseValidatedModel):
    +507    """Request model for setting hostname.
    +508
    +509    SCHEMA: Based on PUT /server/hostname-for-access-keys request body
    +510    """
    +511
    +512    hostname: str = Field(min_length=1)
    +
    + + +

    Request model for setting hostname.

    + +

    SCHEMA: Based on PUT /server/hostname-for-access-keys request body

    +
    + + +
    +
    + hostname: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    +
    + JsonDict = + + dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]] + + +
    + + + + +
    +
    +
    + JsonPayload = + + dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]] | list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]] | None + + +
    + + + + +
    +
    + +
    + + class + LocationMetric(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    403class LocationMetric(BaseValidatedModel):
    +404    """Location-based usage metric.
    +405
    +406    SCHEMA: Based on experimental metrics locations array item
    +407    """
    +408
    +409    location: str
    +410    asn: int | None = None
    +411    as_org: str | None = Field(None, alias="asOrg")
    +412    tunnel_time: TunnelTime = Field(alias="tunnelTime")
    +413    data_transferred: DataTransferred = Field(alias="dataTransferred")
    +
    + + +

    Location-based usage metric.

    + +

    SCHEMA: Based on experimental metrics locations array item

    +
    + + +
    +
    + location: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + asn: int | None = +None + + +
    + + + + +
    +
    +
    + as_org: str | None = +None + + +
    + + + + +
    +
    +
    + tunnel_time: TunnelTime = +PydanticUndefined + + +
    + + + + +
    +
    +
    + data_transferred: DataTransferred = +PydanticUndefined + + +
    + + + + +
    +
    +
    + +
    + + class + MetricsCollector(typing.Protocol): + + + +
    + +
    70class MetricsCollector(Protocol):
    +71    """Protocol for metrics collection.
    +72
    +73    Allows dependency injection of custom metrics backends.
    +74    """
    +75
    +76    def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None:
    +77        """Increment counter metric."""
    +78        ...
    +79
    +80    def timing(
    +81        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +82    ) -> None:
    +83        """Record timing metric."""
    +84        ...
    +85
    +86    def gauge(
    +87        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +88    ) -> None:
    +89        """Set gauge metric."""
    +90        ...
    +
    + + +

    Protocol for metrics collection.

    + +

    Allows dependency injection of custom metrics backends.

    +
    + + +
    + +
    + + MetricsCollector(*args, **kwargs) + + + +
    + +
    1957def _no_init_or_replace_init(self, *args, **kwargs):
    +1958    cls = type(self)
    +1959
    +1960    if cls._is_protocol:
    +1961        raise TypeError('Protocols cannot be instantiated')
    +1962
    +1963    # Already using a custom `__init__`. No need to calculate correct
    +1964    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
    +1965    if cls.__init__ is not _no_init_or_replace_init:
    +1966        return
    +1967
    +1968    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
    +1969    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
    +1970    # searches for a proper new `__init__` in the MRO. The new `__init__`
    +1971    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
    +1972    # instantiation of the protocol subclass will thus use the new
    +1973    # `__init__` and no longer call `_no_init_or_replace_init`.
    +1974    for base in cls.__mro__:
    +1975        init = base.__dict__.get('__init__', _no_init_or_replace_init)
    +1976        if init is not _no_init_or_replace_init:
    +1977            cls.__init__ = init
    +1978            break
    +1979    else:
    +1980        # should not happen
    +1981        cls.__init__ = object.__init__
    +1982
    +1983    cls.__init__(self, *args, **kwargs)
    +
    + + + + +
    +
    + +
    + + def + increment(self, metric: str, *, tags: dict[str, str] | None = None) -> None: + + + +
    + +
    76    def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None:
    +77        """Increment counter metric."""
    +78        ...
    +
    + + +

    Increment counter metric.

    +
    + + +
    +
    + +
    + + def + timing( self, metric: str, value: float, *, tags: dict[str, str] | None = None) -> None: + + + +
    + +
    80    def timing(
    +81        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +82    ) -> None:
    +83        """Record timing metric."""
    +84        ...
    +
    + + +

    Record timing metric.

    +
    + + +
    +
    + +
    + + def + gauge( self, metric: str, value: float, *, tags: dict[str, str] | None = None) -> None: + + + +
    + +
    86    def gauge(
    +87        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +88    ) -> None:
    +89        """Set gauge metric."""
    +90        ...
    +
    + + +

    Set gauge metric.

    +
    + + +
    +
    +
    + +
    + + class + MetricsEnabledRequest(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    551class MetricsEnabledRequest(BaseValidatedModel):
    +552    """Request model for enabling/disabling metrics.
    +553
    +554    SCHEMA: Based on PUT /metrics/enabled request body
    +555    """
    +556
    +557    metrics_enabled: bool = Field(alias="metricsEnabled")
    +
    + + +

    Request model for enabling/disabling metrics.

    + +

    SCHEMA: Based on PUT /metrics/enabled request body

    +
    + + +
    +
    + metrics_enabled: bool = +PydanticUndefined + + +
    + + + + +
    +
    +
    + +
    + + class + MetricsStatusResponse(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    560class MetricsStatusResponse(BaseValidatedModel):
    +561    """Response model for metrics status.
    +562
    +563    Returns current metrics sharing status.
    +564    SCHEMA: Based on GET /metrics/enabled response
    +565    """
    +566
    +567    metrics_enabled: bool = Field(alias="metricsEnabled")
    +
    + + +

    Response model for metrics status.

    + +

    Returns current metrics sharing status. +SCHEMA: Based on GET /metrics/enabled response

    +
    + + +
    +
    + metrics_enabled: bool = +PydanticUndefined + + +
    + + + + +
    +
    +
    +
    + MetricsTags = +dict[str, str] + + +
    + + + + +
    +
    + +
    + + class + MultiServerManager: + + + +
    + +
    557class MultiServerManager:
    +558    """High-performance manager for multiple Outline servers.
    +559
    +560    Features:
    +561    - Concurrent operations across all servers
    +562    - Health checking and automatic failover
    +563    - Aggregated metrics and status
    +564    - Graceful shutdown with cleanup
    +565    - Thread-safe operations
    +566
    +567    Limits:
    +568    - Maximum 50 servers (configurable via _MAX_SERVERS)
    +569    - Automatic cleanup with weak references
    +570    """
    +571
    +572    __slots__ = (
    +573        "_audit_logger",
    +574        "_clients",
    +575        "_configs",
    +576        "_default_timeout",
    +577        "_lock",
    +578        "_metrics",
    +579    )
    +580
    +581    def __init__(
    +582        self,
    +583        configs: Sequence[OutlineClientConfig],
    +584        *,
    +585        audit_logger: AuditLogger | None = None,
    +586        metrics: MetricsCollector | None = None,
    +587        default_timeout: float = _DEFAULT_SERVER_TIMEOUT,
    +588    ) -> None:
    +589        """Initialize multiserver manager.
    +590
    +591        :param configs: Sequence of server configurations
    +592        :param audit_logger: Shared audit logger for all servers
    +593        :param metrics: Shared metrics collector for all servers
    +594        :param default_timeout: Default timeout for operations (seconds)
    +595        :raises ConfigurationError: If too many servers or invalid configs
    +596        """
    +597        if len(configs) > _MAX_SERVERS:
    +598            raise ConfigurationError(
    +599                f"Too many servers: {len(configs)} (max: {_MAX_SERVERS})"
    +600            )
    +601
    +602        if not configs:
    +603            raise ConfigurationError("At least one server configuration required")
    +604
    +605        self._configs = list(configs)
    +606        self._clients: dict[str, AsyncOutlineClient] = {}
    +607        self._audit_logger = audit_logger
    +608        self._metrics = metrics
    +609        self._default_timeout = default_timeout
    +610        self._lock = asyncio.Lock()
    +611
    +612    @property
    +613    def server_count(self) -> int:
    +614        """Get total number of configured servers.
    +615
    +616        :return: Number of servers
    +617        """
    +618        return len(self._configs)
    +619
    +620    @property
    +621    def active_servers(self) -> int:
    +622        """Get number of active (connected) servers.
    +623
    +624        :return: Number of active servers
    +625        """
    +626        return sum(1 for client in self._clients.values() if client.is_connected)
    +627
    +628    def get_server_names(self) -> list[str]:
    +629        """Get list of sanitized server URLs.
    +630
    +631        URLs are sanitized to remove sensitive path information.
    +632
    +633        :return: List of safe server identifiers
    +634        """
    +635        return [
    +636            Validators.sanitize_url_for_logging(config.api_url)
    +637            for config in self._configs
    +638        ]
    +639
    +640    async def __aenter__(self) -> MultiServerManager:
    +641        """Async context manager entry.
    +642
    +643        :return: Self reference
    +644        :raises ConfigurationError: If NO servers can be initialized
    +645        """
    +646        async with self._lock:
    +647            # Create initialization tasks for concurrent execution
    +648            init_tasks = []
    +649            for config in self._configs:
    +650                client = AsyncOutlineClient(
    +651                    config=config,
    +652                    audit_logger=self._audit_logger,
    +653                    metrics=self._metrics,
    +654                )
    +655                init_tasks.append((config, client.__aenter__()))
    +656
    +657            results = await asyncio.gather(
    +658                *[task for _, task in init_tasks],
    +659                return_exceptions=True,
    +660            )
    +661
    +662            # Process results
    +663            errors: list[str] = []
    +664            for idx, ((config, _), result) in enumerate(
    +665                zip(init_tasks, results, strict=True)
    +666            ):
    +667                safe_url = Validators.sanitize_url_for_logging(config.api_url)
    +668
    +669                if isinstance(result, Exception):
    +670                    error_msg = f"Failed to initialize server {safe_url}: {result}"
    +671                    errors.append(error_msg)
    +672                    if logger.isEnabledFor(logging.WARNING):
    +673                        logger.warning(error_msg)
    +674                else:
    +675                    # Get the client that was initialized
    +676                    client = AsyncOutlineClient(
    +677                        config=config,
    +678                        audit_logger=self._audit_logger,
    +679                        metrics=self._metrics,
    +680                    )
    +681                    self._clients[safe_url] = client
    +682
    +683                    if logger.isEnabledFor(logging.INFO):
    +684                        logger.info(
    +685                            "Server %d/%d initialized: %s",
    +686                            idx + 1,
    +687                            len(self._configs),
    +688                            safe_url,
    +689                        )
    +690
    +691            if not self._clients:
    +692                raise ConfigurationError(
    +693                    f"Failed to initialize any servers. Errors: {'; '.join(errors)}"
    +694                )
    +695
    +696            if logger.isEnabledFor(logging.INFO):
    +697                logger.info(
    +698                    "MultiServerManager ready: %d/%d servers active",
    +699                    len(self._clients),
    +700                    len(self._configs),
    +701                )
    +702
    +703        return self
    +704
    +705    async def __aexit__(
    +706        self,
    +707        exc_type: type[BaseException] | None,
    +708        exc_val: BaseException | None,
    +709        exc_tb: object | None,
    +710    ) -> bool:
    +711        """Async context manager exit.
    +712
    +713        :param exc_type: Exception type
    +714        :param exc_val: Exception value
    +715        :param exc_tb: Exception traceback
    +716        :return: False to propagate exceptions
    +717        """
    +718        async with self._lock:
    +719            shutdown_tasks = [
    +720                client.__aexit__(None, None, None) for client in self._clients.values()
    +721            ]
    +722
    +723            results = await asyncio.gather(*shutdown_tasks, return_exceptions=True)
    +724
    +725            errors = [
    +726                f"{server_id}: {result}"
    +727                for (server_id, _), result in zip(
    +728                    self._clients.items(), results, strict=False
    +729                )
    +730                if isinstance(result, Exception)
    +731            ]
    +732
    +733            self._clients.clear()
    +734
    +735            if errors and logger.isEnabledFor(logging.WARNING):
    +736                logger.warning("Shutdown completed with %d error(s)", len(errors))
    +737
    +738        return False
    +739
    +740    def get_client(self, server_identifier: str | int) -> AsyncOutlineClient:
    +741        """Get client by server identifier or index.
    +742
    +743        :param server_identifier: Server URL (sanitized) or 0-based index
    +744        :return: Client instance
    +745        :raises KeyError: If server not found
    +746        :raises IndexError: If index out of range
    +747        """
    +748        # Try as index first (fast path for common case)
    +749        if isinstance(server_identifier, int):
    +750            if 0 <= server_identifier < len(self._configs):
    +751                config = self._configs[server_identifier]
    +752                safe_url = Validators.sanitize_url_for_logging(config.api_url)
    +753                return self._clients[safe_url]
    +754            raise IndexError(
    +755                f"Server index {server_identifier} out of range (0-{len(self._configs) - 1})"
    +756            )
    +757
    +758        # Try as server ID
    +759        if server_identifier in self._clients:
    +760            return self._clients[server_identifier]
    +761
    +762        raise KeyError(f"Server not found: {server_identifier}")
    +763
    +764    def get_all_clients(self) -> list[AsyncOutlineClient]:
    +765        """Get all active clients.
    +766
    +767        :return: List of client instances
    +768        """
    +769        return list(self._clients.values())
    +770
    +771    async def health_check_all(
    +772        self,
    +773        timeout: float | None = None,
    +774    ) -> dict[str, dict[str, Any]]:
    +775        """Perform health check on all servers concurrently.
    +776
    +777        :param timeout: Timeout for each health check
    +778        :return: Dictionary mapping server IDs to health check results
    +779        """
    +780        timeout = timeout or self._default_timeout
    +781
    +782        tasks = [
    +783            self._health_check_single(server_id, client, timeout)
    +784            for server_id, client in self._clients.items()
    +785        ]
    +786
    +787        # Execute concurrently
    +788        results_list = await asyncio.gather(*tasks, return_exceptions=True)
    +789
    +790        # Build result dictionary
    +791        results: dict[str, dict[str, Any]] = {}
    +792        for (server_id, _), result in zip(
    +793            self._clients.items(), results_list, strict=False
    +794        ):
    +795            if isinstance(result, BaseException):
    +796                results[server_id] = {
    +797                    "healthy": False,
    +798                    "error": str(result),
    +799                    "error_type": type(result).__name__,
    +800                }
    +801            else:
    +802                results[server_id] = result
    +803
    +804        return results
    +805
    +806    @staticmethod
    +807    async def _health_check_single(
    +808        server_id: str,
    +809        client: AsyncOutlineClient,
    +810        timeout: float,
    +811    ) -> dict[str, Any]:
    +812        """Perform health check on a single server with timeout.
    +813
    +814        :param server_id: Server identifier
    +815        :param client: Client instance
    +816        :param timeout: Timeout for operation
    +817        :return: Health check result
    +818        """
    +819        try:
    +820            result = await asyncio.wait_for(
    +821                client.health_check(),
    +822                timeout=timeout,
    +823            )
    +824            result["server_id"] = server_id
    +825            return result
    +826        except asyncio.TimeoutError:
    +827            return {
    +828                "server_id": server_id,
    +829                "healthy": False,
    +830                "error": f"Health check timeout after {timeout}s",
    +831                "error_type": "TimeoutError",
    +832            }
    +833        except Exception as e:
    +834            return {
    +835                "server_id": server_id,
    +836                "healthy": False,
    +837                "error": str(e),
    +838                "error_type": type(e).__name__,
    +839            }
    +840
    +841    async def get_healthy_servers(
    +842        self,
    +843        timeout: float | None = None,
    +844    ) -> list[AsyncOutlineClient]:
    +845        """Get list of healthy servers after health check.
    +846
    +847        :param timeout: Timeout for health checks
    +848        :return: List of healthy clients
    +849        """
    +850        health_results = await self.health_check_all(timeout=timeout)
    +851
    +852        healthy_clients: list[AsyncOutlineClient] = []
    +853        for server_id, result in health_results.items():
    +854            if result.get("healthy", False):
    +855                try:
    +856                    client = self.get_client(server_id)
    +857                    healthy_clients.append(client)
    +858                except (KeyError, IndexError):
    +859                    continue
    +860
    +861        return healthy_clients
    +862
    +863    def get_status_summary(self) -> dict[str, Any]:
    +864        """Get aggregated status summary for all servers.
    +865
    +866        Synchronous operation - no API calls made.
    +867
    +868        :return: Status summary dictionary
    +869        """
    +870        return {
    +871            "total_servers": len(self._configs),
    +872            "active_servers": self.active_servers,
    +873            "server_statuses": {
    +874                server_id: client.get_status()
    +875                for server_id, client in self._clients.items()
    +876            },
    +877        }
    +878
    +879    def __repr__(self) -> str:
    +880        """String representation.
    +881
    +882        :return: String representation
    +883        """
    +884        active = self.active_servers
    +885        total = self.server_count
    +886        return f"MultiServerManager(servers={active}/{total} active)"
    +
    + + +

    High-performance manager for multiple Outline servers.

    + +

    Features:

    + +
      +
    • Concurrent operations across all servers
    • +
    • Health checking and automatic failover
    • +
    • Aggregated metrics and status
    • +
    • Graceful shutdown with cleanup
    • +
    • Thread-safe operations
    • +
    + +

    Limits:

    + +
      +
    • Maximum 50 servers (configurable via _MAX_SERVERS)
    • +
    • Automatic cleanup with weak references
    • +
    +
    + + +
    + +
    + + MultiServerManager( configs: Sequence[OutlineClientConfig], *, audit_logger: AuditLogger | None = None, metrics: MetricsCollector | None = None, default_timeout: float = 5.0) + + + +
    + +
    581    def __init__(
    +582        self,
    +583        configs: Sequence[OutlineClientConfig],
    +584        *,
    +585        audit_logger: AuditLogger | None = None,
    +586        metrics: MetricsCollector | None = None,
    +587        default_timeout: float = _DEFAULT_SERVER_TIMEOUT,
    +588    ) -> None:
    +589        """Initialize multiserver manager.
    +590
    +591        :param configs: Sequence of server configurations
    +592        :param audit_logger: Shared audit logger for all servers
    +593        :param metrics: Shared metrics collector for all servers
    +594        :param default_timeout: Default timeout for operations (seconds)
    +595        :raises ConfigurationError: If too many servers or invalid configs
    +596        """
    +597        if len(configs) > _MAX_SERVERS:
    +598            raise ConfigurationError(
    +599                f"Too many servers: {len(configs)} (max: {_MAX_SERVERS})"
    +600            )
    +601
    +602        if not configs:
    +603            raise ConfigurationError("At least one server configuration required")
    +604
    +605        self._configs = list(configs)
    +606        self._clients: dict[str, AsyncOutlineClient] = {}
    +607        self._audit_logger = audit_logger
    +608        self._metrics = metrics
    +609        self._default_timeout = default_timeout
    +610        self._lock = asyncio.Lock()
    +
    + + +

    Initialize multiserver manager.

    + +
    Parameters
    + +
      +
    • configs: Sequence of server configurations
    • +
    • audit_logger: Shared audit logger for all servers
    • +
    • metrics: Shared metrics collector for all servers
    • +
    • default_timeout: Default timeout for operations (seconds)
    • +
    + +
    Raises
    + +
      +
    • ConfigurationError: If too many servers or invalid configs
    • +
    +
    + + +
    +
    + +
    + server_count: int + + + +
    + +
    612    @property
    +613    def server_count(self) -> int:
    +614        """Get total number of configured servers.
    +615
    +616        :return: Number of servers
    +617        """
    +618        return len(self._configs)
    +
    + + +

    Get total number of configured servers.

    + +
    Returns
    + +
    +

    Number of servers

    +
    +
    + + +
    +
    + +
    + active_servers: int + + + +
    + +
    620    @property
    +621    def active_servers(self) -> int:
    +622        """Get number of active (connected) servers.
    +623
    +624        :return: Number of active servers
    +625        """
    +626        return sum(1 for client in self._clients.values() if client.is_connected)
    +
    + + +

    Get number of active (connected) servers.

    + +
    Returns
    + +
    +

    Number of active servers

    +
    +
    + + +
    +
    + +
    + + def + get_server_names(self) -> list[str]: + + + +
    + +
    628    def get_server_names(self) -> list[str]:
    +629        """Get list of sanitized server URLs.
    +630
    +631        URLs are sanitized to remove sensitive path information.
    +632
    +633        :return: List of safe server identifiers
    +634        """
    +635        return [
    +636            Validators.sanitize_url_for_logging(config.api_url)
    +637            for config in self._configs
    +638        ]
    +
    + + +

    Get list of sanitized server URLs.

    + +

    URLs are sanitized to remove sensitive path information.

    + +
    Returns
    + +
    +

    List of safe server identifiers

    +
    +
    + + +
    +
    + +
    + + def + get_client( self, server_identifier: str | int) -> AsyncOutlineClient: + + + +
    + +
    740    def get_client(self, server_identifier: str | int) -> AsyncOutlineClient:
    +741        """Get client by server identifier or index.
    +742
    +743        :param server_identifier: Server URL (sanitized) or 0-based index
    +744        :return: Client instance
    +745        :raises KeyError: If server not found
    +746        :raises IndexError: If index out of range
    +747        """
    +748        # Try as index first (fast path for common case)
    +749        if isinstance(server_identifier, int):
    +750            if 0 <= server_identifier < len(self._configs):
    +751                config = self._configs[server_identifier]
    +752                safe_url = Validators.sanitize_url_for_logging(config.api_url)
    +753                return self._clients[safe_url]
    +754            raise IndexError(
    +755                f"Server index {server_identifier} out of range (0-{len(self._configs) - 1})"
    +756            )
    +757
    +758        # Try as server ID
    +759        if server_identifier in self._clients:
    +760            return self._clients[server_identifier]
    +761
    +762        raise KeyError(f"Server not found: {server_identifier}")
    +
    + + +

    Get client by server identifier or index.

    + +
    Parameters
    + +
      +
    • server_identifier: Server URL (sanitized) or 0-based index
    • +
    + +
    Returns
    + +
    +

    Client instance

    +
    + +
    Raises
    + +
      +
    • KeyError: If server not found
    • +
    • IndexError: If index out of range
    • +
    +
    + + +
    +
    + +
    + + def + get_all_clients(self) -> list[AsyncOutlineClient]: + + + +
    + +
    764    def get_all_clients(self) -> list[AsyncOutlineClient]:
    +765        """Get all active clients.
    +766
    +767        :return: List of client instances
    +768        """
    +769        return list(self._clients.values())
    +
    + + +

    Get all active clients.

    + +
    Returns
    + +
    +

    List of client instances

    +
    +
    + + +
    +
    + +
    + + async def + health_check_all(self, timeout: float | None = None) -> dict[str, dict[str, typing.Any]]: + + + +
    + +
    771    async def health_check_all(
    +772        self,
    +773        timeout: float | None = None,
    +774    ) -> dict[str, dict[str, Any]]:
    +775        """Perform health check on all servers concurrently.
    +776
    +777        :param timeout: Timeout for each health check
    +778        :return: Dictionary mapping server IDs to health check results
    +779        """
    +780        timeout = timeout or self._default_timeout
    +781
    +782        tasks = [
    +783            self._health_check_single(server_id, client, timeout)
    +784            for server_id, client in self._clients.items()
    +785        ]
    +786
    +787        # Execute concurrently
    +788        results_list = await asyncio.gather(*tasks, return_exceptions=True)
    +789
    +790        # Build result dictionary
    +791        results: dict[str, dict[str, Any]] = {}
    +792        for (server_id, _), result in zip(
    +793            self._clients.items(), results_list, strict=False
    +794        ):
    +795            if isinstance(result, BaseException):
    +796                results[server_id] = {
    +797                    "healthy": False,
    +798                    "error": str(result),
    +799                    "error_type": type(result).__name__,
    +800                }
    +801            else:
    +802                results[server_id] = result
    +803
    +804        return results
    +
    + + +

    Perform health check on all servers concurrently.

    + +
    Parameters
    + +
      +
    • timeout: Timeout for each health check
    • +
    + +
    Returns
    + +
    +

    Dictionary mapping server IDs to health check results

    +
    +
    + + +
    +
    + +
    + + async def + get_healthy_servers( self, timeout: float | None = None) -> list[AsyncOutlineClient]: + + + +
    + +
    841    async def get_healthy_servers(
    +842        self,
    +843        timeout: float | None = None,
    +844    ) -> list[AsyncOutlineClient]:
    +845        """Get list of healthy servers after health check.
    +846
    +847        :param timeout: Timeout for health checks
    +848        :return: List of healthy clients
    +849        """
    +850        health_results = await self.health_check_all(timeout=timeout)
    +851
    +852        healthy_clients: list[AsyncOutlineClient] = []
    +853        for server_id, result in health_results.items():
    +854            if result.get("healthy", False):
    +855                try:
    +856                    client = self.get_client(server_id)
    +857                    healthy_clients.append(client)
    +858                except (KeyError, IndexError):
    +859                    continue
    +860
    +861        return healthy_clients
    +
    + + +

    Get list of healthy servers after health check.

    + +
    Parameters
    + +
      +
    • timeout: Timeout for health checks
    • +
    + +
    Returns
    + +
    +

    List of healthy clients

    +
    +
    + + +
    +
    + +
    + + def + get_status_summary(self) -> dict[str, typing.Any]: + + + +
    + +
    863    def get_status_summary(self) -> dict[str, Any]:
    +864        """Get aggregated status summary for all servers.
    +865
    +866        Synchronous operation - no API calls made.
    +867
    +868        :return: Status summary dictionary
    +869        """
    +870        return {
    +871            "total_servers": len(self._configs),
    +872            "active_servers": self.active_servers,
    +873            "server_statuses": {
    +874                server_id: client.get_status()
    +875                for server_id, client in self._clients.items()
    +876            },
    +877        }
    +
    + + +

    Get aggregated status summary for all servers.

    + +

    Synchronous operation - no API calls made.

    + +
    Returns
    + +
    +

    Status summary dictionary

    +
    +
    + + +
    +
    +
    + +
    + + class + NoOpAuditLogger: + + + +
    + +
    537class NoOpAuditLogger:
    +538    """Zero-overhead no-op audit logger.
    +539
    +540    Implements AuditLogger protocol but performs no operations.
    +541    Useful for disabling audit without code changes or performance impact.
    +542    """
    +543
    +544    __slots__ = ()
    +545
    +546    async def alog_action(
    +547        self,
    +548        action: str,
    +549        resource: str,
    +550        *,
    +551        user: str | None = None,
    +552        details: dict[str, Any] | None = None,
    +553        correlation_id: str | None = None,
    +554    ) -> None:
    +555        """No-op async log."""
    +556
    +557    def log_action(
    +558        self,
    +559        action: str,
    +560        resource: str,
    +561        *,
    +562        user: str | None = None,
    +563        details: dict[str, Any] | None = None,
    +564        correlation_id: str | None = None,
    +565    ) -> None:
    +566        """No-op sync log."""
    +567
    +568    async def shutdown(self) -> None:
    +569        """No-op shutdown."""
    +
    + + +

    Zero-overhead no-op audit logger.

    + +

    Implements AuditLogger protocol but performs no operations. +Useful for disabling audit without code changes or performance impact.

    +
    + + +
    + +
    + + async def + alog_action( self, action: str, resource: str, *, user: str | None = None, details: dict[str, typing.Any] | None = None, correlation_id: str | None = None) -> None: + + + +
    + +
    546    async def alog_action(
    +547        self,
    +548        action: str,
    +549        resource: str,
    +550        *,
    +551        user: str | None = None,
    +552        details: dict[str, Any] | None = None,
    +553        correlation_id: str | None = None,
    +554    ) -> None:
    +555        """No-op async log."""
    +
    + + +

    No-op async log.

    +
    + + +
    +
    + +
    + + def + log_action( self, action: str, resource: str, *, user: str | None = None, details: dict[str, typing.Any] | None = None, correlation_id: str | None = None) -> None: + + + +
    + +
    557    def log_action(
    +558        self,
    +559        action: str,
    +560        resource: str,
    +561        *,
    +562        user: str | None = None,
    +563        details: dict[str, Any] | None = None,
    +564        correlation_id: str | None = None,
    +565    ) -> None:
    +566        """No-op sync log."""
    +
    + + +

    No-op sync log.

    +
    + + +
    +
    + +
    + + async def + shutdown(self) -> None: + + + +
    + +
    568    async def shutdown(self) -> None:
    +569        """No-op shutdown."""
    +
    + + +

    No-op shutdown.

    +
    + + +
    +
    +
    + +
    + + class + NoOpMetrics: + + + +
    + +
     93class NoOpMetrics:
    + 94    """No-op metrics collector (zero-overhead default).
    + 95
    + 96    Uses __slots__ to minimize memory footprint.
    + 97    """
    + 98
    + 99    __slots__ = ()
    +100
    +101    def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None:
    +102        """No-op increment (zero overhead)."""
    +103
    +104    def timing(
    +105        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +106    ) -> None:
    +107        """No-op timing (zero overhead)."""
    +108
    +109    def gauge(
    +110        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +111    ) -> None:
    +112        """No-op gauge (zero overhead)."""
    +
    + + +

    No-op metrics collector (zero-overhead default).

    + +

    Uses __slots__ to minimize memory footprint.

    +
    + + +
    + +
    + + def + increment(self, metric: str, *, tags: dict[str, str] | None = None) -> None: + + + +
    + +
    101    def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None:
    +102        """No-op increment (zero overhead)."""
    +
    + + +

    No-op increment (zero overhead).

    +
    + + +
    +
    + +
    + + def + timing( self, metric: str, value: float, *, tags: dict[str, str] | None = None) -> None: + + + +
    + +
    104    def timing(
    +105        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +106    ) -> None:
    +107        """No-op timing (zero overhead)."""
    +
    + + +

    No-op timing (zero overhead).

    +
    + + +
    +
    + +
    + + def + gauge( self, metric: str, value: float, *, tags: dict[str, str] | None = None) -> None: + + + +
    + +
    109    def gauge(
    +110        self, metric: str, value: float, *, tags: MetricsTags | None = None
    +111    ) -> None:
    +112        """No-op gauge (zero overhead)."""
    +
    + + +

    No-op gauge (zero overhead).

    +
    + + +
    +
    +
    + +
    + + class + OutlineClientConfig(pydantic_settings.main.BaseSettings): + + + +
    + +
     74class OutlineClientConfig(BaseSettings):
    + 75    """Main configuration."""
    + 76
    + 77    model_config = SettingsConfigDict(
    + 78        env_prefix=_ENV_PREFIX,
    + 79        env_file=".env",
    + 80        env_file_encoding="utf-8",
    + 81        case_sensitive=False,
    + 82        extra="forbid",
    + 83        validate_assignment=True,
    + 84        validate_default=True,
    + 85        frozen=False,
    + 86    )
    + 87
    + 88    # ===== Core Settings (Required) =====
    + 89
    + 90    api_url: str = Field(..., description="Outline server API URL with secret path")
    + 91    cert_sha256: SecretStr = Field(..., description="SHA-256 certificate fingerprint")
    + 92
    + 93    # ===== Client Settings =====
    + 94
    + 95    timeout: int = Field(
    + 96        default=10,
    + 97        ge=_MIN_TIMEOUT,
    + 98        le=_MAX_TIMEOUT,
    + 99        description="Request timeout (seconds)",
    +100    )
    +101    retry_attempts: int = Field(
    +102        default=2,
    +103        ge=_MIN_RETRY,
    +104        le=_MAX_RETRY,
    +105        description="Number of retries",
    +106    )
    +107    max_connections: int = Field(
    +108        default=10,
    +109        ge=_MIN_CONNECTIONS,
    +110        le=_MAX_CONNECTIONS,
    +111        description="Connection pool size",
    +112    )
    +113    rate_limit: int = Field(
    +114        default=100,
    +115        ge=_MIN_RATE_LIMIT,
    +116        le=_MAX_RATE_LIMIT,
    +117        description="Max concurrent requests",
    +118    )
    +119    user_agent: str = Field(
    +120        default=Constants.DEFAULT_USER_AGENT,
    +121        min_length=1,
    +122        max_length=256,
    +123        description="Custom user agent string",
    +124    )
    +125
    +126    # ===== Optional Features =====
    +127
    +128    enable_circuit_breaker: bool = Field(
    +129        default=True,
    +130        description="Enable circuit breaker",
    +131    )
    +132    enable_logging: bool = Field(
    +133        default=False,
    +134        description="Enable debug logging",
    +135    )
    +136    json_format: bool = Field(
    +137        default=False,
    +138        description="Return raw JSON",
    +139    )
    +140    allow_private_networks: bool = Field(
    +141        default=True,
    +142        description="Allow private or local network addresses in api_url",
    +143    )
    +144    resolve_dns_for_ssrf: bool = Field(
    +145        default=False,
    +146        description="Resolve DNS for SSRF checks (strict mode)",
    +147    )
    +148
    +149    # ===== Circuit Breaker Settings =====
    +150
    +151    circuit_failure_threshold: int = Field(
    +152        default=5,
    +153        ge=1,
    +154        le=100,
    +155        description="Failures before opening",
    +156    )
    +157    circuit_recovery_timeout: float = Field(
    +158        default=60.0,
    +159        ge=1.0,
    +160        le=3600.0,
    +161        description="Recovery wait time (seconds)",
    +162    )
    +163    circuit_success_threshold: int = Field(
    +164        default=2,
    +165        ge=1,
    +166        le=10,
    +167        description="Successes needed to close",
    +168    )
    +169    circuit_call_timeout: float = Field(
    +170        default=10.0,
    +171        ge=0.1,
    +172        le=300.0,
    +173        description="Circuit call timeout (seconds)",
    +174    )
    +175
    +176    # ===== Validators =====
    +177
    +178    @field_validator("api_url")
    +179    @classmethod
    +180    def validate_api_url(cls, v: str) -> str:
    +181        """Validate and normalize API URL with optimized regex.
    +182
    +183        :param v: URL to validate
    +184        :return: Validated URL
    +185        :raises ValueError: If URL is invalid
    +186        """
    +187        return Validators.validate_url(v)
    +188
    +189    @field_validator("cert_sha256")
    +190    @classmethod
    +191    def validate_cert(cls, v: SecretStr) -> SecretStr:
    +192        """Validate certificate fingerprint with constant-time comparison.
    +193
    +194        :param v: Certificate fingerprint
    +195        :return: Validated fingerprint
    +196        :raises ValueError: If fingerprint is invalid
    +197        """
    +198        return Validators.validate_cert_fingerprint(v)
    +199
    +200    @field_validator("user_agent")
    +201    @classmethod
    +202    def validate_user_agent(cls, v: str) -> str:
    +203        """Validate user agent string with efficient control char check.
    +204
    +205        :param v: User agent to validate
    +206        :return: Validated user agent
    +207        :raises ValueError: If user agent is invalid
    +208        """
    +209        v = Validators.validate_string_not_empty(v, "User agent")
    +210
    +211        # Efficient control character check using generator
    +212        if any(ord(c) < 32 for c in v):
    +213            raise ValueError("User agent contains invalid control characters")
    +214
    +215        return v
    +216
    +217    @model_validator(mode="after")
    +218    def validate_config(self) -> Self:
    +219        """Additional validation after model creation with pattern matching.
    +220
    +221        :return: Validated configuration instance
    +222        """
    +223        # Security warning for HTTP using pattern matching
    +224        match (self.api_url, "localhost" in self.api_url):
    +225            case (url, False) if "http://" in url:
    +226                _log_if_enabled(
    +227                    logging.WARNING,
    +228                    "Using HTTP for non-localhost connection. "
    +229                    "This is insecure and should only be used for testing.",
    +230                )
    +231
    +232        # Optional SSRF protection for private networks (no DNS resolution)
    +233        Validators.validate_url(
    +234            self.api_url,
    +235            allow_private_networks=self.allow_private_networks,
    +236            resolve_dns=self.resolve_dns_for_ssrf,
    +237        )
    +238
    +239        # Circuit breaker timeout adjustment with caching
    +240        if self.enable_circuit_breaker:
    +241            max_request_time = self._get_max_request_time()
    +242
    +243            if self.circuit_call_timeout < max_request_time:
    +244                _log_if_enabled(
    +245                    logging.WARNING,
    +246                    f"Circuit timeout ({self.circuit_call_timeout}s) is less than "
    +247                    f"max request time ({max_request_time}s). "
    +248                    f"Auto-adjusting to {max_request_time}s.",
    +249                )
    +250                object.__setattr__(self, "circuit_call_timeout", max_request_time)
    +251
    +252        return self
    +253
    +254    def _get_max_request_time(self) -> float:
    +255        """Calculate worst-case request time with instance caching.
    +256
    +257        :return: Maximum request time in seconds
    +258        """
    +259        if not hasattr(self, "_cached_max_request_time"):
    +260            self._cached_max_request_time = (
    +261                self.timeout * (self.retry_attempts + 1) + _SAFETY_MARGIN
    +262            )
    +263        return self._cached_max_request_time
    +264
    +265    # ===== Custom __setattr__ for SecretStr Protection =====
    +266
    +267    def __setattr__(self, name: str, value: object) -> None:
    +268        """Prevent accidental string assignment to SecretStr fields.
    +269
    +270        :param name: Attribute name
    +271        :param value: Attribute value
    +272        :raises TypeError: If trying to assign str to SecretStr field
    +273        """
    +274        # Fast path: skip check for non-cert fields
    +275        if name != "cert_sha256":
    +276            super().__setattr__(name, value)
    +277            return
    +278
    +279        if isinstance(value, str):
    +280            raise TypeError(
    +281                "cert_sha256 must be SecretStr, not str. " "Use: SecretStr('your_cert')"
    +282            )
    +283
    +284        super().__setattr__(name, value)
    +285
    +286    # ===== Helper Methods =====
    +287
    +288    @cached_property
    +289    def get_sanitized_config(self) -> ConfigDict:
    +290        """Get configuration with sensitive data masked (cached).
    +291
    +292        Safe for logging, debugging, and display.
    +293
    +294        Performance: ~20x speedup with caching for repeated calls
    +295        Memory: Single cached result per instance
    +296
    +297        :return: Sanitized configuration dictionary
    +298        """
    +299        return {
    +300            "api_url": Validators.sanitize_url_for_logging(self.api_url),
    +301            "cert_sha256": "***MASKED***",
    +302            "timeout": self.timeout,
    +303            "retry_attempts": self.retry_attempts,
    +304            "max_connections": self.max_connections,
    +305            "rate_limit": self.rate_limit,
    +306            "user_agent": self.user_agent,
    +307            "enable_circuit_breaker": self.enable_circuit_breaker,
    +308            "enable_logging": self.enable_logging,
    +309            "json_format": self.json_format,
    +310            "allow_private_networks": self.allow_private_networks,
    +311            "circuit_failure_threshold": self.circuit_failure_threshold,
    +312            "circuit_recovery_timeout": self.circuit_recovery_timeout,
    +313            "circuit_success_threshold": self.circuit_success_threshold,
    +314            "circuit_call_timeout": self.circuit_call_timeout,
    +315        }
    +316
    +317    def model_copy_immutable(self, **overrides: ConfigValue) -> OutlineClientConfig:
    +318        """Create immutable copy with overrides (optimized validation).
    +319
    +320        :param overrides: Configuration parameters to override
    +321        :return: Deep copy of configuration with applied updates
    +322        :raises ValueError: If invalid override keys provided
    +323
    +324        Example:
    +325            >>> new_config = config.model_copy_immutable(timeout=20)
    +326        """
    +327        # Optimized: Use frozenset intersection for O(1) validation
    +328        valid_keys = frozenset(ConfigOverrides.__annotations__.keys())
    +329        provided_keys = frozenset(overrides.keys())
    +330        invalid = provided_keys - valid_keys
    +331
    +332        if invalid:
    +333            raise ValueError(
    +334                f"Invalid configuration keys: {', '.join(sorted(invalid))}. "
    +335                f"Valid keys: {', '.join(sorted(valid_keys))}"
    +336            )
    +337
    +338        # Pydantic's model_copy is already optimized
    +339        return cast(  # type: ignore[redundant-cast, unused-ignore]
    +340            OutlineClientConfig, self.model_copy(deep=True, update=overrides)
    +341        )
    +342
    +343    @property
    +344    def circuit_config(self) -> CircuitConfig | None:
    +345        """Get circuit breaker configuration if enabled.
    +346
    +347        Returns None if circuit breaker is disabled, otherwise CircuitConfig instance.
    +348        Cached as property for performance.
    +349
    +350        :return: Circuit config or None if disabled
    +351        """
    +352        if not self.enable_circuit_breaker:
    +353            return None
    +354
    +355        return CircuitConfig(
    +356            failure_threshold=self.circuit_failure_threshold,
    +357            recovery_timeout=self.circuit_recovery_timeout,
    +358            success_threshold=self.circuit_success_threshold,
    +359            call_timeout=self.circuit_call_timeout,
    +360        )
    +361
    +362    # ===== Factory Methods =====
    +363
    +364    @classmethod
    +365    def from_env(
    +366        cls,
    +367        env_file: str | Path | None = None,
    +368        **overrides: ConfigValue,
    +369    ) -> OutlineClientConfig:
    +370        """Load configuration from environment with overrides.
    +371
    +372        :param env_file: Path to .env file
    +373        :param overrides: Configuration parameters to override
    +374        :return: Configuration instance
    +375        :raises ConfigurationError: If environment configuration is invalid
    +376
    +377        Example:
    +378            >>> config = OutlineClientConfig.from_env(
    +379            ...     env_file=".env.prod",
    +380            ...     timeout=20,
    +381            ...     enable_logging=True
    +382            ... )
    +383        """
    +384        # Fast path: validate overrides early
    +385        valid_keys = frozenset(ConfigOverrides.__annotations__.keys())
    +386        filtered_overrides = cast(
    +387            ConfigOverrides,
    +388            {k: v for k, v in overrides.items() if k in valid_keys},
    +389        )
    +390
    +391        if not env_file:
    +392            return cls(  # type: ignore[call-arg, unused-ignore]
    +393                **filtered_overrides
    +394            )
    +395
    +396        match env_file:
    +397            case str():
    +398                env_path = Path(env_file)
    +399            case Path():
    +400                env_path = env_file
    +401            case _:
    +402                raise TypeError(
    +403                    f"env_file must be str or Path, got {type(env_file).__name__}"
    +404                )
    +405
    +406        if not env_path.exists():
    +407            raise ConfigurationError(
    +408                f"Environment file not found: {env_path}",
    +409                field="env_file",
    +410            )
    +411
    +412        return cls(  # type: ignore[call-arg, unused-ignore]
    +413            _env_file=str(env_path),
    +414            **filtered_overrides,
    +415        )
    +416
    +417    @classmethod
    +418    def create_minimal(
    +419        cls,
    +420        api_url: str,
    +421        cert_sha256: str | SecretStr,
    +422        **overrides: ConfigValue,
    +423    ) -> OutlineClientConfig:
    +424        """Create minimal configuration (optimized validation).
    +425
    +426        :param api_url: API URL
    +427        :param cert_sha256: Certificate fingerprint
    +428        :param overrides: Optional configuration parameters
    +429        :return: Configuration instance
    +430        :raises TypeError: If cert_sha256 is not str or SecretStr
    +431
    +432        Example:
    +433            >>> config = OutlineClientConfig.create_minimal(
    +434            ...     api_url="https://server.com/path",
    +435            ...     cert_sha256="a" * 64,
    +436            ...     timeout=20
    +437            ... )
    +438        """
    +439        match cert_sha256:
    +440            case str():
    +441                cert = SecretStr(cert_sha256)
    +442            case SecretStr():
    +443                cert = cert_sha256
    +444            case _:
    +445                raise TypeError(
    +446                    f"cert_sha256 must be str or SecretStr, "
    +447                    f"got {type(cert_sha256).__name__}"
    +448                )
    +449
    +450        valid_keys = frozenset(ConfigOverrides.__annotations__.keys())
    +451        filtered_overrides = cast(
    +452            ConfigOverrides,
    +453            {k: v for k, v in overrides.items() if k in valid_keys},
    +454        )
    +455
    +456        return cls(
    +457            api_url=api_url,
    +458            cert_sha256=cert,
    +459            **filtered_overrides,
    +460        )
    +
    + + +

    Main configuration.

    +
    + + +
    +
    + api_url: str = +PydanticUndefined + + +
    + + +

    Outline server API URL with secret path

    +
    + + +
    +
    +
    + cert_sha256: pydantic.types.SecretStr = +PydanticUndefined + + +
    + + +

    SHA-256 certificate fingerprint

    +
    + + +
    +
    +
    + timeout: int = +10 + + +
    + + +

    Request timeout (seconds)

    +
    + + +
    +
    +
    + retry_attempts: int = +2 + + +
    + + +

    Number of retries

    +
    + + +
    +
    +
    + max_connections: int = +10 + + +
    + + +

    Connection pool size

    +
    + + +
    +
    +
    + rate_limit: int = +100 + + +
    + + +

    Max concurrent requests

    +
    + + +
    +
    +
    + user_agent: str = +'PyOutlineAPI/0.4.0' + + +
    + + +

    Custom user agent string

    +
    + + +
    +
    +
    + enable_circuit_breaker: bool = +True + + +
    + + +

    Enable circuit breaker

    +
    + + +
    +
    +
    + enable_logging: bool = +False + + +
    + + +

    Enable debug logging

    +
    + + +
    +
    +
    + json_format: bool = +False + + +
    + + +

    Return raw JSON

    +
    + + +
    +
    +
    + allow_private_networks: bool = +True + + +
    + + +

    Allow private or local network addresses in api_url

    +
    + + +
    +
    +
    + resolve_dns_for_ssrf: bool = +False + + +
    + + +

    Resolve DNS for SSRF checks (strict mode)

    +
    + + +
    +
    +
    + circuit_failure_threshold: int = +5 + + +
    + + +

    Failures before opening

    +
    + + +
    +
    +
    + circuit_recovery_timeout: float = +60.0 + + +
    + + +

    Recovery wait time (seconds)

    +
    + + +
    +
    +
    + circuit_success_threshold: int = +2 + + +
    + + +

    Successes needed to close

    +
    + + +
    +
    +
    + circuit_call_timeout: float = +10.0 + + +
    + + +

    Circuit call timeout (seconds)

    +
    + + +
    +
    + +
    +
    @field_validator('api_url')
    +
    @classmethod
    + + def + validate_api_url(cls, v: str) -> str: + + + +
    + +
    178    @field_validator("api_url")
    +179    @classmethod
    +180    def validate_api_url(cls, v: str) -> str:
    +181        """Validate and normalize API URL with optimized regex.
    +182
    +183        :param v: URL to validate
    +184        :return: Validated URL
    +185        :raises ValueError: If URL is invalid
    +186        """
    +187        return Validators.validate_url(v)
    +
    + + +

    Validate and normalize API URL with optimized regex.

    + +
    Parameters
    + +
      +
    • v: URL to validate
    • +
    + +
    Returns
    + +
    +

    Validated URL

    +
    + +
    Raises
    + +
      +
    • ValueError: If URL is invalid
    • +
    +
    + + +
    +
    + +
    +
    @field_validator('cert_sha256')
    +
    @classmethod
    + + def + validate_cert(cls, v: pydantic.types.SecretStr) -> pydantic.types.SecretStr: + + + +
    + +
    189    @field_validator("cert_sha256")
    +190    @classmethod
    +191    def validate_cert(cls, v: SecretStr) -> SecretStr:
    +192        """Validate certificate fingerprint with constant-time comparison.
    +193
    +194        :param v: Certificate fingerprint
    +195        :return: Validated fingerprint
    +196        :raises ValueError: If fingerprint is invalid
    +197        """
    +198        return Validators.validate_cert_fingerprint(v)
    +
    + + +

    Validate certificate fingerprint with constant-time comparison.

    + +
    Parameters
    + +
      +
    • v: Certificate fingerprint
    • +
    + +
    Returns
    + +
    +

    Validated fingerprint

    +
    + +
    Raises
    + +
      +
    • ValueError: If fingerprint is invalid
    • +
    +
    + + +
    +
    + +
    +
    @field_validator('user_agent')
    +
    @classmethod
    + + def + validate_user_agent(cls, v: str) -> str: + + + +
    + +
    200    @field_validator("user_agent")
    +201    @classmethod
    +202    def validate_user_agent(cls, v: str) -> str:
    +203        """Validate user agent string with efficient control char check.
    +204
    +205        :param v: User agent to validate
    +206        :return: Validated user agent
    +207        :raises ValueError: If user agent is invalid
    +208        """
    +209        v = Validators.validate_string_not_empty(v, "User agent")
    +210
    +211        # Efficient control character check using generator
    +212        if any(ord(c) < 32 for c in v):
    +213            raise ValueError("User agent contains invalid control characters")
    +214
    +215        return v
    +
    + + +

    Validate user agent string with efficient control char check.

    + +
    Parameters
    + +
      +
    • v: User agent to validate
    • +
    + +
    Returns
    + +
    +

    Validated user agent

    +
    + +
    Raises
    + +
      +
    • ValueError: If user agent is invalid
    • +
    +
    + + +
    +
    + +
    +
    @model_validator(mode='after')
    + + def + validate_config(self) -> Self: + + + +
    + +
    217    @model_validator(mode="after")
    +218    def validate_config(self) -> Self:
    +219        """Additional validation after model creation with pattern matching.
    +220
    +221        :return: Validated configuration instance
    +222        """
    +223        # Security warning for HTTP using pattern matching
    +224        match (self.api_url, "localhost" in self.api_url):
    +225            case (url, False) if "http://" in url:
    +226                _log_if_enabled(
    +227                    logging.WARNING,
    +228                    "Using HTTP for non-localhost connection. "
    +229                    "This is insecure and should only be used for testing.",
    +230                )
    +231
    +232        # Optional SSRF protection for private networks (no DNS resolution)
    +233        Validators.validate_url(
    +234            self.api_url,
    +235            allow_private_networks=self.allow_private_networks,
    +236            resolve_dns=self.resolve_dns_for_ssrf,
    +237        )
    +238
    +239        # Circuit breaker timeout adjustment with caching
    +240        if self.enable_circuit_breaker:
    +241            max_request_time = self._get_max_request_time()
    +242
    +243            if self.circuit_call_timeout < max_request_time:
    +244                _log_if_enabled(
    +245                    logging.WARNING,
    +246                    f"Circuit timeout ({self.circuit_call_timeout}s) is less than "
    +247                    f"max request time ({max_request_time}s). "
    +248                    f"Auto-adjusting to {max_request_time}s.",
    +249                )
    +250                object.__setattr__(self, "circuit_call_timeout", max_request_time)
    +251
    +252        return self
    +
    + + +

    Additional validation after model creation with pattern matching.

    + +
    Returns
    + +
    +

    Validated configuration instance

    +
    +
    + + +
    +
    + +
    + get_sanitized_config: dict[str, int | str | bool | float] + + + +
    + +
    288    @cached_property
    +289    def get_sanitized_config(self) -> ConfigDict:
    +290        """Get configuration with sensitive data masked (cached).
    +291
    +292        Safe for logging, debugging, and display.
    +293
    +294        Performance: ~20x speedup with caching for repeated calls
    +295        Memory: Single cached result per instance
    +296
    +297        :return: Sanitized configuration dictionary
    +298        """
    +299        return {
    +300            "api_url": Validators.sanitize_url_for_logging(self.api_url),
    +301            "cert_sha256": "***MASKED***",
    +302            "timeout": self.timeout,
    +303            "retry_attempts": self.retry_attempts,
    +304            "max_connections": self.max_connections,
    +305            "rate_limit": self.rate_limit,
    +306            "user_agent": self.user_agent,
    +307            "enable_circuit_breaker": self.enable_circuit_breaker,
    +308            "enable_logging": self.enable_logging,
    +309            "json_format": self.json_format,
    +310            "allow_private_networks": self.allow_private_networks,
    +311            "circuit_failure_threshold": self.circuit_failure_threshold,
    +312            "circuit_recovery_timeout": self.circuit_recovery_timeout,
    +313            "circuit_success_threshold": self.circuit_success_threshold,
    +314            "circuit_call_timeout": self.circuit_call_timeout,
    +315        }
    +
    + + +

    Get configuration with sensitive data masked (cached).

    + +

    Safe for logging, debugging, and display.

    + +

    Performance: ~20x speedup with caching for repeated calls +Memory: Single cached result per instance

    + +
    Returns
    + +
    +

    Sanitized configuration dictionary

    +
    +
    + + +
    +
    + +
    + + def + model_copy_immutable( self, **overrides: int | str | bool | float) -> OutlineClientConfig: + + + +
    + +
    317    def model_copy_immutable(self, **overrides: ConfigValue) -> OutlineClientConfig:
    +318        """Create immutable copy with overrides (optimized validation).
    +319
    +320        :param overrides: Configuration parameters to override
    +321        :return: Deep copy of configuration with applied updates
    +322        :raises ValueError: If invalid override keys provided
    +323
    +324        Example:
    +325            >>> new_config = config.model_copy_immutable(timeout=20)
    +326        """
    +327        # Optimized: Use frozenset intersection for O(1) validation
    +328        valid_keys = frozenset(ConfigOverrides.__annotations__.keys())
    +329        provided_keys = frozenset(overrides.keys())
    +330        invalid = provided_keys - valid_keys
    +331
    +332        if invalid:
    +333            raise ValueError(
    +334                f"Invalid configuration keys: {', '.join(sorted(invalid))}. "
    +335                f"Valid keys: {', '.join(sorted(valid_keys))}"
    +336            )
    +337
    +338        # Pydantic's model_copy is already optimized
    +339        return cast(  # type: ignore[redundant-cast, unused-ignore]
    +340            OutlineClientConfig, self.model_copy(deep=True, update=overrides)
    +341        )
    +
    + + +

    Create immutable copy with overrides (optimized validation).

    + +
    Parameters
    + +
      +
    • overrides: Configuration parameters to override
    • +
    + +
    Returns
    + +
    +

    Deep copy of configuration with applied updates

    +
    + +
    Raises
    + +
      +
    • ValueError: If invalid override keys provided
    • +
    + +
    Example:
    + +
    +
    +
    >>> new_config = config.model_copy_immutable(timeout=20)
    +
    +
    +
    +
    + + +
    +
    + +
    + circuit_config: CircuitConfig | None + + + +
    + +
    343    @property
    +344    def circuit_config(self) -> CircuitConfig | None:
    +345        """Get circuit breaker configuration if enabled.
    +346
    +347        Returns None if circuit breaker is disabled, otherwise CircuitConfig instance.
    +348        Cached as property for performance.
    +349
    +350        :return: Circuit config or None if disabled
    +351        """
    +352        if not self.enable_circuit_breaker:
    +353            return None
    +354
    +355        return CircuitConfig(
    +356            failure_threshold=self.circuit_failure_threshold,
    +357            recovery_timeout=self.circuit_recovery_timeout,
    +358            success_threshold=self.circuit_success_threshold,
    +359            call_timeout=self.circuit_call_timeout,
    +360        )
    +
    + + +

    Get circuit breaker configuration if enabled.

    + +

    Returns None if circuit breaker is disabled, otherwise CircuitConfig instance. +Cached as property for performance.

    + +
    Returns
    + +
    +

    Circuit config or None if disabled

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_env( cls, env_file: str | pathlib._local.Path | None = None, **overrides: int | str | bool | float) -> OutlineClientConfig: + + + +
    + +
    364    @classmethod
    +365    def from_env(
    +366        cls,
    +367        env_file: str | Path | None = None,
    +368        **overrides: ConfigValue,
    +369    ) -> OutlineClientConfig:
    +370        """Load configuration from environment with overrides.
    +371
    +372        :param env_file: Path to .env file
    +373        :param overrides: Configuration parameters to override
    +374        :return: Configuration instance
    +375        :raises ConfigurationError: If environment configuration is invalid
    +376
    +377        Example:
    +378            >>> config = OutlineClientConfig.from_env(
    +379            ...     env_file=".env.prod",
    +380            ...     timeout=20,
    +381            ...     enable_logging=True
    +382            ... )
    +383        """
    +384        # Fast path: validate overrides early
    +385        valid_keys = frozenset(ConfigOverrides.__annotations__.keys())
    +386        filtered_overrides = cast(
    +387            ConfigOverrides,
    +388            {k: v for k, v in overrides.items() if k in valid_keys},
    +389        )
    +390
    +391        if not env_file:
    +392            return cls(  # type: ignore[call-arg, unused-ignore]
    +393                **filtered_overrides
    +394            )
    +395
    +396        match env_file:
    +397            case str():
    +398                env_path = Path(env_file)
    +399            case Path():
    +400                env_path = env_file
    +401            case _:
    +402                raise TypeError(
    +403                    f"env_file must be str or Path, got {type(env_file).__name__}"
    +404                )
    +405
    +406        if not env_path.exists():
    +407            raise ConfigurationError(
    +408                f"Environment file not found: {env_path}",
    +409                field="env_file",
    +410            )
    +411
    +412        return cls(  # type: ignore[call-arg, unused-ignore]
    +413            _env_file=str(env_path),
    +414            **filtered_overrides,
    +415        )
    +
    + + +

    Load configuration from environment with overrides.

    + +
    Parameters
    + +
      +
    • env_file: Path to .env file
    • +
    • overrides: Configuration parameters to override
    • +
    + +
    Returns
    + +
    +

    Configuration instance

    +
    + +
    Raises
    + +
      +
    • ConfigurationError: If environment configuration is invalid
    • +
    + +
    Example:
    + +
    +
    +
    >>> config = OutlineClientConfig.from_env(
    +...     env_file=".env.prod",
    +...     timeout=20,
    +...     enable_logging=True
    +... )
    +
    +
    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + create_minimal( cls, api_url: str, cert_sha256: str | pydantic.types.SecretStr, **overrides: int | str | bool | float) -> OutlineClientConfig: + + + +
    + +
    417    @classmethod
    +418    def create_minimal(
    +419        cls,
    +420        api_url: str,
    +421        cert_sha256: str | SecretStr,
    +422        **overrides: ConfigValue,
    +423    ) -> OutlineClientConfig:
    +424        """Create minimal configuration (optimized validation).
    +425
    +426        :param api_url: API URL
    +427        :param cert_sha256: Certificate fingerprint
    +428        :param overrides: Optional configuration parameters
    +429        :return: Configuration instance
    +430        :raises TypeError: If cert_sha256 is not str or SecretStr
    +431
    +432        Example:
    +433            >>> config = OutlineClientConfig.create_minimal(
    +434            ...     api_url="https://server.com/path",
    +435            ...     cert_sha256="a" * 64,
    +436            ...     timeout=20
    +437            ... )
    +438        """
    +439        match cert_sha256:
    +440            case str():
    +441                cert = SecretStr(cert_sha256)
    +442            case SecretStr():
    +443                cert = cert_sha256
    +444            case _:
    +445                raise TypeError(
    +446                    f"cert_sha256 must be str or SecretStr, "
    +447                    f"got {type(cert_sha256).__name__}"
    +448                )
    +449
    +450        valid_keys = frozenset(ConfigOverrides.__annotations__.keys())
    +451        filtered_overrides = cast(
    +452            ConfigOverrides,
    +453            {k: v for k, v in overrides.items() if k in valid_keys},
    +454        )
    +455
    +456        return cls(
    +457            api_url=api_url,
    +458            cert_sha256=cert,
    +459            **filtered_overrides,
    +460        )
    +
    + + +

    Create minimal configuration (optimized validation).

    + +
    Parameters
    + +
      +
    • api_url: API URL
    • +
    • cert_sha256: Certificate fingerprint
    • +
    • overrides: Optional configuration parameters
    • +
    + +
    Returns
    + +
    +

    Configuration instance

    +
    + +
    Raises
    + +
      +
    • TypeError: If cert_sha256 is not str or SecretStr
    • +
    + +
    Example:
    + +
    +
    +
    >>> config = OutlineClientConfig.create_minimal(
    +...     api_url="https://server.com/path",
    +...     cert_sha256="a" * 64,
    +...     timeout=20
    +... )
    +
    +
    +
    +
    + + +
    +
    +
    + +
    + + class + OutlineConnectionError(pyoutlineapi.OutlineError): + + + +
    + +
    403class OutlineConnectionError(OutlineError):
    +404    """Network connection failure.
    +405
    +406    Attributes:
    +407        host: Host that failed
    +408        port: Port that failed
    +409
    +410    Example:
    +411        >>> error = OutlineConnectionError(
    +412        ...     "Connection refused", host="server.com", port=443
    +413        ... )
    +414        >>> error.is_retryable  # True
    +415    """
    +416
    +417    __slots__ = ("host", "port")
    +418
    +419    _is_retryable: ClassVar[bool] = True
    +420    _default_retry_delay: ClassVar[float] = 2.0
    +421
    +422    def __init__(
    +423        self,
    +424        message: str,
    +425        *,
    +426        host: str | None = None,
    +427        port: int | None = None,
    +428    ) -> None:
    +429        """Initialize connection error.
    +430
    +431        Args:
    +432            message: Error message
    +433            host: Host that failed
    +434            port: Port that failed
    +435        """
    +436        safe_details: dict[str, Any] | None = None
    +437        if host or port is not None:
    +438            safe_details = {}
    +439            if host:
    +440                safe_details["host"] = host
    +441            if port is not None:
    +442                safe_details["port"] = port
    +443
    +444        super().__init__(message, safe_details=safe_details)
    +445
    +446        self.host = host
    +447        self.port = port
    +
    + + +

    Network connection failure.

    + +
    Attributes:
    + +
      +
    • host: Host that failed
    • +
    • port: Port that failed
    • +
    + +
    Example:
    + +
    +
    +
    >>> error = OutlineConnectionError(
    +...     "Connection refused", host="server.com", port=443
    +... )
    +>>> error.is_retryable  # True
    +
    +
    +
    +
    + + +
    + +
    + + OutlineConnectionError(message: str, *, host: str | None = None, port: int | None = None) + + + +
    + +
    422    def __init__(
    +423        self,
    +424        message: str,
    +425        *,
    +426        host: str | None = None,
    +427        port: int | None = None,
    +428    ) -> None:
    +429        """Initialize connection error.
    +430
    +431        Args:
    +432            message: Error message
    +433            host: Host that failed
    +434            port: Port that failed
    +435        """
    +436        safe_details: dict[str, Any] | None = None
    +437        if host or port is not None:
    +438            safe_details = {}
    +439            if host:
    +440                safe_details["host"] = host
    +441            if port is not None:
    +442                safe_details["port"] = port
    +443
    +444        super().__init__(message, safe_details=safe_details)
    +445
    +446        self.host = host
    +447        self.port = port
    +
    + + +

    Initialize connection error.

    + +
    Arguments:
    + +
      +
    • message: Error message
    • +
    • host: Host that failed
    • +
    • port: Port that failed
    • +
    +
    + + +
    +
    +
    + host + + +
    + + + + +
    +
    +
    + port + + +
    + + + + +
    +
    +
    + +
    + + class + OutlineError(builtins.Exception): + + + +
    + +
     42class OutlineError(Exception):
    + 43    """Base exception for all PyOutlineAPI errors.
    + 44
    + 45    Provides rich error context, retry guidance, and safe serialization
    + 46    with automatic credential sanitization.
    + 47
    + 48    Attributes:
    + 49        is_retryable: Whether this error type should be retried
    + 50        default_retry_delay: Suggested delay before retry in seconds
    + 51
    + 52    Example:
    + 53        >>> try:
    + 54        ...     raise OutlineError("Connection failed", details={"host": "server"})
    + 55        ... except OutlineError as e:
    + 56        ...     print(e.safe_details)  # {'host': 'server'}
    + 57    """
    + 58
    + 59    __slots__ = ("_cached_str", "_details", "_message", "_safe_details")
    + 60
    + 61    _is_retryable: ClassVar[bool] = False
    + 62    _default_retry_delay: ClassVar[float] = 1.0
    + 63
    + 64    def __init__(
    + 65        self,
    + 66        message: object,
    + 67        *,
    + 68        details: dict[str, Any] | None = None,
    + 69        safe_details: dict[str, Any] | None = None,
    + 70    ) -> None:
    + 71        """Initialize exception with automatic credential sanitization.
    + 72
    + 73        Args:
    + 74            message: Error message (automatically sanitized)
    + 75            details: Internal details (may contain sensitive data)
    + 76            safe_details: Safe details for logging/display
    + 77
    + 78        Raises:
    + 79            ValueError: If message exceeds maximum length after sanitization
    + 80        """
    + 81        # Validate and sanitize message
    + 82        if not isinstance(message, str):
    + 83            message = str(message)
    + 84
    + 85        # Sanitize credentials from message
    + 86        sanitized_message = CredentialSanitizer.sanitize(message)
    + 87
    + 88        # Truncate if too long
    + 89        if len(sanitized_message) > _MAX_MESSAGE_LENGTH:
    + 90            sanitized_message = sanitized_message[:_MAX_MESSAGE_LENGTH] + "..."
    + 91
    + 92        self._message = sanitized_message
    + 93        super().__init__(sanitized_message)
    + 94
    + 95        self._details: dict[str, Any] | MappingProxyType[str, Any] = (
    + 96            dict(details) if details else _EMPTY_DICT
    + 97        )
    + 98        self._safe_details: dict[str, Any] | MappingProxyType[str, Any] = (
    + 99            dict(safe_details) if safe_details else _EMPTY_DICT
    +100        )
    +101
    +102        self._cached_str: str | None = None
    +103
    +104    @property
    +105    def details(self) -> dict[str, Any]:
    +106        """Get internal error details (may contain sensitive data).
    +107
    +108        Warning:
    +109            Use with caution - may contain credentials or sensitive information.
    +110            For logging, use ``safe_details`` instead.
    +111
    +112        Returns:
    +113            Copy of internal details dictionary
    +114        """
    +115        if self._details is _EMPTY_DICT:
    +116            return {}
    +117        return self._details.copy()
    +118
    +119    @property
    +120    def safe_details(self) -> dict[str, Any]:
    +121        """Get sanitized error details safe for logging.
    +122
    +123        Returns:
    +124            Copy of safe details dictionary
    +125        """
    +126        if self._safe_details is _EMPTY_DICT:
    +127            return {}
    +128        return self._safe_details.copy()
    +129
    +130    def _format_details(self) -> str:
    +131        """Format safe details for string representation.
    +132
    +133        :return: Formatted details string
    +134        """
    +135        if not self._safe_details:
    +136            return ""
    +137
    +138        parts = [f"{k}={v}" for k, v in self._safe_details.items()]
    +139        return f" ({', '.join(parts)})"
    +140
    +141    def __str__(self) -> str:
    +142        """Safe string representation using safe_details.
    +143
    +144        Cached for performance on repeated access.
    +145
    +146        :return: String representation
    +147        """
    +148        if self._cached_str is None:
    +149            self._cached_str = f"{self._message}{self._format_details()}"
    +150        return self._cached_str
    +151
    +152    def __repr__(self) -> str:
    +153        """Safe repr without sensitive data.
    +154
    +155        :return: String representation
    +156        """
    +157        class_name = self.__class__.__name__
    +158        return f"{class_name}({self._message!r})"
    +159
    +160    @property
    +161    def is_retryable(self) -> bool:
    +162        """Return whether this error type should be retried."""
    +163        return self._is_retryable
    +164
    +165    @property
    +166    def default_retry_delay(self) -> float:
    +167        """Return suggested delay before retry in seconds."""
    +168        return self._default_retry_delay
    +
    + + +

    Base exception for all PyOutlineAPI errors.

    + +

    Provides rich error context, retry guidance, and safe serialization +with automatic credential sanitization.

    + +
    Attributes:
    + +
      +
    • is_retryable: Whether this error type should be retried
    • +
    • default_retry_delay: Suggested delay before retry in seconds
    • +
    + +
    Example:
    + +
    +
    +
    >>> try:
    +...     raise OutlineError("Connection failed", details={"host": "server"})
    +... except OutlineError as e:
    +...     print(e.safe_details)  # {'host': 'server'}
    +
    +
    +
    +
    + + +
    + +
    + + OutlineError( message: object, *, details: dict[str, typing.Any] | None = None, safe_details: dict[str, typing.Any] | None = None) + + + +
    + +
     64    def __init__(
    + 65        self,
    + 66        message: object,
    + 67        *,
    + 68        details: dict[str, Any] | None = None,
    + 69        safe_details: dict[str, Any] | None = None,
    + 70    ) -> None:
    + 71        """Initialize exception with automatic credential sanitization.
    + 72
    + 73        Args:
    + 74            message: Error message (automatically sanitized)
    + 75            details: Internal details (may contain sensitive data)
    + 76            safe_details: Safe details for logging/display
    + 77
    + 78        Raises:
    + 79            ValueError: If message exceeds maximum length after sanitization
    + 80        """
    + 81        # Validate and sanitize message
    + 82        if not isinstance(message, str):
    + 83            message = str(message)
    + 84
    + 85        # Sanitize credentials from message
    + 86        sanitized_message = CredentialSanitizer.sanitize(message)
    + 87
    + 88        # Truncate if too long
    + 89        if len(sanitized_message) > _MAX_MESSAGE_LENGTH:
    + 90            sanitized_message = sanitized_message[:_MAX_MESSAGE_LENGTH] + "..."
    + 91
    + 92        self._message = sanitized_message
    + 93        super().__init__(sanitized_message)
    + 94
    + 95        self._details: dict[str, Any] | MappingProxyType[str, Any] = (
    + 96            dict(details) if details else _EMPTY_DICT
    + 97        )
    + 98        self._safe_details: dict[str, Any] | MappingProxyType[str, Any] = (
    + 99            dict(safe_details) if safe_details else _EMPTY_DICT
    +100        )
    +101
    +102        self._cached_str: str | None = None
    +
    + + +

    Initialize exception with automatic credential sanitization.

    + +
    Arguments:
    + +
      +
    • message: Error message (automatically sanitized)
    • +
    • details: Internal details (may contain sensitive data)
    • +
    • safe_details: Safe details for logging/display
    • +
    + +
    Raises:
    + +
      +
    • ValueError: If message exceeds maximum length after sanitization
    • +
    +
    + + +
    +
    + +
    + details: dict[str, typing.Any] + + + +
    + +
    104    @property
    +105    def details(self) -> dict[str, Any]:
    +106        """Get internal error details (may contain sensitive data).
    +107
    +108        Warning:
    +109            Use with caution - may contain credentials or sensitive information.
    +110            For logging, use ``safe_details`` instead.
    +111
    +112        Returns:
    +113            Copy of internal details dictionary
    +114        """
    +115        if self._details is _EMPTY_DICT:
    +116            return {}
    +117        return self._details.copy()
    +
    + + +

    Get internal error details (may contain sensitive data).

    + +
    Warning:
    + +
    +

    Use with caution - may contain credentials or sensitive information. + For logging, use safe_details instead.

    +
    + +
    Returns:
    + +
    +

    Copy of internal details dictionary

    +
    +
    + + +
    +
    + +
    + safe_details: dict[str, typing.Any] + + + +
    + +
    119    @property
    +120    def safe_details(self) -> dict[str, Any]:
    +121        """Get sanitized error details safe for logging.
    +122
    +123        Returns:
    +124            Copy of safe details dictionary
    +125        """
    +126        if self._safe_details is _EMPTY_DICT:
    +127            return {}
    +128        return self._safe_details.copy()
    +
    + + +

    Get sanitized error details safe for logging.

    + +
    Returns:
    + +
    +

    Copy of safe details dictionary

    +
    +
    + + +
    +
    + +
    + is_retryable: bool + + + +
    + +
    160    @property
    +161    def is_retryable(self) -> bool:
    +162        """Return whether this error type should be retried."""
    +163        return self._is_retryable
    +
    + + +

    Return whether this error type should be retried.

    +
    + + +
    +
    + +
    + default_retry_delay: float + + + +
    + +
    165    @property
    +166    def default_retry_delay(self) -> float:
    +167        """Return suggested delay before retry in seconds."""
    +168        return self._default_retry_delay
    +
    + + +

    Return suggested delay before retry in seconds.

    +
    + + +
    +
    +
    + +
    + + class + OutlineTimeoutError(pyoutlineapi.OutlineError): + + + +
    + +
    450class OutlineTimeoutError(OutlineError):
    +451    """Operation timeout.
    +452
    +453    Attributes:
    +454        timeout: Timeout value in seconds
    +455        operation: Operation that timed out
    +456
    +457    Example:
    +458        >>> error = OutlineTimeoutError(
    +459        ...     "Request timeout", timeout=30.0, operation="get_server_info"
    +460        ... )
    +461        >>> error.is_retryable  # True
    +462    """
    +463
    +464    __slots__ = ("operation", "timeout")
    +465
    +466    _is_retryable: ClassVar[bool] = True
    +467    _default_retry_delay: ClassVar[float] = 2.0
    +468
    +469    def __init__(
    +470        self,
    +471        message: str,
    +472        *,
    +473        timeout: float | None = None,
    +474        operation: str | None = None,
    +475    ) -> None:
    +476        """Initialize timeout error.
    +477
    +478        Args:
    +479            message: Error message
    +480            timeout: Timeout value in seconds
    +481            operation: Operation that timed out
    +482        """
    +483        safe_details: dict[str, Any] | None = None
    +484        if timeout is not None or operation:
    +485            safe_details = {}
    +486            if timeout is not None:
    +487                safe_details["timeout"] = round(timeout, 2)
    +488            if operation:
    +489                safe_details["operation"] = operation
    +490
    +491        super().__init__(message, safe_details=safe_details)
    +492
    +493        self.timeout = timeout
    +494        self.operation = operation
    +
    + + +

    Operation timeout.

    + +
    Attributes:
    + +
      +
    • timeout: Timeout value in seconds
    • +
    • operation: Operation that timed out
    • +
    + +
    Example:
    + +
    +
    +
    >>> error = OutlineTimeoutError(
    +...     "Request timeout", timeout=30.0, operation="get_server_info"
    +... )
    +>>> error.is_retryable  # True
    +
    +
    +
    +
    + + +
    + +
    + + OutlineTimeoutError( message: str, *, timeout: float | None = None, operation: str | None = None) + + + +
    + +
    469    def __init__(
    +470        self,
    +471        message: str,
    +472        *,
    +473        timeout: float | None = None,
    +474        operation: str | None = None,
    +475    ) -> None:
    +476        """Initialize timeout error.
    +477
    +478        Args:
    +479            message: Error message
    +480            timeout: Timeout value in seconds
    +481            operation: Operation that timed out
    +482        """
    +483        safe_details: dict[str, Any] | None = None
    +484        if timeout is not None or operation:
    +485            safe_details = {}
    +486            if timeout is not None:
    +487                safe_details["timeout"] = round(timeout, 2)
    +488            if operation:
    +489                safe_details["operation"] = operation
    +490
    +491        super().__init__(message, safe_details=safe_details)
    +492
    +493        self.timeout = timeout
    +494        self.operation = operation
    +
    + + +

    Initialize timeout error.

    + +
    Arguments:
    + +
      +
    • message: Error message
    • +
    • timeout: Timeout value in seconds
    • +
    • operation: Operation that timed out
    • +
    +
    + + +
    +
    +
    + timeout + + +
    + + + + +
    +
    +
    + operation + + +
    + + + + +
    +
    +
    + +
    + + class + PeakDeviceCount(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    416class PeakDeviceCount(BaseValidatedModel):
    +417    """Peak device count with timestamp.
    +418
    +419    SCHEMA: Based on experimental metrics connection peakDeviceCount object
    +420    """
    +421
    +422    data: int
    +423    timestamp: TimestampSec
    +
    + + +

    Peak device count with timestamp.

    + +

    SCHEMA: Based on experimental metrics connection peakDeviceCount object

    +
    + + +
    +
    + data: int = +PydanticUndefined + + +
    + + + + +
    +
    +
    + timestamp: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in seconds', metadata=[Ge(ge=0)])] = +PydanticUndefined + + +
    + + +

    Unix timestamp in seconds

    +
    + + +
    +
    +
    + +
    + + class + PortRequest(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    515class PortRequest(BaseValidatedModel):
    +516    """Request model for setting default port.
    +517
    +518    SCHEMA: Based on PUT /server/port-for-new-access-keys request body
    +519    """
    +520
    +521    port: Port
    +
    + + +

    Request model for setting default port.

    + +

    SCHEMA: Based on PUT /server/port-for-new-access-keys request body

    +
    + + +
    +
    + port: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])] = +PydanticUndefined + + +
    + + +

    Port number (1-65535)

    +
    + + +
    +
    +
    + +
    + + class + ProductionConfig(pyoutlineapi.OutlineClientConfig): + + + +
    + +
    484class ProductionConfig(OutlineClientConfig):
    +485    """Production configuration with strict security.
    +486
    +487    Enforces HTTPS and enables all safety features:
    +488    - Circuit breaker enabled
    +489    - Logging disabled (performance)
    +490    - HTTPS enforcement
    +491    - Strict validation
    +492    """
    +493
    +494    model_config = SettingsConfigDict(
    +495        env_prefix=_PROD_ENV_PREFIX,
    +496        env_file=".env.prod",
    +497        case_sensitive=False,
    +498        extra="forbid",
    +499    )
    +500
    +501    enable_circuit_breaker: bool = True
    +502    enable_logging: bool = False
    +503
    +504    @model_validator(mode="after")
    +505    def enforce_security(self) -> Self:
    +506        """Enforce production security with optimized checks.
    +507
    +508        :return: Validated configuration
    +509        :raises ConfigurationError: If HTTP is used in production
    +510        """
    +511        match self.api_url:
    +512            case url if "http://" in url:
    +513                raise ConfigurationError(
    +514                    "Production environment must use HTTPS",
    +515                    field="api_url",
    +516                    security_issue=True,
    +517                )
    +518
    +519        if not self.enable_circuit_breaker:
    +520            _log_if_enabled(
    +521                logging.WARNING,
    +522                "Circuit breaker disabled in production. Not recommended.",
    +523            )
    +524
    +525        return self
    +
    + + +

    Production configuration with strict security.

    + +

    Enforces HTTPS and enables all safety features:

    + +
      +
    • Circuit breaker enabled
    • +
    • Logging disabled (performance)
    • +
    • HTTPS enforcement
    • +
    • Strict validation
    • +
    +
    + + +
    +
    + enable_circuit_breaker: bool = +True + + +
    + + + + +
    +
    +
    + enable_logging: bool = +False + + +
    + + + + +
    +
    + +
    +
    @model_validator(mode='after')
    + + def + enforce_security(self) -> Self: + + + +
    + +
    504    @model_validator(mode="after")
    +505    def enforce_security(self) -> Self:
    +506        """Enforce production security with optimized checks.
    +507
    +508        :return: Validated configuration
    +509        :raises ConfigurationError: If HTTP is used in production
    +510        """
    +511        match self.api_url:
    +512            case url if "http://" in url:
    +513                raise ConfigurationError(
    +514                    "Production environment must use HTTPS",
    +515                    field="api_url",
    +516                    security_issue=True,
    +517                )
    +518
    +519        if not self.enable_circuit_breaker:
    +520            _log_if_enabled(
    +521                logging.WARNING,
    +522                "Circuit breaker disabled in production. Not recommended.",
    +523            )
    +524
    +525        return self
    +
    + + +

    Enforce production security with optimized checks.

    + +
    Returns
    + +
    +

    Validated configuration

    +
    + +
    Raises
    + +
      +
    • ConfigurationError: If HTTP is used in production
    • +
    +
    + + +
    +
    +
    +
    + QueryParams = +dict[str, str | int | float | bool] + + +
    + + + + +
    +
    +
    + ResponseData = + + dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]] + + +
    + + + + +
    +
    + +
    + + class + ResponseParser: + + + +
    + +
     38class ResponseParser:
    + 39    """High-performance utility class for parsing and validating API responses."""
    + 40
    + 41    __slots__ = ()  # Stateless class - zero memory overhead
    + 42
    + 43    @staticmethod
    + 44    @overload
    + 45    def parse(
    + 46        data: dict[str, JsonValue],
    + 47        model: type[T],
    + 48        *,
    + 49        as_json: Literal[True] = True,
    + 50    ) -> JsonDict: ...
    + 51
    + 52    @staticmethod
    + 53    @overload
    + 54    def parse(
    + 55        data: dict[str, JsonValue],
    + 56        model: type[T],
    + 57        *,
    + 58        as_json: Literal[False] = False,
    + 59    ) -> T: ...
    + 60
    + 61    @staticmethod
    + 62    @overload
    + 63    def parse(
    + 64        data: dict[str, JsonValue],
    + 65        model: type[T],
    + 66        *,
    + 67        as_json: bool,
    + 68    ) -> T | JsonDict: ...
    + 69
    + 70    @staticmethod
    + 71    def parse(
    + 72        data: dict[str, JsonValue],
    + 73        model: type[T],
    + 74        *,
    + 75        as_json: bool = False,
    + 76    ) -> T | JsonDict:
    + 77        """Parse and validate response data with comprehensive error handling.
    + 78
    + 79        Type-safe overloads ensure correct return type based on as_json parameter.
    + 80
    + 81        :param data: Raw response data from API
    + 82        :param model: Pydantic model class for validation
    + 83        :param as_json: Return raw JSON dict instead of model instance
    + 84        :return: Validated model instance or JSON dict
    + 85        :raises ValidationError: If validation fails with detailed error info
    + 86
    + 87        Example:
    + 88            >>> data = {"name": "test", "id": 123}
    + 89            >>> # Type-safe: returns MyModel instance
    + 90            >>> result = ResponseParser.parse(data, MyModel, as_json=False)
    + 91            >>> # Type-safe: returns dict
    + 92            >>> json_result = ResponseParser.parse(data, MyModel, as_json=True)
    + 93        """
    + 94        if not isinstance(data, dict):
    + 95            raise OutlineValidationError(
    + 96                f"Expected dict, got {type(data).__name__}",
    + 97                model=model.__name__,
    + 98            )
    + 99
    +100        if not data and logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG):
    +101            logger.debug("Parsing empty dict for model %s", model.__name__)
    +102
    +103        try:
    +104            validated = model.model_validate(data)
    +105
    +106            if as_json:
    +107                return cast(  # type: ignore[redundant-cast, unused-ignore]
    +108                    JsonDict, validated.model_dump(by_alias=True)
    +109                )
    +110            return cast(T, validated)  # type: ignore[redundant-cast, unused-ignore]
    +111
    +112        except ValidationError as e:
    +113            errors = e.errors()
    +114
    +115            if not errors:
    +116                raise OutlineValidationError(
    +117                    "Validation failed with no error details",
    +118                    model=model.__name__,
    +119                ) from e
    +120
    +121            first_error = errors[0]
    +122            field = ".".join(str(loc) for loc in first_error.get("loc", ()))
    +123            message = first_error.get("msg", "Validation failed")
    +124
    +125            error_count = len(errors)
    +126            if error_count > 1:
    +127                if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING):
    +128                    logger.warning(
    +129                        "Multiple validation errors for %s: %d error(s)",
    +130                        model.__name__,
    +131                        error_count,
    +132                    )
    +133
    +134                if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG):
    +135                    logger.debug("Validation error details:")
    +136                    logged_count = min(error_count, _MAX_LOGGED_ERRORS)
    +137
    +138                    for i, error in enumerate(errors[:logged_count], 1):
    +139                        error_field = ".".join(str(loc) for loc in error.get("loc", ()))
    +140                        error_msg = error.get("msg", "Unknown error")
    +141                        logger.debug("  %d. %s: %s", i, error_field, error_msg)
    +142
    +143                    if error_count > _MAX_LOGGED_ERRORS:
    +144                        remaining = error_count - _MAX_LOGGED_ERRORS
    +145                        logger.debug("  ... and %d more error(s)", remaining)
    +146
    +147            raise OutlineValidationError(
    +148                message,
    +149                field=field,
    +150                model=model.__name__,
    +151            ) from e
    +152
    +153        except Exception as e:
    +154            # Catch any other unexpected errors during validation
    +155            if logger.isEnabledFor(Constants.LOG_LEVEL_ERROR):
    +156                logger.error(
    +157                    "Unexpected error during validation: %s",
    +158                    e,
    +159                    exc_info=True,
    +160                )
    +161            raise OutlineValidationError(
    +162                f"Unexpected error during validation: {e}",
    +163                model=model.__name__,
    +164            ) from e
    +165
    +166    @staticmethod
    +167    def parse_simple(data: Mapping[str, JsonValue] | object) -> bool:
    +168        """Parse simple success/error responses efficiently.
    +169
    +170        Handles various response formats with minimal overhead:
    +171        - {"success": true/false}
    +172        - {"error": "..."}  → False
    +173        - {"message": "..."}  → False
    +174        - Empty dict  → True (assumed success)
    +175
    +176        :param data: Response data
    +177        :return: True if successful, False otherwise
    +178
    +179        Example:
    +180            >>> ResponseParser.parse_simple({"success": True})
    +181            True
    +182            >>> ResponseParser.parse_simple({"error": "Something failed"})
    +183            False
    +184            >>> ResponseParser.parse_simple({})
    +185            True
    +186        """
    +187        if not isinstance(data, dict):
    +188            if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING):
    +189                logger.warning(
    +190                    "Expected dict in parse_simple, got %s",
    +191                    type(data).__name__,
    +192                )
    +193            return False
    +194
    +195        if "success" in data:
    +196            success = data["success"]
    +197            if not isinstance(success, bool):
    +198                if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING):
    +199                    logger.warning(
    +200                        "success field is not bool: %s, coercing to bool",
    +201                        type(success).__name__,
    +202                    )
    +203                return bool(success)
    +204            return success
    +205
    +206        return "error" not in data and "message" not in data
    +207
    +208    @staticmethod
    +209    def validate_response_structure(
    +210        data: Mapping[str, JsonValue] | object,
    +211        required_fields: Sequence[str] | None = None,
    +212    ) -> bool:
    +213        """Validate response structure without full parsing.
    +214
    +215        Lightweight validation before expensive Pydantic validation.
    +216        Useful for early rejection of malformed responses.
    +217
    +218        :param data: Response data to validate
    +219        :param required_fields: Sequence of required field names
    +220        :return: True if structure is valid
    +221
    +222        Example:
    +223            >>> data = {"id": 1, "name": "test"}
    +224            >>> ResponseParser.validate_response_structure(data, ["id", "name"])
    +225            True
    +226            >>> ResponseParser.validate_response_structure(data, ["id", "missing"])
    +227            False
    +228        """
    +229        if not isinstance(data, dict):
    +230            return False
    +231
    +232        if not data and not required_fields:
    +233            return True
    +234
    +235        if not required_fields:
    +236            return True
    +237
    +238        return all(field in data for field in required_fields)
    +239
    +240    @staticmethod
    +241    def extract_error_message(data: Mapping[str, JsonValue] | object) -> str | None:
    +242        """Extract error message from response data efficiently.
    +243
    +244        Checks common error field names in order of preference.
    +245        Uses pre-computed tuple for fast iteration.
    +246
    +247        :param data: Response data
    +248        :return: Error message or None if not found
    +249
    +250        Example:
    +251            >>> ResponseParser.extract_error_message({"error": "Not found"})
    +252            'Not found'
    +253            >>> ResponseParser.extract_error_message({"message": "Failed"})
    +254            'Failed'
    +255            >>> ResponseParser.extract_error_message({"success": True})
    +256            None
    +257        """
    +258        if not isinstance(data, dict):
    +259            return None
    +260
    +261        for field in _ERROR_FIELDS:
    +262            if field in data:
    +263                value = data[field]
    +264                # Fast path: already a string
    +265                if isinstance(value, str):
    +266                    return value
    +267                # Convert non-string to string (None → None)
    +268                return str(value) if value is not None else None
    +269
    +270        return None
    +271
    +272    @staticmethod
    +273    def is_error_response(data: Mapping[str, object] | object) -> bool:
    +274        """Check if response indicates an error efficiently.
    +275
    +276        Fast boolean check for error indicators in response.
    +277
    +278        :param data: Response data
    +279        :return: True if response indicates an error
    +280
    +281        Example:
    +282            >>> ResponseParser.is_error_response({"error": "Failed"})
    +283            True
    +284            >>> ResponseParser.is_error_response({"success": False})
    +285            True
    +286            >>> ResponseParser.is_error_response({"success": True})
    +287            False
    +288            >>> ResponseParser.is_error_response({})
    +289            False
    +290        """
    +291        if not isinstance(data, dict):
    +292            return False
    +293
    +294        if "error" in data or "error_message" in data:
    +295            return True
    +296
    +297        if "success" in data:
    +298            success = data["success"]
    +299            return success is False
    +300
    +301        # No error indicators found
    +302        return False
    +
    + + +

    High-performance utility class for parsing and validating API responses.

    +
    + + +
    + +
    +
    @staticmethod
    + + def + parse( data: dict[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]], model: type[~T], *, as_json: bool = False) -> Union[~T, dict[str, Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]]]: + + + +
    + +
     70    @staticmethod
    + 71    def parse(
    + 72        data: dict[str, JsonValue],
    + 73        model: type[T],
    + 74        *,
    + 75        as_json: bool = False,
    + 76    ) -> T | JsonDict:
    + 77        """Parse and validate response data with comprehensive error handling.
    + 78
    + 79        Type-safe overloads ensure correct return type based on as_json parameter.
    + 80
    + 81        :param data: Raw response data from API
    + 82        :param model: Pydantic model class for validation
    + 83        :param as_json: Return raw JSON dict instead of model instance
    + 84        :return: Validated model instance or JSON dict
    + 85        :raises ValidationError: If validation fails with detailed error info
    + 86
    + 87        Example:
    + 88            >>> data = {"name": "test", "id": 123}
    + 89            >>> # Type-safe: returns MyModel instance
    + 90            >>> result = ResponseParser.parse(data, MyModel, as_json=False)
    + 91            >>> # Type-safe: returns dict
    + 92            >>> json_result = ResponseParser.parse(data, MyModel, as_json=True)
    + 93        """
    + 94        if not isinstance(data, dict):
    + 95            raise OutlineValidationError(
    + 96                f"Expected dict, got {type(data).__name__}",
    + 97                model=model.__name__,
    + 98            )
    + 99
    +100        if not data and logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG):
    +101            logger.debug("Parsing empty dict for model %s", model.__name__)
    +102
    +103        try:
    +104            validated = model.model_validate(data)
    +105
    +106            if as_json:
    +107                return cast(  # type: ignore[redundant-cast, unused-ignore]
    +108                    JsonDict, validated.model_dump(by_alias=True)
    +109                )
    +110            return cast(T, validated)  # type: ignore[redundant-cast, unused-ignore]
    +111
    +112        except ValidationError as e:
    +113            errors = e.errors()
    +114
    +115            if not errors:
    +116                raise OutlineValidationError(
    +117                    "Validation failed with no error details",
    +118                    model=model.__name__,
    +119                ) from e
    +120
    +121            first_error = errors[0]
    +122            field = ".".join(str(loc) for loc in first_error.get("loc", ()))
    +123            message = first_error.get("msg", "Validation failed")
    +124
    +125            error_count = len(errors)
    +126            if error_count > 1:
    +127                if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING):
    +128                    logger.warning(
    +129                        "Multiple validation errors for %s: %d error(s)",
    +130                        model.__name__,
    +131                        error_count,
    +132                    )
    +133
    +134                if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG):
    +135                    logger.debug("Validation error details:")
    +136                    logged_count = min(error_count, _MAX_LOGGED_ERRORS)
    +137
    +138                    for i, error in enumerate(errors[:logged_count], 1):
    +139                        error_field = ".".join(str(loc) for loc in error.get("loc", ()))
    +140                        error_msg = error.get("msg", "Unknown error")
    +141                        logger.debug("  %d. %s: %s", i, error_field, error_msg)
    +142
    +143                    if error_count > _MAX_LOGGED_ERRORS:
    +144                        remaining = error_count - _MAX_LOGGED_ERRORS
    +145                        logger.debug("  ... and %d more error(s)", remaining)
    +146
    +147            raise OutlineValidationError(
    +148                message,
    +149                field=field,
    +150                model=model.__name__,
    +151            ) from e
    +152
    +153        except Exception as e:
    +154            # Catch any other unexpected errors during validation
    +155            if logger.isEnabledFor(Constants.LOG_LEVEL_ERROR):
    +156                logger.error(
    +157                    "Unexpected error during validation: %s",
    +158                    e,
    +159                    exc_info=True,
    +160                )
    +161            raise OutlineValidationError(
    +162                f"Unexpected error during validation: {e}",
    +163                model=model.__name__,
    +164            ) from e
    +
    + + +

    Parse and validate response data with comprehensive error handling.

    + +

    Type-safe overloads ensure correct return type based on as_json parameter.

    + +
    Parameters
    + +
      +
    • data: Raw response data from API
    • +
    • model: Pydantic model class for validation
    • +
    • as_json: Return raw JSON dict instead of model instance
    • +
    + +
    Returns
    + +
    +

    Validated model instance or JSON dict

    +
    + +
    Raises
    + +
      +
    • ValidationError: If validation fails with detailed error info
    • +
    + +
    Example:
    + +
    +
    +
    >>> data = {"name": "test", "id": 123}
    +>>> # Type-safe: returns MyModel instance
    +>>> result = ResponseParser.parse(data, MyModel, as_json=False)
    +>>> # Type-safe: returns dict
    +>>> json_result = ResponseParser.parse(data, MyModel, as_json=True)
    +
    +
    +
    +
    + + +
    +
    + +
    +
    @staticmethod
    + + def + parse_simple( data: Mapping[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]] | object) -> bool: + + + +
    + +
    166    @staticmethod
    +167    def parse_simple(data: Mapping[str, JsonValue] | object) -> bool:
    +168        """Parse simple success/error responses efficiently.
    +169
    +170        Handles various response formats with minimal overhead:
    +171        - {"success": true/false}
    +172        - {"error": "..."}  → False
    +173        - {"message": "..."}  → False
    +174        - Empty dict  → True (assumed success)
    +175
    +176        :param data: Response data
    +177        :return: True if successful, False otherwise
    +178
    +179        Example:
    +180            >>> ResponseParser.parse_simple({"success": True})
    +181            True
    +182            >>> ResponseParser.parse_simple({"error": "Something failed"})
    +183            False
    +184            >>> ResponseParser.parse_simple({})
    +185            True
    +186        """
    +187        if not isinstance(data, dict):
    +188            if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING):
    +189                logger.warning(
    +190                    "Expected dict in parse_simple, got %s",
    +191                    type(data).__name__,
    +192                )
    +193            return False
    +194
    +195        if "success" in data:
    +196            success = data["success"]
    +197            if not isinstance(success, bool):
    +198                if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING):
    +199                    logger.warning(
    +200                        "success field is not bool: %s, coercing to bool",
    +201                        type(success).__name__,
    +202                    )
    +203                return bool(success)
    +204            return success
    +205
    +206        return "error" not in data and "message" not in data
    +
    + + +

    Parse simple success/error responses efficiently.

    + +

    Handles various response formats with minimal overhead:

    + +
      +
    • {"success": true/false}
    • +
    • {"error": "..."} → False
    • +
    • {"message": "..."} → False
    • +
    • Empty dict → True (assumed success)
    • +
    + +
    Parameters
    + +
      +
    • data: Response data
    • +
    + +
    Returns
    + +
    +

    True if successful, False otherwise

    +
    + +
    Example:
    + +
    +
    +
    >>> ResponseParser.parse_simple({"success": True})
    +True
    +>>> ResponseParser.parse_simple({"error": "Something failed"})
    +False
    +>>> ResponseParser.parse_simple({})
    +True
    +
    +
    +
    +
    + + +
    +
    + +
    +
    @staticmethod
    + + def + validate_response_structure( data: Mapping[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]] | object, required_fields: Sequence[str] | None = None) -> bool: + + + +
    + +
    208    @staticmethod
    +209    def validate_response_structure(
    +210        data: Mapping[str, JsonValue] | object,
    +211        required_fields: Sequence[str] | None = None,
    +212    ) -> bool:
    +213        """Validate response structure without full parsing.
    +214
    +215        Lightweight validation before expensive Pydantic validation.
    +216        Useful for early rejection of malformed responses.
    +217
    +218        :param data: Response data to validate
    +219        :param required_fields: Sequence of required field names
    +220        :return: True if structure is valid
    +221
    +222        Example:
    +223            >>> data = {"id": 1, "name": "test"}
    +224            >>> ResponseParser.validate_response_structure(data, ["id", "name"])
    +225            True
    +226            >>> ResponseParser.validate_response_structure(data, ["id", "missing"])
    +227            False
    +228        """
    +229        if not isinstance(data, dict):
    +230            return False
    +231
    +232        if not data and not required_fields:
    +233            return True
    +234
    +235        if not required_fields:
    +236            return True
    +237
    +238        return all(field in data for field in required_fields)
    +
    + + +

    Validate response structure without full parsing.

    + +

    Lightweight validation before expensive Pydantic validation. +Useful for early rejection of malformed responses.

    + +
    Parameters
    + +
      +
    • data: Response data to validate
    • +
    • required_fields: Sequence of required field names
    • +
    + +
    Returns
    + +
    +

    True if structure is valid

    +
    + +
    Example:
    + +
    +
    +
    >>> data = {"id": 1, "name": "test"}
    +>>> ResponseParser.validate_response_structure(data, ["id", "name"])
    +True
    +>>> ResponseParser.validate_response_structure(data, ["id", "missing"])
    +False
    +
    +
    +
    +
    -

    PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    -

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru -All rights reserved.

    +
    +
    + +
    +
    @staticmethod
    -

    This software is licensed under the MIT License.

    + def + extract_error_message( data: Mapping[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]] | object) -> str | None: -
    You can find the full license text at:
    + + +
    + +
    240    @staticmethod
    +241    def extract_error_message(data: Mapping[str, JsonValue] | object) -> str | None:
    +242        """Extract error message from response data efficiently.
    +243
    +244        Checks common error field names in order of preference.
    +245        Uses pre-computed tuple for fast iteration.
    +246
    +247        :param data: Response data
    +248        :return: Error message or None if not found
    +249
    +250        Example:
    +251            >>> ResponseParser.extract_error_message({"error": "Not found"})
    +252            'Not found'
    +253            >>> ResponseParser.extract_error_message({"message": "Failed"})
    +254            'Failed'
    +255            >>> ResponseParser.extract_error_message({"success": True})
    +256            None
    +257        """
    +258        if not isinstance(data, dict):
    +259            return None
    +260
    +261        for field in _ERROR_FIELDS:
    +262            if field in data:
    +263                value = data[field]
    +264                # Fast path: already a string
    +265                if isinstance(value, str):
    +266                    return value
    +267                # Convert non-string to string (None → None)
    +268                return str(value) if value is not None else None
    +269
    +270        return None
    +
    + + +

    Extract error message from response data efficiently.

    + +

    Checks common error field names in order of preference. +Uses pre-computed tuple for fast iteration.

    + +
    Parameters
    + +
      +
    • data: Response data
    • +
    + +
    Returns
    -

    https://opensource.org/licenses/MIT

    +

    Error message or None if not found

    -
    Source code repository:
    +
    Example:
    -

    https://github.com/orenlab/pyoutlineapi

    +
    +
    >>> ResponseParser.extract_error_message({"error": "Not found"})
    +'Not found'
    +>>> ResponseParser.extract_error_message({"message": "Failed"})
    +'Failed'
    +>>> ResponseParser.extract_error_message({"success": True})
    +None
    +
    +
    - - +
    +
    + +
    +
    @staticmethod
    -
      1"""
    -  2PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.
    -  3
    -  4Copyright (c) 2025 Denis Rozhnovskiy <pytelemonbot@mail.ru>
    -  5All rights reserved.
    -  6
    -  7This software is licensed under the MIT License.
    -  8You can find the full license text at:
    -  9    https://opensource.org/licenses/MIT
    - 10
    - 11Source code repository:
    - 12    https://github.com/orenlab/pyoutlineapi
    - 13"""
    - 14
    - 15from __future__ import annotations
    - 16
    - 17import sys
    - 18from importlib import metadata
    - 19from typing import Final, TYPE_CHECKING
    - 20
    - 21
    - 22def check_python_version():
    - 23    if sys.version_info < (3, 10):
    - 24        raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher")
    - 25
    - 26
    - 27check_python_version()
    - 28
    - 29# Core client imports
    - 30from .client import AsyncOutlineClient
    - 31from .exceptions import APIError, OutlineError
    - 32
    - 33# Package metadata
    - 34try:
    - 35    __version__: str = metadata.version("pyoutlineapi")
    - 36except metadata.PackageNotFoundError:  # Fallback for development
    - 37    __version__ = "0.3.0-dev"
    - 38
    - 39__author__: Final[str] = "Denis Rozhnovskiy"
    - 40__email__: Final[str] = "pytelemonbot@mail.ru"
    - 41__license__: Final[str] = "MIT"
    - 42
    - 43# Type checking imports
    - 44if TYPE_CHECKING:
    - 45    from .models import (
    - 46        AccessKey,
    - 47        AccessKeyCreateRequest,
    - 48        AccessKeyList,
    - 49        AccessKeyNameRequest,
    - 50        DataLimit,
    - 51        DataLimitRequest,
    - 52        ErrorResponse,
    - 53        ExperimentalMetrics,
    - 54        HostnameRequest,
    - 55        MetricsEnabledRequest,
    - 56        MetricsStatusResponse,
    - 57        PortRequest,
    - 58        Server,
    - 59        ServerMetrics,
    - 60        ServerNameRequest
    - 61    )
    - 62
    - 63# Runtime imports
    - 64from .models import (
    - 65    AccessKey,
    - 66    AccessKeyCreateRequest,
    - 67    AccessKeyList,
    - 68    AccessKeyNameRequest,
    - 69    DataLimit,
    - 70    DataLimitRequest,
    - 71    ErrorResponse,
    - 72    ExperimentalMetrics,
    - 73    HostnameRequest,
    - 74    MetricsEnabledRequest,
    - 75    MetricsStatusResponse,
    - 76    PortRequest,
    - 77    Server,
    - 78    ServerMetrics,
    - 79    ServerNameRequest,
    - 80)
    - 81
    - 82__all__: Final[list[str]] = [
    - 83    # Client
    - 84    "AsyncOutlineClient",
    - 85    "OutlineError",
    - 86    "APIError",
    - 87    # Models
    - 88    "AccessKey",
    - 89    "AccessKeyCreateRequest",
    - 90    "AccessKeyList",
    - 91    "AccessKeyNameRequest",
    - 92    "DataLimit",
    - 93    "DataLimitRequest",
    - 94    "ErrorResponse",
    - 95    "ExperimentalMetrics",
    - 96    "HostnameRequest",
    - 97    "MetricsEnabledRequest",
    - 98    "MetricsStatusResponse",
    - 99    "PortRequest",
    -100    "Server",
    -101    "ServerMetrics",
    -102    "ServerNameRequest",
    -103]
    +        def
    +        is_error_response(data: Mapping[str, object] | object) -> bool:
    +
    +                
    +
    +    
    + +
    272    @staticmethod
    +273    def is_error_response(data: Mapping[str, object] | object) -> bool:
    +274        """Check if response indicates an error efficiently.
    +275
    +276        Fast boolean check for error indicators in response.
    +277
    +278        :param data: Response data
    +279        :return: True if response indicates an error
    +280
    +281        Example:
    +282            >>> ResponseParser.is_error_response({"error": "Failed"})
    +283            True
    +284            >>> ResponseParser.is_error_response({"success": False})
    +285            True
    +286            >>> ResponseParser.is_error_response({"success": True})
    +287            False
    +288            >>> ResponseParser.is_error_response({})
    +289            False
    +290        """
    +291        if not isinstance(data, dict):
    +292            return False
    +293
    +294        if "error" in data or "error_message" in data:
    +295            return True
    +296
    +297        if "success" in data:
    +298            success = data["success"]
    +299            return success is False
    +300
    +301        # No error indicators found
    +302        return False
    +
    + + +

    Check if response indicates an error efficiently.

    + +

    Fast boolean check for error indicators in response.

    + +
    Parameters
    + +
      +
    • data: Response data
    • +
    + +
    Returns
    + +
    +

    True if response indicates an error

    +
    + +
    Example:
    + +
    +
    +
    >>> ResponseParser.is_error_response({"error": "Failed"})
    +True
    +>>> ResponseParser.is_error_response({"success": False})
    +True
    +>>> ResponseParser.is_error_response({"success": True})
    +False
    +>>> ResponseParser.is_error_response({})
    +False
    +
    +
    +
    +
    + + +
    +
    +
    + +
    + + class + SecureIDGenerator: + + + +
    + +
    298class SecureIDGenerator:
    +299    """Cryptographically secure ID generation."""
    +300
    +301    __slots__ = ()
    +302
    +303    @staticmethod
    +304    def generate_correlation_id() -> str:
    +305        """Generate secure correlation ID with 128 bits entropy.
    +306
    +307        Format: {timestamp_us}-{random_hex}
    +308
    +309        :return: Correlation ID string
    +310        """
    +311        # 16 bytes = 128 bits of entropy
    +312        random_part = secrets.token_hex(16)
    +313
    +314        # Microsecond timestamp for uniqueness and ordering
    +315        timestamp = int(time.time() * 1_000_000)
    +316
    +317        return f"{timestamp}-{random_part}"
    +318
    +319    @staticmethod
    +320    def generate_request_id() -> str:
    +321        """Generate secure request ID.
    +322
    +323        Alias for correlation ID for API compatibility.
    +324
    +325        :return: Request ID string
    +326        """
    +327        return SecureIDGenerator.generate_correlation_id()
    +
    + + +

    Cryptographically secure ID generation.

    +
    + + +
    + +
    +
    @staticmethod
    + + def + generate_correlation_id() -> str: + + + +
    + +
    303    @staticmethod
    +304    def generate_correlation_id() -> str:
    +305        """Generate secure correlation ID with 128 bits entropy.
    +306
    +307        Format: {timestamp_us}-{random_hex}
    +308
    +309        :return: Correlation ID string
    +310        """
    +311        # 16 bytes = 128 bits of entropy
    +312        random_part = secrets.token_hex(16)
    +313
    +314        # Microsecond timestamp for uniqueness and ordering
    +315        timestamp = int(time.time() * 1_000_000)
    +316
    +317        return f"{timestamp}-{random_part}"
    +
    + + +

    Generate secure correlation ID with 128 bits entropy.

    + +

    Format: {timestamp_us}-{random_hex}

    + +
    Returns
    + +
    +

    Correlation ID string

    +
    +
    + + +
    +
    + +
    +
    @staticmethod
    + + def + generate_request_id() -> str: + + + +
    + +
    319    @staticmethod
    +320    def generate_request_id() -> str:
    +321        """Generate secure request ID.
    +322
    +323        Alias for correlation ID for API compatibility.
    +324
    +325        :return: Request ID string
    +326        """
    +327        return SecureIDGenerator.generate_correlation_id()
    +
    + + +

    Generate secure request ID.

    + +

    Alias for correlation ID for API compatibility.

    + +
    Returns
    + +
    +

    Request ID string

    +
    +
    + + +
    +
    +
    + +
    + + class + Server(pyoutlineapi.common_types.BaseValidatedModel): + + + +
    + +
    250class Server(BaseValidatedModel):
    +251    """Server information model with optimized properties.
    +252
    +253    SCHEMA: Based on GET /server response
    +254    """
    +255
    +256    name: str | None = None
    +257    server_id: str = Field(alias="serverId")
    +258    metrics_enabled: bool = Field(alias="metricsEnabled")
    +259    created_timestamp_ms: TimestampMs = Field(alias="createdTimestampMs")
    +260    port_for_new_access_keys: Port = Field(alias="portForNewAccessKeys")
    +261    hostname_for_access_keys: str | None = Field(None, alias="hostnameForAccessKeys")
    +262    access_key_data_limit: DataLimit | None = Field(None, alias="accessKeyDataLimit")
    +263    version: str | None = None
    +264
    +265    @field_validator("name", mode="before")
    +266    @classmethod
    +267    def validate_name(cls, v: str) -> str:
    +268        """Validate server name.
    +269
    +270        :param v: Server name
    +271        :return: Validated name
    +272        :raises ValueError: If name is empty
    +273        """
    +274        validated = Validators.validate_name(v)
    +275        if validated is None:
    +276            raise ValueError("Server name cannot be empty")
    +277        return validated
    +278
    +279    @property
    +280    def has_global_limit(self) -> bool:
    +281        """Check if server has global data limit (optimized).
    +282
    +283        :return: True if global limit exists
    +284        """
    +285        return self.access_key_data_limit is not None
    +286
    +287    @cached_property
    +288    def created_timestamp_seconds(self) -> float:
    +289        """Get creation timestamp in seconds (cached).
    +290
    +291        NOTE: Cached because timestamp is immutable
    +292
    +293        :return: Timestamp in seconds
    +294        """
    +295        return self.created_timestamp_ms / _MS_IN_SEC
    +
    + + +

    Server information model with optimized properties.

    + +

    SCHEMA: Based on GET /server response

    +
    + + +
    +
    + name: str | None = +None + + +
    + + + + +
    +
    +
    + server_id: str = +PydanticUndefined + + +
    + + + + +
    +
    +
    + metrics_enabled: bool = +PydanticUndefined + + +
    + + + + +
    +
    +
    + created_timestamp_ms: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in milliseconds', metadata=[Ge(ge=0)])] = +PydanticUndefined + + +
    + + +

    Unix timestamp in milliseconds

    +
    + + +
    +
    +
    + port_for_new_access_keys: Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])] = +PydanticUndefined + + +
    + + +

    Port number (1-65535)

    +
    + + +
    +
    +
    + hostname_for_access_keys: str | None = +None + + +
    + + + + +
    +
    +
    + access_key_data_limit: DataLimit | None = +None + + +
    + + + + +
    +
    +
    + version: str | None = +None + + +
    + + + + +
    +
    + +
    +
    @field_validator('name', mode='before')
    +
    @classmethod
    + + def + validate_name(cls, v: str) -> str: + + + +
    + +
    265    @field_validator("name", mode="before")
    +266    @classmethod
    +267    def validate_name(cls, v: str) -> str:
    +268        """Validate server name.
    +269
    +270        :param v: Server name
    +271        :return: Validated name
    +272        :raises ValueError: If name is empty
    +273        """
    +274        validated = Validators.validate_name(v)
    +275        if validated is None:
    +276            raise ValueError("Server name cannot be empty")
    +277        return validated
    +
    + + +

    Validate server name.

    + +
    Parameters
    + +
      +
    • v: Server name
    • +
    + +
    Returns
    + +
    +

    Validated name

    +
    + +
    Raises
    + +
      +
    • ValueError: If name is empty
    • +
    +
    + + +
    +
    + +
    + has_global_limit: bool + + + +
    + +
    279    @property
    +280    def has_global_limit(self) -> bool:
    +281        """Check if server has global data limit (optimized).
    +282
    +283        :return: True if global limit exists
    +284        """
    +285        return self.access_key_data_limit is not None
    +
    + + +

    Check if server has global data limit (optimized).

    + +
    Returns
    + +
    +

    True if global limit exists

    +
    +
    + + +
    +
    + +
    + created_timestamp_seconds: float + + + +
    + +
    287    @cached_property
    +288    def created_timestamp_seconds(self) -> float:
    +289        """Get creation timestamp in seconds (cached).
    +290
    +291        NOTE: Cached because timestamp is immutable
    +292
    +293        :return: Timestamp in seconds
    +294        """
    +295        return self.created_timestamp_ms / _MS_IN_SEC
     
    -
    -
    - +

    Get creation timestamp in seconds (cached).

    + +

    NOTE: Cached because timestamp is immutable

    + +
    Returns
    + +
    +

    Timestamp in seconds

    +
    +
    + + + +
    +
    +
    class - AsyncOutlineClient: + ServerExperimentalMetric(pyoutlineapi.common_types.BaseValidatedModel): - +
    - -
     123class AsyncOutlineClient:
    - 124    """
    - 125    Asynchronous client for the Outline VPN Server API.
    - 126
    - 127    Args:
    - 128        api_url: Base URL for the Outline server API
    - 129        cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    - 130        json_format: Return raw JSON instead of Pydantic models
    - 131        timeout: Request timeout in seconds
    - 132        retry_attempts: Number of retry attempts connecting to the API
    - 133        enable_logging: Enable debug logging for API calls
    - 134        user_agent: Custom user agent string
    - 135        max_connections: Maximum number of connections in the pool
    - 136        rate_limit_delay: Minimum delay between requests (seconds)
    - 137
    - 138    Examples:
    - 139        >>> async def main():
    - 140        ...     async with AsyncOutlineClient(
    - 141        ...         "https://example.com:1234/secret",
    - 142        ...         "ab12cd34...",
    - 143        ...         enable_logging=True
    - 144        ...     ) as client:
    - 145        ...         server_info = await client.get_server_info()
    - 146        ...         print(f"Server: {server_info.name}")
    - 147        ...
    - 148        ...     # Or use as context manager factory
    - 149        ...     async with AsyncOutlineClient.create(
    - 150        ...         "https://example.com:1234/secret",
    - 151        ...         "ab12cd34..."
    - 152        ...     ) as client:
    - 153        ...         await client.get_server_info()
    - 154
    - 155    """
    - 156
    - 157    def __init__(
    - 158            self,
    - 159            api_url: str,
    - 160            cert_sha256: str,
    - 161            *,
    - 162            json_format: bool = False,
    - 163            timeout: int = 30,
    - 164            retry_attempts: int = 3,
    - 165            enable_logging: bool = False,
    - 166            user_agent: Optional[str] = None,
    - 167            max_connections: int = 10,
    - 168            rate_limit_delay: float = 0.0,
    - 169    ) -> None:
    - 170
    - 171        # Validate api_url
    - 172        if not api_url or not api_url.strip():
    - 173            raise ValueError("api_url cannot be empty or whitespace")
    - 174
    - 175        # Validate cert_sha256
    - 176        if not cert_sha256 or not cert_sha256.strip():
    - 177            raise ValueError("cert_sha256 cannot be empty or whitespace")
    - 178
    - 179        # Additional validation for cert_sha256 format (should be hex)
    - 180        cert_sha256_clean = cert_sha256.strip()
    - 181        if not all(c in '0123456789abcdefABCDEF' for c in cert_sha256_clean):
    - 182            raise ValueError("cert_sha256 must contain only hexadecimal characters")
    - 183
    - 184        # Check cert_sha256 length (SHA-256 should be 64 hex characters)
    - 185        if len(cert_sha256_clean) != 64:
    - 186            raise ValueError("cert_sha256 must be exactly 64 hexadecimal characters (SHA-256)")
    - 187
    - 188        self._api_url = api_url.rstrip("/")
    - 189        self._cert_sha256 = cert_sha256
    - 190        self._json_format = json_format
    - 191        self._timeout = aiohttp.ClientTimeout(total=timeout)
    - 192        self._ssl_context: Optional[Fingerprint] = None
    - 193        self._session: Optional[aiohttp.ClientSession] = None
    - 194        self._retry_attempts = retry_attempts
    - 195        self._enable_logging = enable_logging
    - 196        self._user_agent = user_agent or f"PyOutlineAPI/0.3.0"
    - 197        self._max_connections = max_connections
    - 198        self._rate_limit_delay = rate_limit_delay
    - 199        self._last_request_time: float = 0.0
    - 200
    - 201        # Health check state
    - 202        self._last_health_check: float = 0.0
    - 203        self._health_check_interval: float = 300.0  # 5 minutes
    - 204        self._is_healthy: bool = True
    - 205
    - 206        if enable_logging:
    - 207            self._setup_logging()
    - 208
    - 209    @staticmethod
    - 210    def _setup_logging() -> None:
    - 211        """Setup logging configuration if not already configured."""
    - 212        if not logger.handlers:
    - 213            handler = logging.StreamHandler()
    - 214            formatter = logging.Formatter(
    - 215                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    - 216            )
    - 217            handler.setFormatter(formatter)
    - 218            logger.addHandler(handler)
    - 219            logger.setLevel(logging.DEBUG)
    - 220
    - 221    @classmethod
    - 222    @asynccontextmanager
    - 223    async def create(
    - 224            cls,
    - 225            api_url: str,
    - 226            cert_sha256: str,
    - 227            **kwargs
    - 228    ) -> AsyncGenerator[AsyncOutlineClient, None]:
    - 229        """
    - 230        Factory method that returns an async context manager.
    - 231
    - 232        This is the recommended way to create clients for one-off operations.
    - 233        """
    - 234        client = cls(api_url, cert_sha256, **kwargs)
    - 235        async with client:
    - 236            yield client
    - 237
    - 238    async def __aenter__(self) -> AsyncOutlineClient:
    - 239        """Set up client session."""
    - 240        headers = {"User-Agent": self._user_agent}
    - 241
    - 242        connector = aiohttp.TCPConnector(
    - 243            ssl=self._get_ssl_context(),
    - 244            limit=self._max_connections,
    - 245            limit_per_host=self._max_connections // 2,
    - 246            enable_cleanup_closed=True,
    - 247        )
    - 248
    - 249        self._session = aiohttp.ClientSession(
    - 250            timeout=self._timeout,
    - 251            raise_for_status=False,
    - 252            connector=connector,
    - 253            headers=headers,
    - 254        )
    - 255
    - 256        if self._enable_logging:
    - 257            logger.info(f"Initialized OutlineAPI client for {self._api_url}")
    - 258
    - 259        return self
    - 260
    - 261    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    - 262        """Clean up client session."""
    - 263        if self._session:
    - 264            await self._session.close()
    - 265            self._session = None
    - 266
    - 267            if self._enable_logging:
    - 268                logger.info("OutlineAPI client session closed")
    - 269
    - 270    async def _apply_rate_limiting(self) -> None:
    - 271        """Apply rate limiting if configured."""
    - 272        if self._rate_limit_delay <= 0:
    - 273            return
    - 274
    - 275        time_since_last = time.time() - self._last_request_time
    - 276        if time_since_last < self._rate_limit_delay:
    - 277            delay = self._rate_limit_delay - time_since_last
    - 278            await asyncio.sleep(delay)
    - 279
    - 280        self._last_request_time = time.time()
    - 281
    - 282    async def health_check(self, force: bool = False) -> bool:
    - 283        """
    - 284        Perform a health check on the Outline server.
    - 285
    - 286        Args:
    - 287            force: Force health check even if recently performed
    - 288
    - 289        Returns:
    - 290            True if server is healthy
    - 291        """
    - 292        current_time = time.time()
    - 293
    - 294        if not force and (current_time - self._last_health_check) < self._health_check_interval:
    - 295            return self._is_healthy
    - 296
    - 297        try:
    - 298            await self.get_server_info()
    - 299            self._is_healthy = True
    - 300            if self._enable_logging:
    - 301                logger.info("Health check passed")
    - 302
    - 303            return self._is_healthy
    - 304        except Exception as e:
    - 305            self._is_healthy = False
    - 306            if self._enable_logging:
    - 307                logger.warning(f"Health check failed: {e}")
    - 308
    - 309            return self._is_healthy
    - 310        finally:
    - 311            self._last_health_check = current_time
    - 312
    - 313    @overload
    - 314    async def _parse_response(
    - 315            self,
    - 316            response: ClientResponse,
    - 317            model: type[BaseModel],
    - 318            json_format: Literal[True],
    - 319    ) -> JsonDict:
    - 320        ...
    - 321
    - 322    @overload
    - 323    async def _parse_response(
    - 324            self,
    - 325            response: ClientResponse,
    - 326            model: type[BaseModel],
    - 327            json_format: Literal[False],
    - 328    ) -> BaseModel:
    - 329        ...
    - 330
    - 331    @overload
    - 332    async def _parse_response(
    - 333            self, response: ClientResponse, model: type[BaseModel], json_format: bool
    - 334    ) -> ResponseType:
    - 335        ...
    - 336
    - 337    @ensure_context
    - 338    async def _parse_response(
    - 339            self,
    - 340            response: ClientResponse,
    - 341            model: type[BaseModel],
    - 342            json_format: bool = False,
    - 343    ) -> ResponseType:
    - 344        """
    - 345        Parse and validate API response data.
    - 346
    - 347        Args:
    - 348            response: API response to parse
    - 349            model: Pydantic model for validation
    - 350            json_format: Whether to return raw JSON
    - 351
    - 352        Returns:
    - 353            Validated response data
    - 354
    - 355        Raises:
    - 356            ValueError: If response validation fails
    - 357        """
    - 358        try:
    - 359            data = await response.json()
    - 360            validated = model.model_validate(data)
    - 361            return validated.model_dump(by_alias=True) if json_format else validated
    - 362        except aiohttp.ContentTypeError as content_error:
    - 363            raise ValueError("Invalid response format") from content_error
    - 364        except Exception as exception:
    - 365            raise ValueError(f"Validation error: {exception}") from exception
    - 366
    - 367    @staticmethod
    - 368    async def _handle_error_response(response: ClientResponse) -> None:
    - 369        """Handle error responses from the API."""
    - 370        try:
    - 371            error_data = await response.json()
    - 372            error = ErrorResponse.model_validate(error_data)
    - 373            raise APIError(f"{error.code}: {error.message}", response.status)
    - 374        except (ValueError, aiohttp.ContentTypeError):
    - 375            raise APIError(
    - 376                f"HTTP {response.status}: {response.reason}", response.status
    - 377            )
    - 378
    - 379    @ensure_context
    - 380    async def _request(
    - 381            self,
    - 382            method: str,
    - 383            endpoint: str,
    - 384            *,
    - 385            json: Any = None,
    - 386            params: Optional[JsonDict] = None,
    - 387    ) -> Any:
    - 388        """Make an API request."""
    - 389        await self._apply_rate_limiting()
    - 390        url = self._build_url(endpoint)
    - 391        return await self._make_request(method, url, json, params)
    - 392
    - 393    async def _make_request(
    - 394            self,
    - 395            method: str,
    - 396            url: str,
    - 397            json: Any = None,
    - 398            params: Optional[JsonDict] = None,
    - 399    ) -> Any:
    - 400        """Internal method to execute the actual request with retry logic."""
    - 401
    - 402        async def _do_request() -> Any:
    - 403            if self._enable_logging:
    - 404                # Don't log sensitive data
    - 405                safe_url = url.split('?')[0] if '?' in url else url
    - 406                logger.debug(f"Making {method} request to {safe_url}")
    - 407
    - 408            async with self._session.request(
    - 409                    method,
    - 410                    url,
    - 411                    json=json,
    - 412                    params=params,
    - 413                    raise_for_status=False,
    - 414            ) as response:
    - 415                if self._enable_logging:
    - 416                    logger.debug(f"Response: {response.status} {response.reason}")
    - 417
    - 418                if response.status >= 400:
    - 419                    await self._handle_error_response(response)
    - 420
    - 421                if response.status == 204:
    - 422                    return True
    - 423
    - 424                try:
    - 425                    # See #b1746e6
    - 426                    await response.json()
    - 427                    return response
    - 428                except Exception as exception:
    - 429                    raise APIError(
    - 430                        f"Failed to process response: {exception}", response.status
    - 431                    )
    - 432
    - 433        return await self._retry_request(_do_request, attempts=self._retry_attempts)
    - 434
    - 435    @staticmethod
    - 436    async def _retry_request(
    - 437            request_func: Callable[[], Awaitable[T]],
    - 438            *,
    - 439            attempts: int = DEFAULT_RETRY_ATTEMPTS,
    - 440            delay: float = DEFAULT_RETRY_DELAY,
    - 441    ) -> T:
    - 442        """
    - 443        Execute request with retry logic.
    - 444
    - 445        Args:
    - 446            request_func: Async function to execute
    - 447            attempts: Maximum number of retry attempts
    - 448            delay: Delay between retries in seconds
    - 449
    - 450        Returns:
    - 451            Response from the successful request
    - 452
    - 453        Raises:
    - 454            APIError: If all retry attempts fail
    - 455        """
    - 456        last_error = None
    - 457
    - 458        for attempt in range(attempts):
    - 459            try:
    - 460                return await request_func()
    - 461            except (aiohttp.ClientError, APIError) as error:
    - 462                last_error = error
    - 463
    - 464                # Don't retry if it's not a retriable error
    - 465                if isinstance(error, APIError) and (
    - 466                        error.status_code not in RETRY_STATUS_CODES
    - 467                ):
    - 468                    raise
    - 469
    - 470                # Don't sleep on the last attempt
    - 471                if attempt < attempts - 1:
    - 472                    await asyncio.sleep(delay * (attempt + 1))
    - 473
    - 474        raise APIError(
    - 475            f"Request failed after {attempts} attempts: {last_error}",
    - 476            getattr(last_error, "status_code", None),
    - 477        )
    - 478
    - 479    def _build_url(self, endpoint: str) -> str:
    - 480        """Build and validate the full URL for the API request."""
    - 481        if not isinstance(endpoint, str):
    - 482            raise ValueError("Endpoint must be a string")
    - 483
    - 484        url = f"{self._api_url}/{endpoint.lstrip('/')}"
    - 485        parsed_url = urlparse(url)
    - 486
    - 487        if not all([parsed_url.scheme, parsed_url.netloc]):
    - 488            raise ValueError(f"Invalid URL: {url}")
    - 489
    - 490        return url
    - 491
    - 492    def _get_ssl_context(self) -> Optional[Fingerprint]:
    - 493        """Create an SSL context if a certificate fingerprint is provided."""
    - 494        if not self._cert_sha256:
    - 495            return None
    - 496
    - 497        try:
    - 498            return Fingerprint(binascii.unhexlify(self._cert_sha256))
    - 499        except binascii.Error as validation_error:
    - 500            raise ValueError(
    - 501                f"Invalid certificate SHA256: {self._cert_sha256}"
    - 502            ) from validation_error
    - 503        except Exception as exception:
    - 504            raise OutlineError("Failed to create SSL context") from exception
    - 505
    - 506    # Server Management Methods
    - 507
    - 508    @log_method_call
    - 509    async def get_server_info(self) -> Union[JsonDict, Server]:
    - 510        """
    - 511        Get server information.
    - 512
    - 513        Returns:
    - 514            Server information including name, ID, and configuration.
    - 515
    - 516        Examples:
    - 517            >>> async def main():
    - 518            ...     async with AsyncOutlineClient(
    - 519            ...         "https://example.com:1234/secret",
    - 520            ...         "ab12cd34..."
    - 521            ...     ) as client:
    - 522            ...         server = await client.get_server_info()
    - 523            ...         print(f"Server {server.name} running version {server.version}")
    - 524        """
    - 525        response = await self._request("GET", "server")
    - 526        return await self._parse_response(
    - 527            response, Server, json_format=self._json_format
    - 528        )
    - 529
    - 530    @log_method_call
    - 531    async def rename_server(self, name: str) -> bool:
    - 532        """
    - 533        Rename the server.
    - 534
    - 535        Args:
    - 536            name: New server name
    - 537
    - 538        Returns:
    - 539            True if successful
    - 540
    - 541        Examples:
    - 542            >>> async def main():
    - 543            ...     async with AsyncOutlineClient(
    - 544            ...         "https://example.com:1234/secret",
    - 545            ...         "ab12cd34..."
    - 546            ...     ) as client:
    - 547            ...         success = await client.rename_server("My VPN Server")
    - 548            ...         if success:
    - 549            ...             print("Server renamed successfully")
    - 550        """
    - 551        request = ServerNameRequest(name=name)
    - 552        return await self._request(
    - 553            "PUT", "name", json=request.model_dump(by_alias=True)
    - 554        )
    - 555
    - 556    @log_method_call
    - 557    async def set_hostname(self, hostname: str) -> bool:
    - 558        """
    - 559        Set server hostname for access keys.
    - 560
    - 561        Args:
    - 562            hostname: New hostname or IP address
    - 563
    - 564        Returns:
    - 565            True if successful
    - 566
    - 567        Raises:
    - 568            APIError: If hostname is invalid
    - 569
    - 570        Examples:
    - 571            >>> async def main():
    - 572            ...     async with AsyncOutlineClient(
    - 573            ...         "https://example.com:1234/secret",
    - 574            ...         "ab12cd34..."
    - 575            ...     ) as client:
    - 576            ...         await client.set_hostname("vpn.example.com")
    - 577            ...         # Or use IP address
    - 578            ...         await client.set_hostname("203.0.113.1")
    - 579        """
    - 580        request = HostnameRequest(hostname=hostname)
    - 581        return await self._request(
    - 582            "PUT",
    - 583            "server/hostname-for-access-keys",
    - 584            json=request.model_dump(by_alias=True),
    - 585        )
    - 586
    - 587    @log_method_call
    - 588    async def set_default_port(self, port: int) -> bool:
    - 589        """
    - 590        Set default port for new access keys.
    - 591
    - 592        Args:
    - 593            port: Port number (1025-65535)
    - 594
    - 595        Returns:
    - 596            True if successful
    - 597
    - 598        Raises:
    - 599            APIError: If port is invalid or in use
    - 600
    - 601        Examples:
    - 602            >>> async def main():
    - 603            ...     async with AsyncOutlineClient(
    - 604            ...         "https://example.com:1234/secret",
    - 605            ...         "ab12cd34..."
    - 606            ...     ) as client:
    - 607            ...         await client.set_default_port(8388)
    - 608        """
    - 609        if port < MIN_PORT or port > MAX_PORT:
    - 610            raise ValueError(
    - 611                f"Privileged ports are not allowed. Use range: {MIN_PORT}-{MAX_PORT}"
    - 612            )
    - 613
    - 614        request = PortRequest(port=port)
    - 615        return await self._request(
    - 616            "PUT",
    - 617            "server/port-for-new-access-keys",
    - 618            json=request.model_dump(by_alias=True),
    - 619        )
    - 620
    - 621    # Metrics Methods
    - 622
    - 623    @log_method_call
    - 624    async def get_metrics_status(self) -> Union[JsonDict, MetricsStatusResponse]:
    - 625        """
    - 626        Get whether metrics collection is enabled.
    - 627
    - 628        Returns:
    - 629            Current metrics collection status
    - 630
    - 631        Examples:
    - 632            >>> async def main():
    - 633            ...     async with AsyncOutlineClient(
    - 634            ...         "https://example.com:1234/secret",
    - 635            ...         "ab12cd34..."
    - 636            ...     ) as client:
    - 637            ...         status = await client.get_metrics_status()
    - 638            ...         if status.metrics_enabled:
    - 639            ...             print("Metrics collection is enabled")
    - 640        """
    - 641        response = await self._request("GET", "metrics/enabled")
    - 642        return await self._parse_response(
    - 643            response, MetricsStatusResponse, json_format=self._json_format
    - 644        )
    - 645
    - 646    @log_method_call
    - 647    async def set_metrics_status(self, enabled: bool) -> bool:
    - 648        """
    - 649        Enable or disable metrics collection.
    - 650
    - 651        Args:
    - 652            enabled: Whether to enable metrics
    - 653
    - 654        Returns:
    - 655            True if successful
    - 656
    - 657        Examples:
    - 658            >>> async def main():
    - 659            ...     async with AsyncOutlineClient(
    - 660            ...         "https://example.com:1234/secret",
    - 661            ...         "ab12cd34..."
    - 662            ...     ) as client:
    - 663            ...         # Enable metrics
    - 664            ...         await client.set_metrics_status(True)
    - 665            ...         # Check new status
    - 666            ...         status = await client.get_metrics_status()
    - 667        """
    - 668        request = MetricsEnabledRequest(metricsEnabled=enabled)
    - 669        return await self._request(
    - 670            "PUT", "metrics/enabled", json=request.model_dump(by_alias=True)
    - 671        )
    - 672
    - 673    @log_method_call
    - 674    async def get_transfer_metrics(self) -> Union[JsonDict, ServerMetrics]:
    - 675        """
    - 676        Get transfer metrics for all access keys.
    - 677
    - 678        Returns:
    - 679            Transfer metrics data for each access key
    - 680
    - 681        Examples:
    - 682            >>> async def main():
    - 683            ...     async with AsyncOutlineClient(
    - 684            ...         "https://example.com:1234/secret",
    - 685            ...         "ab12cd34..."
    - 686            ...     ) as client:
    - 687            ...         metrics = await client.get_transfer_metrics()
    - 688            ...         for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items():
    - 689            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    - 690        """
    - 691        response = await self._request("GET", "metrics/transfer")
    - 692        return await self._parse_response(
    - 693            response, ServerMetrics, json_format=self._json_format
    - 694        )
    - 695
    - 696    @log_method_call
    - 697    async def get_experimental_metrics(
    - 698            self, since: str
    - 699    ) -> Union[JsonDict, ExperimentalMetrics]:
    - 700        """
    - 701        Get experimental server metrics.
    - 702
    - 703        Args:
    - 704            since: Required time range filter (e.g., "24h", "7d", "30d", or ISO timestamp)
    - 705
    - 706        Returns:
    - 707            Detailed server and access key metrics
    - 708
    - 709        Examples:
    - 710            >>> async def main():
    - 711            ...     async with AsyncOutlineClient(
    - 712            ...         "https://example.com:1234/secret",
    - 713            ...         "ab12cd34..."
    - 714            ...     ) as client:
    - 715            ...         # Get metrics for the last 24 hours
    - 716            ...         metrics = await client.get_experimental_metrics("24h")
    - 717            ...         print(f"Server tunnel time: {metrics.server.tunnel_time.seconds}s")
    - 718            ...         print(f"Server data transferred: {metrics.server.data_transferred.bytes} bytes")
    - 719            ...
    - 720            ...         # Get metrics for the last 7 days
    - 721            ...         metrics = await client.get_experimental_metrics("7d")
    - 722            ...
    - 723            ...         # Get metrics since specific timestamp
    - 724            ...         metrics = await client.get_experimental_metrics("2024-01-01T00:00:00Z")
    - 725        """
    - 726        if not since or not since.strip():
    - 727            raise ValueError("Parameter 'since' is required and cannot be empty")
    - 728
    - 729        params = {"since": since}
    - 730        response = await self._request(
    - 731            "GET", "experimental/server/metrics", params=params
    - 732        )
    - 733        return await self._parse_response(
    - 734            response, ExperimentalMetrics, json_format=self._json_format
    - 735        )
    - 736
    - 737    # Access Key Management Methods
    - 738
    - 739    @log_method_call
    - 740    async def create_access_key(
    - 741            self,
    - 742            *,
    - 743            name: Optional[str] = None,
    - 744            password: Optional[str] = None,
    - 745            port: Optional[int] = None,
    - 746            method: Optional[str] = None,
    - 747            limit: Optional[DataLimit] = None,
    - 748    ) -> Union[JsonDict, AccessKey]:
    - 749        """
    - 750        Create a new access key.
    - 751
    - 752        Args:
    - 753            name: Optional key name
    - 754            password: Optional password
    - 755            port: Optional port number (1-65535)
    - 756            method: Optional encryption method
    - 757            limit: Optional data transfer limit
    - 758
    - 759        Returns:
    - 760            New access key details
    - 761
    - 762        Examples:
    - 763            >>> async def main():
    - 764            ...     async with AsyncOutlineClient(
    - 765            ...         "https://example.com:1234/secret",
    - 766            ...         "ab12cd34..."
    - 767            ...     ) as client:
    - 768            ...         # Create basic key
    - 769            ...         key = await client.create_access_key(name="User 1")
    - 770            ...
    - 771            ...         # Create key with data limit
    - 772            ...         lim = DataLimit(bytes=5 * 1024**3)  # 5 GB
    - 773            ...         key = await client.create_access_key(
    - 774            ...             name="Limited User",
    - 775            ...             port=8388,
    - 776            ...             limit=lim
    - 777            ...         )
    - 778            ...         print(f"Created key: {key.access_url}")
    - 779        """
    - 780        request = AccessKeyCreateRequest(
    - 781            name=name, password=password, port=port, method=method, limit=limit
    - 782        )
    - 783        response = await self._request(
    - 784            "POST",
    - 785            "access-keys",
    - 786            json=request.model_dump(exclude_none=True, by_alias=True),
    - 787        )
    - 788        return await self._parse_response(
    - 789            response, AccessKey, json_format=self._json_format
    - 790        )
    - 791
    - 792    @log_method_call
    - 793    async def create_access_key_with_id(
    - 794            self,
    - 795            key_id: str,
    - 796            *,
    - 797            name: Optional[str] = None,
    - 798            password: Optional[str] = None,
    - 799            port: Optional[int] = None,
    - 800            method: Optional[str] = None,
    - 801            limit: Optional[DataLimit] = None,
    - 802    ) -> Union[JsonDict, AccessKey]:
    - 803        """
    - 804        Create a new access key with specific ID.
    - 805
    - 806        Args:
    - 807            key_id: Specific ID for the access key
    - 808            name: Optional key name
    - 809            password: Optional password
    - 810            port: Optional port number (1-65535)
    - 811            method: Optional encryption method
    - 812            limit: Optional data transfer limit
    - 813
    - 814        Returns:
    - 815            New access key details
    - 816
    - 817        Examples:
    - 818            >>> async def main():
    - 819            ...     async with AsyncOutlineClient(
    - 820            ...         "https://example.com:1234/secret",
    - 821            ...         "ab12cd34..."
    - 822            ...     ) as client:
    - 823            ...         key = await client.create_access_key_with_id(
    - 824            ...             "my-custom-id",
    - 825            ...             name="Custom Key"
    - 826            ...         )
    - 827        """
    - 828        request = AccessKeyCreateRequest(
    - 829            name=name, password=password, port=port, method=method, limit=limit
    - 830        )
    - 831        response = await self._request(
    - 832            "PUT",
    - 833            f"access-keys/{key_id}",
    - 834            json=request.model_dump(exclude_none=True, by_alias=True),
    - 835        )
    - 836        return await self._parse_response(
    - 837            response, AccessKey, json_format=self._json_format
    - 838        )
    - 839
    - 840    @log_method_call
    - 841    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    - 842        """
    - 843        Get all access keys.
    - 844
    - 845        Returns:
    - 846            List of all access keys
    - 847
    - 848        Examples:
    - 849            >>> async def main():
    - 850            ...     async with AsyncOutlineClient(
    - 851            ...         "https://example.com:1234/secret",
    - 852            ...         "ab12cd34..."
    - 853            ...     ) as client:
    - 854            ...         keys = await client.get_access_keys()
    - 855            ...         for key in keys.access_keys:
    - 856            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    - 857            ...             if key.data_limit:
    - 858            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    - 859        """
    - 860        response = await self._request("GET", "access-keys")
    - 861        return await self._parse_response(
    - 862            response, AccessKeyList, json_format=self._json_format
    - 863        )
    - 864
    - 865    @log_method_call
    - 866    async def get_access_key(self, key_id: str) -> Union[JsonDict, AccessKey]:
    - 867        """
    - 868        Get specific access key.
    - 869
    - 870        Args:
    - 871            key_id: Access key ID
    - 872
    - 873        Returns:
    - 874            Access key details
    - 875
    - 876        Raises:
    - 877            APIError: If key doesn't exist
    - 878
    - 879        Examples:
    - 880            >>> async def main():
    - 881            ...     async with AsyncOutlineClient(
    - 882            ...         "https://example.com:1234/secret",
    - 883            ...         "ab12cd34..."
    - 884            ...     ) as client:
    - 885            ...         key = await client.get_access_key("1")
    - 886            ...         print(f"Port: {key.port}")
    - 887            ...         print(f"URL: {key.access_url}")
    - 888        """
    - 889        response = await self._request("GET", f"access-keys/{key_id}")
    - 890        return await self._parse_response(
    - 891            response, AccessKey, json_format=self._json_format
    - 892        )
    - 893
    - 894    @log_method_call
    - 895    async def rename_access_key(self, key_id: str, name: str) -> bool:
    - 896        """
    - 897        Rename access key.
    - 898
    - 899        Args:
    - 900            key_id: Access key ID
    - 901            name: New name
    - 902
    - 903        Returns:
    - 904            True if successful
    - 905
    - 906        Raises:
    - 907            APIError: If key doesn't exist
    - 908
    - 909        Examples:
    - 910            >>> async def main():
    - 911            ...     async with AsyncOutlineClient(
    - 912            ...         "https://example.com:1234/secret",
    - 913            ...         "ab12cd34..."
    - 914            ...     ) as client:
    - 915            ...         # Rename key
    - 916            ...         await client.rename_access_key("1", "Alice")
    - 917            ...
    - 918            ...         # Verify new name
    - 919            ...         key = await client.get_access_key("1")
    - 920            ...         assert key.name == "Alice"
    - 921        """
    - 922        request = AccessKeyNameRequest(name=name)
    - 923        return await self._request(
    - 924            "PUT", f"access-keys/{key_id}/name", json=request.model_dump(by_alias=True)
    - 925        )
    - 926
    - 927    @log_method_call
    - 928    async def delete_access_key(self, key_id: str) -> bool:
    - 929        """
    - 930        Delete access key.
    - 931
    - 932        Args:
    - 933            key_id: Access key ID
    - 934
    - 935        Returns:
    - 936            True if successful
    - 937
    - 938        Raises:
    - 939            APIError: If key doesn't exist
    - 940
    - 941        Examples:
    - 942            >>> async def main():
    - 943            ...     async with AsyncOutlineClient(
    - 944            ...         "https://example.com:1234/secret",
    - 945            ...         "ab12cd34..."
    - 946            ...     ) as client:
    - 947            ...         if await client.delete_access_key("1"):
    - 948            ...             print("Key deleted")
    - 949        """
    - 950        return await self._request("DELETE", f"access-keys/{key_id}")
    - 951
    - 952    @log_method_call
    - 953    async def set_access_key_data_limit(self, key_id: str, bytes_limit: int) -> bool:
    - 954        """
    - 955        Set data transfer limit for access key.
    - 956
    - 957        Args:
    - 958            key_id: Access key ID
    - 959            bytes_limit: Limit in bytes (must be non-negative)
    - 960
    - 961        Returns:
    - 962            True if successful
    - 963
    - 964        Raises:
    - 965            APIError: If key doesn't exist or limit is invalid
    - 966
    - 967        Examples:
    - 968            >>> async def main():
    - 969            ...     async with AsyncOutlineClient(
    - 970            ...         "https://example.com:1234/secret",
    - 971            ...         "ab12cd34..."
    - 972            ...     ) as client:
    - 973            ...         # Set 5 GB limit
    - 974            ...         limit = 5 * 1024**3  # 5 GB in bytes
    - 975            ...         await client.set_access_key_data_limit("1", limit)
    - 976            ...
    - 977            ...         # Verify limit
    - 978            ...         key = await client.get_access_key("1")
    - 979            ...         assert key.data_limit and key.data_limit.bytes == limit
    - 980        """
    - 981        request = DataLimitRequest(limit=DataLimit(bytes=bytes_limit))
    - 982        return await self._request(
    - 983            "PUT",
    - 984            f"access-keys/{key_id}/data-limit",
    - 985            json=request.model_dump(by_alias=True),
    - 986        )
    - 987
    - 988    @log_method_call
    - 989    async def remove_access_key_data_limit(self, key_id: str) -> bool:
    - 990        """
    - 991        Remove data transfer limit from access key.
    - 992
    - 993        Args:
    - 994            key_id: Access key ID
    - 995
    - 996        Returns:
    - 997            True if successful
    - 998
    - 999        Raises:
    -1000            APIError: If key doesn't exist
    -1001
    -1002        Examples:
    -1003            >>> async def main():
    -1004            ...     async with AsyncOutlineClient(
    -1005            ...         "https://example.com:1234/secret",
    -1006            ...         "ab12cd34..."
    -1007            ...     ) as client:
    -1008            ...         await client.remove_access_key_data_limit("1")
    -1009        """
    -1010        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
    -1011
    -1012    # Global Data Limit Methods
    -1013
    -1014    @log_method_call
    -1015    async def set_global_data_limit(self, bytes_limit: int) -> bool:
    -1016        """
    -1017        Set global data transfer limit for all access keys.
    -1018
    -1019        Args:
    -1020            bytes_limit: Limit in bytes (must be non-negative)
    -1021
    -1022        Returns:
    -1023            True if successful
    -1024
    -1025        Examples:
    -1026            >>> async def main():
    -1027            ...     async with AsyncOutlineClient(
    -1028            ...         "https://example.com:1234/secret",
    -1029            ...         "ab12cd34..."
    -1030            ...     ) as client:
    -1031            ...         # Set 100 GB global limit
    -1032            ...         await client.set_global_data_limit(100 * 1024**3)
    -1033        """
    -1034        request = DataLimitRequest(limit=DataLimit(bytes=bytes_limit))
    -1035        return await self._request(
    -1036            "PUT",
    -1037            "server/access-key-data-limit",
    -1038            json=request.model_dump(by_alias=True),
    -1039        )
    -1040
    -1041    @log_method_call
    -1042    async def remove_global_data_limit(self) -> bool:
    -1043        """
    -1044        Remove global data transfer limit.
    -1045
    -1046        Returns:
    -1047            True if successful
    -1048
    -1049        Examples:
    -1050            >>> async def main():
    -1051            ...     async with AsyncOutlineClient(
    -1052            ...         "https://example.com:1234/secret",
    -1053            ...         "ab12cd34..."
    -1054            ...     ) as client:
    -1055            ...         await client.remove_global_data_limit()
    -1056        """
    -1057        return await self._request("DELETE", "server/access-key-data-limit")
    -1058
    -1059    # Batch Operations
    -1060
    -1061    async def batch_create_access_keys(
    -1062            self,
    -1063            keys_config: list[dict[str, Any]],
    -1064            fail_fast: bool = True
    -1065    ) -> list[Union[AccessKey, Exception]]:
    -1066        """
    -1067        Create multiple access keys in batch.
    -1068
    -1069        Args:
    -1070            keys_config: List of key configurations (same as create_access_key kwargs)
    -1071            fail_fast: If True, stop on first error. If False, continue and return errors.
    -1072
    -1073        Returns:
    -1074            List of created keys or exceptions
    -1075
    -1076        Examples:
    -1077            >>> async def main():
    -1078            ...     async with AsyncOutlineClient(
    -1079            ...         "https://example.com:1234/secret",
    -1080            ...         "ab12cd34..."
    -1081            ...     ) as client:
    -1082            ...         configs = [
    -1083            ...             {"name": "User1", "limit": DataLimit(bytes=1024**3)},
    -1084            ...             {"name": "User2", "port": 8388},
    -1085            ...         ]
    -1086            ...         res = await client.batch_create_access_keys(configs)
    -1087        """
    -1088        results = []
    -1089
    -1090        for config in keys_config:
    -1091            try:
    -1092                key = await self.create_access_key(**config)
    -1093                results.append(key)
    -1094            except Exception as e:
    -1095                if fail_fast:
    -1096                    raise
    -1097                results.append(e)
    -1098
    -1099        return results
    -1100
    -1101    async def get_server_summary(self, metrics_since: str = "24h") -> dict[str, Any]:
    -1102        """
    -1103        Get comprehensive server summary including info, metrics, and key count.
    -1104
    -1105        Args:
    -1106            metrics_since: Time range for experimental metrics (default: "24h")
    -1107
    -1108        Returns:
    -1109            Dictionary with server info, health status, and statistics
    -1110        """
    -1111        summary = {}
    -1112
    -1113        try:
    -1114            # Get basic server info
    -1115            server_info = await self.get_server_info()
    -1116            summary["server"] = server_info.model_dump() if isinstance(server_info, BaseModel) else server_info
    -1117
    -1118            # Get access keys count
    -1119            keys = await self.get_access_keys()
    -1120            key_list = keys.access_keys if isinstance(keys, BaseModel) else keys.get("accessKeys", [])
    -1121            summary["access_keys_count"] = len(key_list)
    -1122
    -1123            # Get metrics if available
    -1124            try:
    -1125                metrics_status = await self.get_metrics_status()
    -1126                if (isinstance(metrics_status, BaseModel) and metrics_status.metrics_enabled) or \
    -1127                        (isinstance(metrics_status, dict) and metrics_status.get("metricsEnabled")):
    -1128                    transfer_metrics = await self.get_transfer_metrics()
    -1129                    summary["transfer_metrics"] = transfer_metrics.model_dump() if isinstance(transfer_metrics,
    -1130                                                                                              BaseModel) else transfer_metrics
    -1131
    -1132                    # Try to get experimental metrics
    -1133                    try:
    -1134                        experimental_metrics = await self.get_experimental_metrics(metrics_since)
    -1135                        summary["experimental_metrics"] = experimental_metrics.model_dump() if isinstance(
    -1136                            experimental_metrics,
    -1137                            BaseModel) else experimental_metrics
    -1138                    except Exception:
    -1139                        summary["experimental_metrics"] = None
    -1140            except Exception:
    -1141                summary["transfer_metrics"] = None
    -1142                summary["experimental_metrics"] = None
    -1143
    -1144            summary["healthy"] = True
    -1145
    -1146        except Exception as e:
    -1147            summary["healthy"] = False
    -1148            summary["error"] = str(e)
    -1149
    -1150        return summary
    -1151
    -1152    # Utility and management methods
    -1153
    -1154    def configure_logging(self, level: str = "INFO", format_string: Optional[str] = None) -> None:
    -1155        """
    -1156        Configure logging for the client.
    -1157
    -1158        Args:
    -1159            level: Logging level (DEBUG, INFO, WARNING, ERROR)
    -1160            format_string: Custom format string for log messages
    -1161        """
    -1162        self._enable_logging = True
    -1163
    -1164        # Clear existing handlers
    -1165        logger.handlers.clear()
    -1166
    -1167        handler = logging.StreamHandler()
    -1168        if format_string:
    -1169            formatter = logging.Formatter(format_string)
    -1170        else:
    -1171            formatter = logging.Formatter(
    -1172                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    -1173            )
    -1174        handler.setFormatter(formatter)
    -1175        logger.addHandler(handler)
    -1176        logger.setLevel(getattr(logging, level.upper()))
    -1177
    -1178    @property
    -1179    def is_healthy(self) -> bool:
    -1180        """Check if the last health check passed."""
    -1181        return self._is_healthy
    -1182
    -1183    @property
    -1184    def session(self) -> Optional[aiohttp.ClientSession]:
    -1185        """Access the current client session."""
    -1186        return self._session
    -1187
    -1188    @property
    -1189    def api_url(self) -> str:
    -1190        """Get the API URL (without sensitive parts)."""
    -1191        from urllib.parse import urlparse
    -1192        parsed = urlparse(self._api_url)
    -1193        return f"{parsed.scheme}://{parsed.netloc}"
    -1194
    -1195    def __repr__(self) -> str:
    -1196        """String representation of the client."""
    -1197        status = "connected" if self._session and not self._session.closed else "disconnected"
    -1198        return f"AsyncOutlineClient(url={self.api_url}, status={status})"
    -
    - - -

    Asynchronous client for the Outline VPN Server API.

    - -
    Arguments:
    + +
    448class ServerExperimentalMetric(BaseValidatedModel):
    +449    """Server-level experimental metrics.
    +450
    +451    SCHEMA: Based on experimental metrics server object
    +452    """
    +453
    +454    tunnel_time: TunnelTime = Field(alias="tunnelTime")
    +455    data_transferred: DataTransferred = Field(alias="dataTransferred")
    +456    bandwidth: BandwidthInfo
    +457    locations: list[LocationMetric]
    +
    -
      -
    • api_url: Base URL for the Outline server API
    • -
    • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    • -
    • json_format: Return raw JSON instead of Pydantic models
    • -
    • timeout: Request timeout in seconds
    • -
    • retry_attempts: Number of retry attempts connecting to the API
    • -
    • enable_logging: Enable debug logging for API calls
    • -
    • user_agent: Custom user agent string
    • -
    • max_connections: Maximum number of connections in the pool
    • -
    • rate_limit_delay: Minimum delay between requests (seconds)
    • -
    -
    Examples:
    +

    Server-level experimental metrics.

    -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34...",
    -...         enable_logging=True
    -...     ) as client:
    -...         server_info = await client.get_server_info()
    -...         print(f"Server: {server_info.name}")
    -...
    -...     # Or use as context manager factory
    -...     async with AsyncOutlineClient.create(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         await client.get_server_info()
    -
    -
    -
    +

    SCHEMA: Based on experimental metrics server object

    -
    - -
    - - AsyncOutlineClient( api_url: str, cert_sha256: str, *, json_format: bool = False, timeout: int = 30, retry_attempts: int = 3, enable_logging: bool = False, user_agent: Optional[str] = None, max_connections: int = 10, rate_limit_delay: float = 0.0) +
    +
    + tunnel_time: TunnelTime = +PydanticUndefined - + +
    + + + + +
    +
    +
    + data_transferred: DataTransferred = +PydanticUndefined +
    - -
    157    def __init__(
    -158            self,
    -159            api_url: str,
    -160            cert_sha256: str,
    -161            *,
    -162            json_format: bool = False,
    -163            timeout: int = 30,
    -164            retry_attempts: int = 3,
    -165            enable_logging: bool = False,
    -166            user_agent: Optional[str] = None,
    -167            max_connections: int = 10,
    -168            rate_limit_delay: float = 0.0,
    -169    ) -> None:
    -170
    -171        # Validate api_url
    -172        if not api_url or not api_url.strip():
    -173            raise ValueError("api_url cannot be empty or whitespace")
    -174
    -175        # Validate cert_sha256
    -176        if not cert_sha256 or not cert_sha256.strip():
    -177            raise ValueError("cert_sha256 cannot be empty or whitespace")
    -178
    -179        # Additional validation for cert_sha256 format (should be hex)
    -180        cert_sha256_clean = cert_sha256.strip()
    -181        if not all(c in '0123456789abcdefABCDEF' for c in cert_sha256_clean):
    -182            raise ValueError("cert_sha256 must contain only hexadecimal characters")
    -183
    -184        # Check cert_sha256 length (SHA-256 should be 64 hex characters)
    -185        if len(cert_sha256_clean) != 64:
    -186            raise ValueError("cert_sha256 must be exactly 64 hexadecimal characters (SHA-256)")
    -187
    -188        self._api_url = api_url.rstrip("/")
    -189        self._cert_sha256 = cert_sha256
    -190        self._json_format = json_format
    -191        self._timeout = aiohttp.ClientTimeout(total=timeout)
    -192        self._ssl_context: Optional[Fingerprint] = None
    -193        self._session: Optional[aiohttp.ClientSession] = None
    -194        self._retry_attempts = retry_attempts
    -195        self._enable_logging = enable_logging
    -196        self._user_agent = user_agent or f"PyOutlineAPI/0.3.0"
    -197        self._max_connections = max_connections
    -198        self._rate_limit_delay = rate_limit_delay
    -199        self._last_request_time: float = 0.0
    -200
    -201        # Health check state
    -202        self._last_health_check: float = 0.0
    -203        self._health_check_interval: float = 300.0  # 5 minutes
    -204        self._is_healthy: bool = True
    -205
    -206        if enable_logging:
    -207            self._setup_logging()
    -
    + + + +
    +
    +
    + bandwidth: BandwidthInfo = +PydanticUndefined + +
    + +
    -
    - -
    -
    @classmethod
    -
    @asynccontextmanager
    +
    +
    + locations: list[LocationMetric] = +PydanticUndefined - def - create( cls, api_url: str, cert_sha256: str, **kwargs) -> AsyncGenerator[AsyncOutlineClient, NoneType]: + +
    + + + - +
    +
    +
    + +
    + + class + ServerMetrics(pyoutlineapi.common_types.BaseValidatedModel): + +
    - -
    221    @classmethod
    -222    @asynccontextmanager
    -223    async def create(
    -224            cls,
    -225            api_url: str,
    -226            cert_sha256: str,
    -227            **kwargs
    -228    ) -> AsyncGenerator[AsyncOutlineClient, None]:
    -229        """
    -230        Factory method that returns an async context manager.
    -231
    -232        This is the recommended way to create clients for one-off operations.
    -233        """
    -234        client = cls(api_url, cert_sha256, **kwargs)
    -235        async with client:
    -236            yield client
    +    
    +            
    301class ServerMetrics(BaseValidatedModel):
    +302    """Transfer metrics with optimized aggregations.
    +303
    +304    SCHEMA: Based on GET /metrics/transfer response
    +305    """
    +306
    +307    bytes_transferred_by_user_id: BytesPerUserDict = Field(
    +308        alias="bytesTransferredByUserId"
    +309    )
    +310
    +311    @cached_property
    +312    def total_bytes(self) -> int:
    +313        """Calculate total bytes with caching.
    +314
    +315        :return: Total bytes transferred
    +316        """
    +317        return sum(self.bytes_transferred_by_user_id.values())
    +318
    +319    @cached_property
    +320    def total_gigabytes(self) -> float:
    +321        """Get total in gigabytes (uses cached total_bytes).
    +322
    +323        :return: Total GB transferred
    +324        """
    +325        return self.total_bytes / _BYTES_IN_GB
    +326
    +327    @cached_property
    +328    def user_count(self) -> int:
    +329        """Get number of users (cached).
    +330
    +331        :return: Number of users
    +332        """
    +333        return len(self.bytes_transferred_by_user_id)
    +334
    +335    def get_user_bytes(self, user_id: str) -> int:
    +336        """Get bytes for specific user (O(1) dict lookup).
    +337
    +338        :param user_id: User/key ID
    +339        :return: Bytes transferred or 0 if not found
    +340        """
    +341        return self.bytes_transferred_by_user_id.get(user_id, 0)
    +342
    +343    def top_users(self, limit: int = 10) -> list[tuple[str, int]]:
    +344        """Get top users by bytes transferred (optimized sorting).
    +345
    +346        :param limit: Number of top users to return
    +347        :return: List of (user_id, bytes) tuples
    +348        """
    +349        return sorted(
    +350            self.bytes_transferred_by_user_id.items(),
    +351            key=lambda x: x[1],
    +352            reverse=True,
    +353        )[:limit]
     
    -

    Factory method that returns an async context manager.

    +

    Transfer metrics with optimized aggregations.

    -

    This is the recommended way to create clients for one-off operations.

    +

    SCHEMA: Based on GET /metrics/transfer response

    +
    +
    + bytes_transferred_by_user_id: dict[str, int] = +PydanticUndefined + + +
    + + + +
    -
    - -
    - - async def - health_check(self, force: bool = False) -> bool: +
    + +
    + total_bytes: int - +
    - -
    282    async def health_check(self, force: bool = False) -> bool:
    -283        """
    -284        Perform a health check on the Outline server.
    -285
    -286        Args:
    -287            force: Force health check even if recently performed
    -288
    -289        Returns:
    -290            True if server is healthy
    -291        """
    -292        current_time = time.time()
    -293
    -294        if not force and (current_time - self._last_health_check) < self._health_check_interval:
    -295            return self._is_healthy
    -296
    -297        try:
    -298            await self.get_server_info()
    -299            self._is_healthy = True
    -300            if self._enable_logging:
    -301                logger.info("Health check passed")
    -302
    -303            return self._is_healthy
    -304        except Exception as e:
    -305            self._is_healthy = False
    -306            if self._enable_logging:
    -307                logger.warning(f"Health check failed: {e}")
    -308
    -309            return self._is_healthy
    -310        finally:
    -311            self._last_health_check = current_time
    -
    - - -

    Perform a health check on the Outline server.

    + +
    311    @cached_property
    +312    def total_bytes(self) -> int:
    +313        """Calculate total bytes with caching.
    +314
    +315        :return: Total bytes transferred
    +316        """
    +317        return sum(self.bytes_transferred_by_user_id.values())
    +
    -
    Arguments:
    -
      -
    • force: Force health check even if recently performed
    • -
    +

    Calculate total bytes with caching.

    -
    Returns:
    +
    Returns
    -

    True if server is healthy

    +

    Total bytes transferred

    -
    - -
    -
    @log_method_call
    +
    + +
    + total_gigabytes: float - async def - get_server_info(self) -> Union[dict[str, Any], Server]: - - - -
    - -
    508    @log_method_call
    -509    async def get_server_info(self) -> Union[JsonDict, Server]:
    -510        """
    -511        Get server information.
    -512
    -513        Returns:
    -514            Server information including name, ID, and configuration.
    -515
    -516        Examples:
    -517            >>> async def main():
    -518            ...     async with AsyncOutlineClient(
    -519            ...         "https://example.com:1234/secret",
    -520            ...         "ab12cd34..."
    -521            ...     ) as client:
    -522            ...         server = await client.get_server_info()
    -523            ...         print(f"Server {server.name} running version {server.version}")
    -524        """
    -525        response = await self._request("GET", "server")
    -526        return await self._parse_response(
    -527            response, Server, json_format=self._json_format
    -528        )
    -
    - - -

    Get server information.

    + -
    Returns:
    +
    + +
    319    @cached_property
    +320    def total_gigabytes(self) -> float:
    +321        """Get total in gigabytes (uses cached total_bytes).
    +322
    +323        :return: Total GB transferred
    +324        """
    +325        return self.total_bytes / _BYTES_IN_GB
    +
    -
    -

    Server information including name, ID, and configuration.

    -
    -
    Examples:
    +

    Get total in gigabytes (uses cached total_bytes).

    + +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         server = await client.get_server_info()
    -...         print(f"Server {server.name} running version {server.version}")
    -
    -
    +

    Total GB transferred

    -
    - -
    -
    @log_method_call
    - - async def - rename_server(self, name: str) -> bool: - - - -
    - -
    530    @log_method_call
    -531    async def rename_server(self, name: str) -> bool:
    -532        """
    -533        Rename the server.
    -534
    -535        Args:
    -536            name: New server name
    -537
    -538        Returns:
    -539            True if successful
    -540
    -541        Examples:
    -542            >>> async def main():
    -543            ...     async with AsyncOutlineClient(
    -544            ...         "https://example.com:1234/secret",
    -545            ...         "ab12cd34..."
    -546            ...     ) as client:
    -547            ...         success = await client.rename_server("My VPN Server")
    -548            ...         if success:
    -549            ...             print("Server renamed successfully")
    -550        """
    -551        request = ServerNameRequest(name=name)
    -552        return await self._request(
    -553            "PUT", "name", json=request.model_dump(by_alias=True)
    -554        )
    -
    - - -

    Rename the server.

    +
    + +
    + user_count: int -
    Arguments:
    + -
      -
    • name: New server name
    • -
    +
    + +
    327    @cached_property
    +328    def user_count(self) -> int:
    +329        """Get number of users (cached).
    +330
    +331        :return: Number of users
    +332        """
    +333        return len(self.bytes_transferred_by_user_id)
    +
    -
    Returns:
    -
    -

    True if successful

    -
    +

    Get number of users (cached).

    -
    Examples:
    +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         success = await client.rename_server("My VPN Server")
    -...         if success:
    -...             print("Server renamed successfully")
    -
    -
    +

    Number of users

    -
    - +
    +
    -
    @log_method_call
    - - async def - set_hostname(self, hostname: str) -> bool: - - - -
    - -
    556    @log_method_call
    -557    async def set_hostname(self, hostname: str) -> bool:
    -558        """
    -559        Set server hostname for access keys.
    -560
    -561        Args:
    -562            hostname: New hostname or IP address
    -563
    -564        Returns:
    -565            True if successful
    -566
    -567        Raises:
    -568            APIError: If hostname is invalid
    -569
    -570        Examples:
    -571            >>> async def main():
    -572            ...     async with AsyncOutlineClient(
    -573            ...         "https://example.com:1234/secret",
    -574            ...         "ab12cd34..."
    -575            ...     ) as client:
    -576            ...         await client.set_hostname("vpn.example.com")
    -577            ...         # Or use IP address
    -578            ...         await client.set_hostname("203.0.113.1")
    -579        """
    -580        request = HostnameRequest(hostname=hostname)
    -581        return await self._request(
    -582            "PUT",
    -583            "server/hostname-for-access-keys",
    -584            json=request.model_dump(by_alias=True),
    -585        )
    -
    - - -

    Set server hostname for access keys.

    + + def + get_user_bytes(self, user_id: str) -> int: -
    Arguments:
    + -
      -
    • hostname: New hostname or IP address
    • -
    +
    + +
    335    def get_user_bytes(self, user_id: str) -> int:
    +336        """Get bytes for specific user (O(1) dict lookup).
    +337
    +338        :param user_id: User/key ID
    +339        :return: Bytes transferred or 0 if not found
    +340        """
    +341        return self.bytes_transferred_by_user_id.get(user_id, 0)
    +
    -
    Returns:
    -
    -

    True if successful

    -
    +

    Get bytes for specific user (O(1) dict lookup).

    -
    Raises:
    +
    Parameters
      -
    • APIError: If hostname is invalid
    • +
    • user_id: User/key ID
    -
    Examples:
    +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         await client.set_hostname("vpn.example.com")
    -...         # Or use IP address
    -...         await client.set_hostname("203.0.113.1")
    -
    -
    +

    Bytes transferred or 0 if not found

    -
    - +
    +
    -
    @log_method_call
    - - async def - set_default_port(self, port: int) -> bool: - - - -
    - -
    587    @log_method_call
    -588    async def set_default_port(self, port: int) -> bool:
    -589        """
    -590        Set default port for new access keys.
    -591
    -592        Args:
    -593            port: Port number (1025-65535)
    -594
    -595        Returns:
    -596            True if successful
    -597
    -598        Raises:
    -599            APIError: If port is invalid or in use
    -600
    -601        Examples:
    -602            >>> async def main():
    -603            ...     async with AsyncOutlineClient(
    -604            ...         "https://example.com:1234/secret",
    -605            ...         "ab12cd34..."
    -606            ...     ) as client:
    -607            ...         await client.set_default_port(8388)
    -608        """
    -609        if port < MIN_PORT or port > MAX_PORT:
    -610            raise ValueError(
    -611                f"Privileged ports are not allowed. Use range: {MIN_PORT}-{MAX_PORT}"
    -612            )
    -613
    -614        request = PortRequest(port=port)
    -615        return await self._request(
    -616            "PUT",
    -617            "server/port-for-new-access-keys",
    -618            json=request.model_dump(by_alias=True),
    -619        )
    -
    - - -

    Set default port for new access keys.

    + + def + top_users(self, limit: int = 10) -> list[tuple[str, int]]: -
    Arguments:
    + -
      -
    • port: Port number (1025-65535)
    • -
    +
    + +
    343    def top_users(self, limit: int = 10) -> list[tuple[str, int]]:
    +344        """Get top users by bytes transferred (optimized sorting).
    +345
    +346        :param limit: Number of top users to return
    +347        :return: List of (user_id, bytes) tuples
    +348        """
    +349        return sorted(
    +350            self.bytes_transferred_by_user_id.items(),
    +351            key=lambda x: x[1],
    +352            reverse=True,
    +353        )[:limit]
    +
    -
    Returns:
    -
    -

    True if successful

    -
    +

    Get top users by bytes transferred (optimized sorting).

    -
    Raises:
    +
    Parameters
      -
    • APIError: If port is invalid or in use
    • +
    • limit: Number of top users to return
    -
    Examples:
    +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         await client.set_default_port(8388)
    -
    -
    +

    List of (user_id, bytes) tuples

    -
    - -
    -
    @log_method_call
    +
    +
    + +
    + + class + ServerNameRequest(pyoutlineapi.common_types.BaseValidatedModel): - async def - get_metrics_status(self) -> Union[dict[str, Any], MetricsStatusResponse]: - - - -
    - -
    623    @log_method_call
    -624    async def get_metrics_status(self) -> Union[JsonDict, MetricsStatusResponse]:
    -625        """
    -626        Get whether metrics collection is enabled.
    -627
    -628        Returns:
    -629            Current metrics collection status
    -630
    -631        Examples:
    -632            >>> async def main():
    -633            ...     async with AsyncOutlineClient(
    -634            ...         "https://example.com:1234/secret",
    -635            ...         "ab12cd34..."
    -636            ...     ) as client:
    -637            ...         status = await client.get_metrics_status()
    -638            ...         if status.metrics_enabled:
    -639            ...             print("Metrics collection is enabled")
    -640        """
    -641        response = await self._request("GET", "metrics/enabled")
    -642        return await self._parse_response(
    -643            response, MetricsStatusResponse, json_format=self._json_format
    -644        )
    -
    - - -

    Get whether metrics collection is enabled.

    + -
    Returns:
    +
    + +
    497class ServerNameRequest(BaseValidatedModel):
    +498    """Request model for renaming server.
    +499
    +500    SCHEMA: Based on PUT /name request body
    +501    """
    +502
    +503    name: str = Field(min_length=1, max_length=255)
    +
    -
    -

    Current metrics collection status

    -
    -
    Examples:
    +

    Request model for renaming server.

    -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         status = await client.get_metrics_status()
    -...         if status.metrics_enabled:
    -...             print("Metrics collection is enabled")
    -
    -
    -
    +

    SCHEMA: Based on PUT /name request body

    - -
    - -
    -
    @log_method_call
    - - async def - set_metrics_status(self, enabled: bool) -> bool: - - - -
    - -
    646    @log_method_call
    -647    async def set_metrics_status(self, enabled: bool) -> bool:
    -648        """
    -649        Enable or disable metrics collection.
    -650
    -651        Args:
    -652            enabled: Whether to enable metrics
    -653
    -654        Returns:
    -655            True if successful
    -656
    -657        Examples:
    -658            >>> async def main():
    -659            ...     async with AsyncOutlineClient(
    -660            ...         "https://example.com:1234/secret",
    -661            ...         "ab12cd34..."
    -662            ...     ) as client:
    -663            ...         # Enable metrics
    -664            ...         await client.set_metrics_status(True)
    -665            ...         # Check new status
    -666            ...         status = await client.get_metrics_status()
    -667        """
    -668        request = MetricsEnabledRequest(metricsEnabled=enabled)
    -669        return await self._request(
    -670            "PUT", "metrics/enabled", json=request.model_dump(by_alias=True)
    -671        )
    -
    - - -

    Enable or disable metrics collection.

    +
    +
    + name: str = +PydanticUndefined -
    Arguments:
    + +
    + + + -
      -
    • enabled: Whether to enable metrics
    • -
    +
    +
    +
    + +
    + + class + ServerSummary(pyoutlineapi.common_types.BaseValidatedModel): -
    Returns:
    + -
    -

    True if successful

    -
    +
    + +
    626class ServerSummary(BaseValidatedModel):
    +627    """Server summary with optimized aggregations."""
    +628
    +629    server: dict[str, Any]
    +630    access_keys_count: int
    +631    healthy: bool
    +632    transfer_metrics: BytesPerUserDict | None = None
    +633    experimental_metrics: dict[str, Any] | None = None
    +634    error: str | None = None
    +635
    +636    @property
    +637    def total_bytes_transferred(self) -> int:
    +638        """Get total bytes with early return optimization.
    +639
    +640        :return: Total bytes or 0 if no metrics
    +641        """
    +642        if not self.transfer_metrics:
    +643            return 0  # Early return
    +644        return sum(self.transfer_metrics.values())
    +645
    +646    @property
    +647    def total_gigabytes_transferred(self) -> float:
    +648        """Get total GB (uses total_bytes_transferred).
    +649
    +650        :return: Total GB or 0.0 if no metrics
    +651        """
    +652        return self.total_bytes_transferred / _BYTES_IN_GB
    +653
    +654    @property
    +655    def has_errors(self) -> bool:
    +656        """Check if summary has errors (optimized None check).
    +657
    +658        :return: True if errors present
    +659        """
    +660        return self.error is not None
    +
    -
    Examples:
    -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         # Enable metrics
    -...         await client.set_metrics_status(True)
    -...         # Check new status
    -...         status = await client.get_metrics_status()
    -
    -
    -
    +

    Server summary with optimized aggregations.

    - -
    - -
    -
    @log_method_call
    - - async def - get_transfer_metrics(self) -> Union[dict[str, Any], ServerMetrics]: - - - -
    - -
    673    @log_method_call
    -674    async def get_transfer_metrics(self) -> Union[JsonDict, ServerMetrics]:
    -675        """
    -676        Get transfer metrics for all access keys.
    -677
    -678        Returns:
    -679            Transfer metrics data for each access key
    -680
    -681        Examples:
    -682            >>> async def main():
    -683            ...     async with AsyncOutlineClient(
    -684            ...         "https://example.com:1234/secret",
    -685            ...         "ab12cd34..."
    -686            ...     ) as client:
    -687            ...         metrics = await client.get_transfer_metrics()
    -688            ...         for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items():
    -689            ...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    -690        """
    -691        response = await self._request("GET", "metrics/transfer")
    -692        return await self._parse_response(
    -693            response, ServerMetrics, json_format=self._json_format
    -694        )
    -
    - - -

    Get transfer metrics for all access keys.

    - -
    Returns:
    - -
    -

    Transfer metrics data for each access key

    -
    - -
    Examples:
    - -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         metrics = await client.get_transfer_metrics()
    -...         for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items():
    -...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")
    -
    -
    -
    -
    +
    +
    + server: dict[str, typing.Any] = +PydanticUndefined + +
    + + +
    -
    - -
    -
    @log_method_call
    - - async def - get_experimental_metrics( self, since: str) -> Union[dict[str, Any], ExperimentalMetrics]: - - - -
    - -
    696    @log_method_call
    -697    async def get_experimental_metrics(
    -698            self, since: str
    -699    ) -> Union[JsonDict, ExperimentalMetrics]:
    -700        """
    -701        Get experimental server metrics.
    -702
    -703        Args:
    -704            since: Required time range filter (e.g., "24h", "7d", "30d", or ISO timestamp)
    -705
    -706        Returns:
    -707            Detailed server and access key metrics
    -708
    -709        Examples:
    -710            >>> async def main():
    -711            ...     async with AsyncOutlineClient(
    -712            ...         "https://example.com:1234/secret",
    -713            ...         "ab12cd34..."
    -714            ...     ) as client:
    -715            ...         # Get metrics for the last 24 hours
    -716            ...         metrics = await client.get_experimental_metrics("24h")
    -717            ...         print(f"Server tunnel time: {metrics.server.tunnel_time.seconds}s")
    -718            ...         print(f"Server data transferred: {metrics.server.data_transferred.bytes} bytes")
    -719            ...
    -720            ...         # Get metrics for the last 7 days
    -721            ...         metrics = await client.get_experimental_metrics("7d")
    -722            ...
    -723            ...         # Get metrics since specific timestamp
    -724            ...         metrics = await client.get_experimental_metrics("2024-01-01T00:00:00Z")
    -725        """
    -726        if not since or not since.strip():
    -727            raise ValueError("Parameter 'since' is required and cannot be empty")
    -728
    -729        params = {"since": since}
    -730        response = await self._request(
    -731            "GET", "experimental/server/metrics", params=params
    -732        )
    -733        return await self._parse_response(
    -734            response, ExperimentalMetrics, json_format=self._json_format
    -735        )
    -
    - - -

    Get experimental server metrics.

    +
    +
    + access_keys_count: int = +PydanticUndefined -
    Arguments:
    + +
    + + + -
      -
    • since: Required time range filter (e.g., "24h", "7d", "30d", or ISO timestamp)
    • -
    +
    +
    +
    + healthy: bool = +PydanticUndefined -
    Returns:
    + +
    + + + -
    -

    Detailed server and access key metrics

    -
    +
    +
    +
    + transfer_metrics: dict[str, int] | None = +None -
    Examples:
    + +
    + + + -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         # Get metrics for the last 24 hours
    -...         metrics = await client.get_experimental_metrics("24h")
    -...         print(f"Server tunnel time: {metrics.server.tunnel_time.seconds}s")
    -...         print(f"Server data transferred: {metrics.server.data_transferred.bytes} bytes")
    -...
    -...         # Get metrics for the last 7 days
    -...         metrics = await client.get_experimental_metrics("7d")
    -...
    -...         # Get metrics since specific timestamp
    -...         metrics = await client.get_experimental_metrics("2024-01-01T00:00:00Z")
    -
    -
    -
    -
    +
    +
    +
    + experimental_metrics: dict[str, typing.Any] | None = +None + +
    + + +
    -
    - -
    -
    @log_method_call
    +
    +
    + error: str | None = +None - async def - create_access_key( self, *, name: Optional[str] = None, password: Optional[str] = None, port: Optional[int] = None, method: Optional[str] = None, limit: Optional[DataLimit] = None) -> Union[dict[str, Any], AccessKey]: - - - -
    - -
    739    @log_method_call
    -740    async def create_access_key(
    -741            self,
    -742            *,
    -743            name: Optional[str] = None,
    -744            password: Optional[str] = None,
    -745            port: Optional[int] = None,
    -746            method: Optional[str] = None,
    -747            limit: Optional[DataLimit] = None,
    -748    ) -> Union[JsonDict, AccessKey]:
    -749        """
    -750        Create a new access key.
    -751
    -752        Args:
    -753            name: Optional key name
    -754            password: Optional password
    -755            port: Optional port number (1-65535)
    -756            method: Optional encryption method
    -757            limit: Optional data transfer limit
    -758
    -759        Returns:
    -760            New access key details
    -761
    -762        Examples:
    -763            >>> async def main():
    -764            ...     async with AsyncOutlineClient(
    -765            ...         "https://example.com:1234/secret",
    -766            ...         "ab12cd34..."
    -767            ...     ) as client:
    -768            ...         # Create basic key
    -769            ...         key = await client.create_access_key(name="User 1")
    -770            ...
    -771            ...         # Create key with data limit
    -772            ...         lim = DataLimit(bytes=5 * 1024**3)  # 5 GB
    -773            ...         key = await client.create_access_key(
    -774            ...             name="Limited User",
    -775            ...             port=8388,
    -776            ...             limit=lim
    -777            ...         )
    -778            ...         print(f"Created key: {key.access_url}")
    -779        """
    -780        request = AccessKeyCreateRequest(
    -781            name=name, password=password, port=port, method=method, limit=limit
    -782        )
    -783        response = await self._request(
    -784            "POST",
    -785            "access-keys",
    -786            json=request.model_dump(exclude_none=True, by_alias=True),
    -787        )
    -788        return await self._parse_response(
    -789            response, AccessKey, json_format=self._json_format
    -790        )
    -
    - - -

    Create a new access key.

    + +
    + + + -
    Arguments:
    +
    +
    + +
    + total_bytes_transferred: int -
      -
    • name: Optional key name
    • -
    • password: Optional password
    • -
    • port: Optional port number (1-65535)
    • -
    • method: Optional encryption method
    • -
    • limit: Optional data transfer limit
    • -
    + -
    Returns:
    +
    + +
    636    @property
    +637    def total_bytes_transferred(self) -> int:
    +638        """Get total bytes with early return optimization.
    +639
    +640        :return: Total bytes or 0 if no metrics
    +641        """
    +642        if not self.transfer_metrics:
    +643            return 0  # Early return
    +644        return sum(self.transfer_metrics.values())
    +
    -
    -

    New access key details

    -
    -
    Examples:
    +

    Get total bytes with early return optimization.

    + +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         # Create basic key
    -...         key = await client.create_access_key(name="User 1")
    -...
    -...         # Create key with data limit
    -...         lim = DataLimit(bytes=5 * 1024**3)  # 5 GB
    -...         key = await client.create_access_key(
    -...             name="Limited User",
    -...             port=8388,
    -...             limit=lim
    -...         )
    -...         print(f"Created key: {key.access_url}")
    -
    -
    +

    Total bytes or 0 if no metrics

    -
    - -
    -
    @log_method_call
    - - async def - create_access_key_with_id( self, key_id: str, *, name: Optional[str] = None, password: Optional[str] = None, port: Optional[int] = None, method: Optional[str] = None, limit: Optional[DataLimit] = None) -> Union[dict[str, Any], AccessKey]: - - - -
    - -
    792    @log_method_call
    -793    async def create_access_key_with_id(
    -794            self,
    -795            key_id: str,
    -796            *,
    -797            name: Optional[str] = None,
    -798            password: Optional[str] = None,
    -799            port: Optional[int] = None,
    -800            method: Optional[str] = None,
    -801            limit: Optional[DataLimit] = None,
    -802    ) -> Union[JsonDict, AccessKey]:
    -803        """
    -804        Create a new access key with specific ID.
    -805
    -806        Args:
    -807            key_id: Specific ID for the access key
    -808            name: Optional key name
    -809            password: Optional password
    -810            port: Optional port number (1-65535)
    -811            method: Optional encryption method
    -812            limit: Optional data transfer limit
    -813
    -814        Returns:
    -815            New access key details
    -816
    -817        Examples:
    -818            >>> async def main():
    -819            ...     async with AsyncOutlineClient(
    -820            ...         "https://example.com:1234/secret",
    -821            ...         "ab12cd34..."
    -822            ...     ) as client:
    -823            ...         key = await client.create_access_key_with_id(
    -824            ...             "my-custom-id",
    -825            ...             name="Custom Key"
    -826            ...         )
    -827        """
    -828        request = AccessKeyCreateRequest(
    -829            name=name, password=password, port=port, method=method, limit=limit
    -830        )
    -831        response = await self._request(
    -832            "PUT",
    -833            f"access-keys/{key_id}",
    -834            json=request.model_dump(exclude_none=True, by_alias=True),
    -835        )
    -836        return await self._parse_response(
    -837            response, AccessKey, json_format=self._json_format
    -838        )
    -
    - - -

    Create a new access key with specific ID.

    +
    + +
    + total_gigabytes_transferred: float -
    Arguments:
    + -
      -
    • key_id: Specific ID for the access key
    • -
    • name: Optional key name
    • -
    • password: Optional password
    • -
    • port: Optional port number (1-65535)
    • -
    • method: Optional encryption method
    • -
    • limit: Optional data transfer limit
    • -
    +
    + +
    646    @property
    +647    def total_gigabytes_transferred(self) -> float:
    +648        """Get total GB (uses total_bytes_transferred).
    +649
    +650        :return: Total GB or 0.0 if no metrics
    +651        """
    +652        return self.total_bytes_transferred / _BYTES_IN_GB
    +
    -
    Returns:
    -
    -

    New access key details

    -
    +

    Get total GB (uses total_bytes_transferred).

    -
    Examples:
    +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         key = await client.create_access_key_with_id(
    -...             "my-custom-id",
    -...             name="Custom Key"
    -...         )
    -
    -
    +

    Total GB or 0.0 if no metrics

    -
    - -
    -
    @log_method_call
    +
    + +
    + has_errors: bool - async def - get_access_keys(self) -> Union[dict[str, Any], AccessKeyList]: - - - -
    - -
    840    @log_method_call
    -841    async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]:
    -842        """
    -843        Get all access keys.
    -844
    -845        Returns:
    -846            List of all access keys
    -847
    -848        Examples:
    -849            >>> async def main():
    -850            ...     async with AsyncOutlineClient(
    -851            ...         "https://example.com:1234/secret",
    -852            ...         "ab12cd34..."
    -853            ...     ) as client:
    -854            ...         keys = await client.get_access_keys()
    -855            ...         for key in keys.access_keys:
    -856            ...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    -857            ...             if key.data_limit:
    -858            ...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    -859        """
    -860        response = await self._request("GET", "access-keys")
    -861        return await self._parse_response(
    -862            response, AccessKeyList, json_format=self._json_format
    -863        )
    -
    - - -

    Get all access keys.

    + -
    Returns:
    +
    + +
    654    @property
    +655    def has_errors(self) -> bool:
    +656        """Check if summary has errors (optimized None check).
    +657
    +658        :return: True if errors present
    +659        """
    +660        return self.error is not None
    +
    -
    -

    List of all access keys

    -
    -
    Examples:
    +

    Check if summary has errors (optimized None check).

    + +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         keys = await client.get_access_keys()
    -...         for key in keys.access_keys:
    -...             print(f"Key {key.id}: {key.name or 'unnamed'}")
    -...             if key.data_limit:
    -...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")
    -
    -
    +

    True if errors present

    -
    - -
    -
    @log_method_call
    +
    +
    +
    + TimestampMs = + + typing.Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in milliseconds', metadata=[Ge(ge=0)])] - async def - get_access_key( self, key_id: str) -> Union[dict[str, Any], AccessKey]: - - - -
    - -
    865    @log_method_call
    -866    async def get_access_key(self, key_id: str) -> Union[JsonDict, AccessKey]:
    -867        """
    -868        Get specific access key.
    -869
    -870        Args:
    -871            key_id: Access key ID
    -872
    -873        Returns:
    -874            Access key details
    -875
    -876        Raises:
    -877            APIError: If key doesn't exist
    -878
    -879        Examples:
    -880            >>> async def main():
    -881            ...     async with AsyncOutlineClient(
    -882            ...         "https://example.com:1234/secret",
    -883            ...         "ab12cd34..."
    -884            ...     ) as client:
    -885            ...         key = await client.get_access_key("1")
    -886            ...         print(f"Port: {key.port}")
    -887            ...         print(f"URL: {key.access_url}")
    -888        """
    -889        response = await self._request("GET", f"access-keys/{key_id}")
    -890        return await self._parse_response(
    -891            response, AccessKey, json_format=self._json_format
    -892        )
    -
    - - -

    Get specific access key.

    + +
    + + + -
    Arguments:
    +
    +
    +
    + TimestampSec = + + typing.Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in seconds', metadata=[Ge(ge=0)])] -
      -
    • key_id: Access key ID
    • -
    + +
    + + + -
    Returns:
    +
    +
    + +
    + + class + TunnelTime(pyoutlineapi.common_types.BaseValidatedModel, pyoutlineapi.models.TimeConversionMixin): -
    -

    Access key details

    -
    + -
    Raises:
    +
    + +
    356class TunnelTime(BaseValidatedModel, TimeConversionMixin):
    +357    """Tunnel time metric with time conversions.
    +358
    +359    SCHEMA: Based on experimental metrics tunnelTime object
    +360    """
    +361
    +362    seconds: int = Field(ge=0)
    +
    -
      -
    • APIError: If key doesn't exist
    • -
    -
    Examples:
    +

    Tunnel time metric with time conversions.

    -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         key = await client.get_access_key("1")
    -...         print(f"Port: {key.port}")
    -...         print(f"URL: {key.access_url}")
    -
    -
    -
    +

    SCHEMA: Based on experimental metrics tunnelTime object

    +
    +
    + seconds: int = +PydanticUndefined + + +
    + + + +
    -
    - -
    -
    @log_method_call
    +
    +
    + +
    + + class + ValidationError(pyoutlineapi.OutlineError): - async def - rename_access_key(self, key_id: str, name: str) -> bool: - - - -
    - -
    894    @log_method_call
    -895    async def rename_access_key(self, key_id: str, name: str) -> bool:
    -896        """
    -897        Rename access key.
    -898
    -899        Args:
    -900            key_id: Access key ID
    -901            name: New name
    -902
    -903        Returns:
    -904            True if successful
    -905
    -906        Raises:
    -907            APIError: If key doesn't exist
    -908
    -909        Examples:
    -910            >>> async def main():
    -911            ...     async with AsyncOutlineClient(
    -912            ...         "https://example.com:1234/secret",
    -913            ...         "ab12cd34..."
    -914            ...     ) as client:
    -915            ...         # Rename key
    -916            ...         await client.rename_access_key("1", "Alice")
    -917            ...
    -918            ...         # Verify new name
    -919            ...         key = await client.get_access_key("1")
    -920            ...         assert key.name == "Alice"
    -921        """
    -922        request = AccessKeyNameRequest(name=name)
    -923        return await self._request(
    -924            "PUT", f"access-keys/{key_id}/name", json=request.model_dump(by_alias=True)
    -925        )
    -
    - - -

    Rename access key.

    + -
    Arguments:
    +
    + +
    358class ValidationError(OutlineError):
    +359    """Data validation failure.
    +360
    +361    Raised when data fails validation against expected schema.
    +362
    +363    Attributes:
    +364        field: Field name that failed validation
    +365        model: Model name
    +366
    +367    Example:
    +368        >>> error = ValidationError(
    +369        ...     "Invalid port number", field="port", model="ServerConfig"
    +370        ... )
    +371    """
    +372
    +373    __slots__ = ("field", "model")
    +374
    +375    def __init__(
    +376        self,
    +377        message: str,
    +378        *,
    +379        field: str | None = None,
    +380        model: str | None = None,
    +381    ) -> None:
    +382        """Initialize validation error.
    +383
    +384        Args:
    +385            message: Error message
    +386            field: Field name that failed validation
    +387            model: Model name
    +388        """
    +389        safe_details: dict[str, Any] | None = None
    +390        if field or model:
    +391            safe_details = {}
    +392            if field:
    +393                safe_details["field"] = field
    +394            if model:
    +395                safe_details["model"] = model
    +396
    +397        super().__init__(message, safe_details=safe_details)
    +398
    +399        self.field = field
    +400        self.model = model
    +
    -
      -
    • key_id: Access key ID
    • -
    • name: New name
    • -
    -
    Returns:
    +

    Data validation failure.

    -
    -

    True if successful

    -
    +

    Raised when data fails validation against expected schema.

    -
    Raises:
    +
    Attributes:
      -
    • APIError: If key doesn't exist
    • +
    • field: Field name that failed validation
    • +
    • model: Model name
    -
    Examples:
    +
    Example:
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         # Rename key
    -...         await client.rename_access_key("1", "Alice")
    -...
    -...         # Verify new name
    -...         key = await client.get_access_key("1")
    -...         assert key.name == "Alice"
    +
    >>> error = ValidationError(
    +...     "Invalid port number", field="port", model="ServerConfig"
    +... )
     
    - -
    - +
    +
    -
    @log_method_call
    - - async def - delete_access_key(self, key_id: str) -> bool: - - - -
    - -
    927    @log_method_call
    -928    async def delete_access_key(self, key_id: str) -> bool:
    -929        """
    -930        Delete access key.
    -931
    -932        Args:
    -933            key_id: Access key ID
    -934
    -935        Returns:
    -936            True if successful
    -937
    -938        Raises:
    -939            APIError: If key doesn't exist
    -940
    -941        Examples:
    -942            >>> async def main():
    -943            ...     async with AsyncOutlineClient(
    -944            ...         "https://example.com:1234/secret",
    -945            ...         "ab12cd34..."
    -946            ...     ) as client:
    -947            ...         if await client.delete_access_key("1"):
    -948            ...             print("Key deleted")
    -949        """
    -950        return await self._request("DELETE", f"access-keys/{key_id}")
    -
    - - -

    Delete access key.

    + + ValidationError(message: str, *, field: str | None = None, model: str | None = None) -
    Arguments:
    + -
      -
    • key_id: Access key ID
    • -
    +
    + +
    375    def __init__(
    +376        self,
    +377        message: str,
    +378        *,
    +379        field: str | None = None,
    +380        model: str | None = None,
    +381    ) -> None:
    +382        """Initialize validation error.
    +383
    +384        Args:
    +385            message: Error message
    +386            field: Field name that failed validation
    +387            model: Model name
    +388        """
    +389        safe_details: dict[str, Any] | None = None
    +390        if field or model:
    +391            safe_details = {}
    +392            if field:
    +393                safe_details["field"] = field
    +394            if model:
    +395                safe_details["model"] = model
    +396
    +397        super().__init__(message, safe_details=safe_details)
    +398
    +399        self.field = field
    +400        self.model = model
    +
    -
    Returns:
    -
    -

    True if successful

    -
    +

    Initialize validation error.

    -
    Raises:
    +
    Arguments:
      -
    • APIError: If key doesn't exist
    • +
    • message: Error message
    • +
    • field: Field name that failed validation
    • +
    • model: Model name
    - -
    Examples:
    - -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         if await client.delete_access_key("1"):
    -...             print("Key deleted")
    -
    -
    -
    -
    - -
    -
    @log_method_call
    - - async def - set_access_key_data_limit(self, key_id: str, bytes_limit: int) -> bool: - - - -
    - -
    952    @log_method_call
    -953    async def set_access_key_data_limit(self, key_id: str, bytes_limit: int) -> bool:
    -954        """
    -955        Set data transfer limit for access key.
    -956
    -957        Args:
    -958            key_id: Access key ID
    -959            bytes_limit: Limit in bytes (must be non-negative)
    -960
    -961        Returns:
    -962            True if successful
    -963
    -964        Raises:
    -965            APIError: If key doesn't exist or limit is invalid
    -966
    -967        Examples:
    -968            >>> async def main():
    -969            ...     async with AsyncOutlineClient(
    -970            ...         "https://example.com:1234/secret",
    -971            ...         "ab12cd34..."
    -972            ...     ) as client:
    -973            ...         # Set 5 GB limit
    -974            ...         limit = 5 * 1024**3  # 5 GB in bytes
    -975            ...         await client.set_access_key_data_limit("1", limit)
    -976            ...
    -977            ...         # Verify limit
    -978            ...         key = await client.get_access_key("1")
    -979            ...         assert key.data_limit and key.data_limit.bytes == limit
    -980        """
    -981        request = DataLimitRequest(limit=DataLimit(bytes=bytes_limit))
    -982        return await self._request(
    -983            "PUT",
    -984            f"access-keys/{key_id}/data-limit",
    -985            json=request.model_dump(by_alias=True),
    -986        )
    -
    - - -

    Set data transfer limit for access key.

    +
    +
    + field -
    Arguments:
    + +
    + + + -
      -
    • key_id: Access key ID
    • -
    • bytes_limit: Limit in bytes (must be non-negative)
    • -
    +
    +
    +
    + model -
    Returns:
    + +
    + + + -
    -

    True if successful

    -
    +
    +
    +
    + +
    + + class + Validators: -
    Raises:
    + -
      -
    • APIError: If key doesn't exist or limit is invalid
    • -
    +
    + +
    392class Validators:
    +393    """Input validation utilities with security hardening."""
    +394
    +395    __slots__ = ()
    +396
    +397    @staticmethod
    +398    @lru_cache(maxsize=64)
    +399    def validate_cert_fingerprint(fingerprint: SecretStr) -> SecretStr:
    +400        """Validate and normalize certificate fingerprint.
    +401
    +402        :param fingerprint: SHA-256 fingerprint
    +403        :return: Normalized fingerprint (lowercase, no separators)
    +404        :raises ValueError: If format is invalid
    +405        """
    +406        if not fingerprint:
    +407            raise ValueError("Certificate fingerprint cannot be empty")
    +408
    +409        # Remove common separators
    +410        cleaned = fingerprint.get_secret_value().lower()
    +411
    +412        # Validate hex format
    +413        if not re.match(r"^[a-f0-9]{64}$", cleaned):
    +414            raise ValueError(
    +415                f"Invalid certificate fingerprint format. "
    +416                f"Expected 64 hex characters, got: {len(cleaned)}"
    +417            )
    +418
    +419        return SecretStr(cleaned)
    +420
    +421    @staticmethod
    +422    def validate_port(port: int) -> int:
    +423        """Validate port number.
    +424
    +425        :param port: Port number
    +426        :return: Validated port
    +427        :raises ValueError: If port is out of range
    +428        """
    +429        if not is_valid_port(port):
    +430            raise ValueError(
    +431                f"Port must be between {Constants.MIN_PORT} and {Constants.MAX_PORT}"
    +432            )
    +433        return port
    +434
    +435    @staticmethod
    +436    def validate_name(name: str) -> str:
    +437        """Validate name field.
    +438
    +439        :param name: Name to validate
    +440        :return: Validated name
    +441        :raises ValueError: If name is invalid
    +442        """
    +443        if not name or not name.strip():
    +444            raise ValueError("Name cannot be empty")
    +445
    +446        name = name.strip()
    +447        if len(name) > Constants.MAX_NAME_LENGTH:
    +448            raise ValueError(
    +449                f"Name too long: {len(name)} (max {Constants.MAX_NAME_LENGTH})"
    +450            )
    +451
    +452        return name
    +453
    +454    @staticmethod
    +455    def validate_url(
    +456        url: str,
    +457        *,
    +458        allow_private_networks: bool = True,
    +459        resolve_dns: bool = False,
    +460    ) -> str:
    +461        """Validate and sanitize URL.
    +462
    +463        :param url: URL to validate
    +464        :param allow_private_networks: Allow private/local network addresses
    +465        :param resolve_dns: Resolve hostname and block private/reserved IPs
    +466        :return: Validated URL
    +467        :raises ValueError: If URL is invalid
    +468        """
    +469        if not url or not url.strip():
    +470            raise ValueError("URL cannot be empty")
    +471
    +472        url = url.strip()
    +473
    +474        if len(url) > Constants.MAX_URL_LENGTH:
    +475            raise ValueError(
    +476                f"URL too long: {len(url)} (max {Constants.MAX_URL_LENGTH})"
    +477            )
    +478
    +479        # Check for null bytes
    +480        if "\x00" in url:
    +481            raise ValueError("URL contains null bytes")
    +482
    +483        # Parse URL
    +484        try:
    +485            parsed = urlparse(url)
    +486            if not parsed.scheme or not parsed.netloc:
    +487                raise ValueError("Invalid URL format")
    +488        except Exception as e:
    +489            raise ValueError(f"Invalid URL: {e}") from e
    +490
    +491        # SSRF protection for raw IPs in hostname (does not resolve DNS)
    +492        if (
    +493            not allow_private_networks
    +494            and parsed.hostname
    +495            and SSRFProtection.is_blocked_ip(parsed.hostname)
    +496        ):
    +497            raise ValueError(
    +498                f"Access to {parsed.hostname} is blocked (SSRF protection)"
    +499            )
    +500
    +501        # Strict SSRF protection with DNS resolution (guards against rebinding)
    +502        if (
    +503            resolve_dns
    +504            and not allow_private_networks
    +505            and parsed.hostname
    +506            and not SSRFProtection.is_blocked_ip(parsed.hostname)
    +507            and SSRFProtection.is_blocked_hostname(parsed.hostname)
    +508        ):
    +509            raise ValueError(
    +510                f"Access to {parsed.hostname} is blocked (SSRF protection)"
    +511            )
    +512
    +513        return url
    +514
    +515    @staticmethod
    +516    def validate_string_not_empty(value: str, field_name: str) -> str:
    +517        """Validate string is not empty.
    +518
    +519        :param value: String value
    +520        :param field_name: Field name for error messages
    +521        :return: Stripped string
    +522        :raises ValueError: If string is empty
    +523        """
    +524        if not value or not value.strip():
    +525            raise ValueError(f"{field_name} cannot be empty")
    +526        return value.strip()
    +527
    +528    @staticmethod
    +529    def _validate_length(value: str, max_length: int, name: str) -> None:
    +530        """Validate string length.
    +531
    +532        :param value: String value
    +533        :param max_length: Maximum allowed length
    +534        :param name: Field name for error messages
    +535        :raises ValueError: If string is too long
    +536        """
    +537        if len(value) > max_length:
    +538            raise ValueError(f"{name} too long: {len(value)} (max {max_length})")
    +539
    +540    @staticmethod
    +541    def _validate_no_null_bytes(value: str, name: str) -> None:
    +542        """Validate string contains no null bytes.
    +543
    +544        :param value: String value
    +545        :param name: Field name for error messages
    +546        :raises ValueError: If string contains null bytes
    +547        """
    +548        if "\x00" in value:
    +549            raise ValueError(f"{name} contains null bytes")
    +550
    +551    @staticmethod
    +552    def validate_non_negative(value: DataLimit | int, name: str) -> int:
    +553        """Validate integer is non-negative.
    +554
    +555        :param value: Integer value
    +556        :param name: Field name for error messages
    +557        :return: Validated value
    +558        :raises ValueError: If value is negative
    +559        """
    +560        from .models import DataLimit
    +561
    +562        raw_value = value.bytes if isinstance(value, DataLimit) else value
    +563        if raw_value < 0:
    +564            raise ValueError(f"{name} must be non-negative, got {raw_value}")
    +565        return raw_value
    +566
    +567    @staticmethod
    +568    def validate_since(value: str) -> str:
    +569        """Validate experimental metrics 'since' parameter.
    +570
    +571        Accepts:
    +572        - Relative durations: 24h, 7d, 30m, 15s
    +573        - ISO-8601 timestamps (e.g., 2024-01-01T00:00:00Z)
    +574
    +575        :param value: Since parameter
    +576        :return: Sanitized since value
    +577        :raises ValueError: If value is invalid
    +578        """
    +579        if not value or not value.strip():
    +580            raise ValueError("'since' parameter cannot be empty")
    +581
    +582        sanitized = value.strip()
    +583
    +584        # Relative format (number + suffix)
    +585        if len(sanitized) >= 2 and sanitized[-1] in {"h", "d", "m", "s"}:
    +586            number = sanitized[:-1]
    +587            if number.isdigit():
    +588                return sanitized
    +589
    +590        # ISO-8601 timestamp (allow trailing Z)
    +591        iso_value = sanitized.replace("Z", "+00:00")
    +592        try:
    +593            datetime.fromisoformat(iso_value)
    +594            return sanitized
    +595        except ValueError:
    +596            raise ValueError(
    +597                "'since' must be a relative duration (e.g., '24h', '7d') "
    +598                "or ISO-8601 timestamp"
    +599            ) from None
    +600
    +601    @classmethod
    +602    @lru_cache(maxsize=256)
    +603    def validate_key_id(cls, key_id: str) -> str:
    +604        """Enhanced key_id validation.
    +605
    +606        :param key_id: Key ID to validate
    +607        :return: Validated key ID
    +608        :raises ValueError: If key ID is invalid
    +609        """
    +610        clean_id = cls.validate_string_not_empty(key_id, "key_id")
    +611        cls._validate_length(clean_id, Constants.MAX_KEY_ID_LENGTH, "key_id")
    +612        cls._validate_no_null_bytes(clean_id, "key_id")
    +613
    +614        try:
    +615            decoded = urllib.parse.unquote(clean_id)
    +616            double_decoded = urllib.parse.unquote(decoded)
    +617
    +618            # Check all variants for malicious characters
    +619            for variant in [clean_id, decoded, double_decoded]:
    +620                if any(c in variant for c in {".", "/", "\\", "%", "\x00"}):
    +621                    raise ValueError(
    +622                        "key_id contains invalid characters (., /, \\, %, null)"
    +623                    )
    +624        except Exception as e:
    +625            raise ValueError(f"Invalid key_id encoding: {e}") from e
    +626
    +627        # Strict whitelist approach
    +628        allowed_chars = frozenset(
    +629            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
    +630        )
    +631        if not all(c in allowed_chars for c in clean_id):
    +632            raise ValueError("key_id must be alphanumeric, dashes, underscores only")
    +633
    +634        return clean_id
    +635
    +636    @staticmethod
    +637    @lru_cache(maxsize=256)
    +638    def sanitize_url_for_logging(url: str) -> str:
    +639        """Remove secret path from URL for safe logging.
    +640
    +641        :param url: URL to sanitize
    +642        :return: Sanitized URL
    +643        """
    +644        try:
    +645            parsed = urlparse(url)
    +646            return f"{parsed.scheme}://{parsed.netloc}/***"
    +647        except Exception:
    +648            return "***INVALID_URL***"
    +649
    +650    @staticmethod
    +651    @lru_cache(maxsize=512)
    +652    def sanitize_endpoint_for_logging(endpoint: str) -> str:
    +653        """Sanitize endpoint for safe logging.
    +654
    +655        :param endpoint: Endpoint to sanitize
    +656        :return: Sanitized endpoint
    +657        """
    +658        if not endpoint:
    +659            return "***EMPTY***"
    +660
    +661        parts = endpoint.split("/")
    +662        sanitized = [part if len(part) <= 20 else "***" for part in parts]
    +663        return "/".join(sanitized)
    +
    -
    Examples:
    -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         # Set 5 GB limit
    -...         limit = 5 * 1024**3  # 5 GB in bytes
    -...         await client.set_access_key_data_limit("1", limit)
    -...
    -...         # Verify limit
    -...         key = await client.get_access_key("1")
    -...         assert key.data_limit and key.data_limit.bytes == limit
    -
    -
    -
    +

    Input validation utilities with security hardening.

    - -
    - +
    +
    -
    @log_method_call
    +
    @staticmethod
    +
    @lru_cache(maxsize=64)
    - async def - remove_access_key_data_limit(self, key_id: str) -> bool: - - - -
    - -
     988    @log_method_call
    - 989    async def remove_access_key_data_limit(self, key_id: str) -> bool:
    - 990        """
    - 991        Remove data transfer limit from access key.
    - 992
    - 993        Args:
    - 994            key_id: Access key ID
    - 995
    - 996        Returns:
    - 997            True if successful
    - 998
    - 999        Raises:
    -1000            APIError: If key doesn't exist
    -1001
    -1002        Examples:
    -1003            >>> async def main():
    -1004            ...     async with AsyncOutlineClient(
    -1005            ...         "https://example.com:1234/secret",
    -1006            ...         "ab12cd34..."
    -1007            ...     ) as client:
    -1008            ...         await client.remove_access_key_data_limit("1")
    -1009        """
    -1010        return await self._request("DELETE", f"access-keys/{key_id}/data-limit")
    -
    - - -

    Remove data transfer limit from access key.

    + def + validate_cert_fingerprint(fingerprint: pydantic.types.SecretStr) -> pydantic.types.SecretStr: -
    Arguments:
    + -
      -
    • key_id: Access key ID
    • -
    +
    + +
    397    @staticmethod
    +398    @lru_cache(maxsize=64)
    +399    def validate_cert_fingerprint(fingerprint: SecretStr) -> SecretStr:
    +400        """Validate and normalize certificate fingerprint.
    +401
    +402        :param fingerprint: SHA-256 fingerprint
    +403        :return: Normalized fingerprint (lowercase, no separators)
    +404        :raises ValueError: If format is invalid
    +405        """
    +406        if not fingerprint:
    +407            raise ValueError("Certificate fingerprint cannot be empty")
    +408
    +409        # Remove common separators
    +410        cleaned = fingerprint.get_secret_value().lower()
    +411
    +412        # Validate hex format
    +413        if not re.match(r"^[a-f0-9]{64}$", cleaned):
    +414            raise ValueError(
    +415                f"Invalid certificate fingerprint format. "
    +416                f"Expected 64 hex characters, got: {len(cleaned)}"
    +417            )
    +418
    +419        return SecretStr(cleaned)
    +
    -
    Returns:
    -
    -

    True if successful

    -
    +

    Validate and normalize certificate fingerprint.

    -
    Raises:
    +
    Parameters
      -
    • APIError: If key doesn't exist
    • +
    • fingerprint: SHA-256 fingerprint
    -
    Examples:
    +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         await client.remove_access_key_data_limit("1")
    -
    -
    +

    Normalized fingerprint (lowercase, no separators)

    + +
    Raises
    + +
      +
    • ValueError: If format is invalid
    • +
    -
    - +
    +
    -
    @log_method_call
    +
    @staticmethod
    - async def - set_global_data_limit(self, bytes_limit: int) -> bool: - - - -
    - -
    1014    @log_method_call
    -1015    async def set_global_data_limit(self, bytes_limit: int) -> bool:
    -1016        """
    -1017        Set global data transfer limit for all access keys.
    -1018
    -1019        Args:
    -1020            bytes_limit: Limit in bytes (must be non-negative)
    -1021
    -1022        Returns:
    -1023            True if successful
    -1024
    -1025        Examples:
    -1026            >>> async def main():
    -1027            ...     async with AsyncOutlineClient(
    -1028            ...         "https://example.com:1234/secret",
    -1029            ...         "ab12cd34..."
    -1030            ...     ) as client:
    -1031            ...         # Set 100 GB global limit
    -1032            ...         await client.set_global_data_limit(100 * 1024**3)
    -1033        """
    -1034        request = DataLimitRequest(limit=DataLimit(bytes=bytes_limit))
    -1035        return await self._request(
    -1036            "PUT",
    -1037            "server/access-key-data-limit",
    -1038            json=request.model_dump(by_alias=True),
    -1039        )
    -
    - - -

    Set global data transfer limit for all access keys.

    + def + validate_port(port: int) -> int: -
    Arguments:
    + + +
    + +
    421    @staticmethod
    +422    def validate_port(port: int) -> int:
    +423        """Validate port number.
    +424
    +425        :param port: Port number
    +426        :return: Validated port
    +427        :raises ValueError: If port is out of range
    +428        """
    +429        if not is_valid_port(port):
    +430            raise ValueError(
    +431                f"Port must be between {Constants.MIN_PORT} and {Constants.MAX_PORT}"
    +432            )
    +433        return port
    +
    + + +

    Validate port number.

    + +
    Parameters
      -
    • bytes_limit: Limit in bytes (must be non-negative)
    • +
    • port: Port number
    -
    Returns:
    +
    Returns
    -

    True if successful

    +

    Validated port

    -
    Examples:
    +
    Raises
    -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         # Set 100 GB global limit
    -...         await client.set_global_data_limit(100 * 1024**3)
    -
    -
    -
    +
      +
    • ValueError: If port is out of range
    • +
    -
    - +
    +
    -
    @log_method_call
    +
    @staticmethod
    - async def - remove_global_data_limit(self) -> bool: + def + validate_name(name: str) -> str: - +
    - -
    1041    @log_method_call
    -1042    async def remove_global_data_limit(self) -> bool:
    -1043        """
    -1044        Remove global data transfer limit.
    -1045
    -1046        Returns:
    -1047            True if successful
    -1048
    -1049        Examples:
    -1050            >>> async def main():
    -1051            ...     async with AsyncOutlineClient(
    -1052            ...         "https://example.com:1234/secret",
    -1053            ...         "ab12cd34..."
    -1054            ...     ) as client:
    -1055            ...         await client.remove_global_data_limit()
    -1056        """
    -1057        return await self._request("DELETE", "server/access-key-data-limit")
    +    
    +            
    435    @staticmethod
    +436    def validate_name(name: str) -> str:
    +437        """Validate name field.
    +438
    +439        :param name: Name to validate
    +440        :return: Validated name
    +441        :raises ValueError: If name is invalid
    +442        """
    +443        if not name or not name.strip():
    +444            raise ValueError("Name cannot be empty")
    +445
    +446        name = name.strip()
    +447        if len(name) > Constants.MAX_NAME_LENGTH:
    +448            raise ValueError(
    +449                f"Name too long: {len(name)} (max {Constants.MAX_NAME_LENGTH})"
    +450            )
    +451
    +452        return name
     
    -

    Remove global data transfer limit.

    +

    Validate name field.

    -
    Returns:
    +
    Parameters
    -
    -

    True if successful

    -
    +
      +
    • name: Name to validate
    • +
    -
    Examples:
    +
    Returns
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         await client.remove_global_data_limit()
    -
    -
    +

    Validated name

    + +
    Raises
    + +
      +
    • ValueError: If name is invalid
    • +
    -
    - +
    +
    - - async def - batch_create_access_keys( self, keys_config: list[dict[str, typing.Any]], fail_fast: bool = True) -> list[typing.Union[AccessKey, Exception]]: - - - -
    - -
    1061    async def batch_create_access_keys(
    -1062            self,
    -1063            keys_config: list[dict[str, Any]],
    -1064            fail_fast: bool = True
    -1065    ) -> list[Union[AccessKey, Exception]]:
    -1066        """
    -1067        Create multiple access keys in batch.
    -1068
    -1069        Args:
    -1070            keys_config: List of key configurations (same as create_access_key kwargs)
    -1071            fail_fast: If True, stop on first error. If False, continue and return errors.
    -1072
    -1073        Returns:
    -1074            List of created keys or exceptions
    -1075
    -1076        Examples:
    -1077            >>> async def main():
    -1078            ...     async with AsyncOutlineClient(
    -1079            ...         "https://example.com:1234/secret",
    -1080            ...         "ab12cd34..."
    -1081            ...     ) as client:
    -1082            ...         configs = [
    -1083            ...             {"name": "User1", "limit": DataLimit(bytes=1024**3)},
    -1084            ...             {"name": "User2", "port": 8388},
    -1085            ...         ]
    -1086            ...         res = await client.batch_create_access_keys(configs)
    -1087        """
    -1088        results = []
    -1089
    -1090        for config in keys_config:
    -1091            try:
    -1092                key = await self.create_access_key(**config)
    -1093                results.append(key)
    -1094            except Exception as e:
    -1095                if fail_fast:
    -1096                    raise
    -1097                results.append(e)
    -1098
    -1099        return results
    -
    - - -

    Create multiple access keys in batch.

    +
    @staticmethod
    -
    Arguments:
    + def + validate_url( url: str, *, allow_private_networks: bool = True, resolve_dns: bool = False) -> str: + + + +
    + +
    454    @staticmethod
    +455    def validate_url(
    +456        url: str,
    +457        *,
    +458        allow_private_networks: bool = True,
    +459        resolve_dns: bool = False,
    +460    ) -> str:
    +461        """Validate and sanitize URL.
    +462
    +463        :param url: URL to validate
    +464        :param allow_private_networks: Allow private/local network addresses
    +465        :param resolve_dns: Resolve hostname and block private/reserved IPs
    +466        :return: Validated URL
    +467        :raises ValueError: If URL is invalid
    +468        """
    +469        if not url or not url.strip():
    +470            raise ValueError("URL cannot be empty")
    +471
    +472        url = url.strip()
    +473
    +474        if len(url) > Constants.MAX_URL_LENGTH:
    +475            raise ValueError(
    +476                f"URL too long: {len(url)} (max {Constants.MAX_URL_LENGTH})"
    +477            )
    +478
    +479        # Check for null bytes
    +480        if "\x00" in url:
    +481            raise ValueError("URL contains null bytes")
    +482
    +483        # Parse URL
    +484        try:
    +485            parsed = urlparse(url)
    +486            if not parsed.scheme or not parsed.netloc:
    +487                raise ValueError("Invalid URL format")
    +488        except Exception as e:
    +489            raise ValueError(f"Invalid URL: {e}") from e
    +490
    +491        # SSRF protection for raw IPs in hostname (does not resolve DNS)
    +492        if (
    +493            not allow_private_networks
    +494            and parsed.hostname
    +495            and SSRFProtection.is_blocked_ip(parsed.hostname)
    +496        ):
    +497            raise ValueError(
    +498                f"Access to {parsed.hostname} is blocked (SSRF protection)"
    +499            )
    +500
    +501        # Strict SSRF protection with DNS resolution (guards against rebinding)
    +502        if (
    +503            resolve_dns
    +504            and not allow_private_networks
    +505            and parsed.hostname
    +506            and not SSRFProtection.is_blocked_ip(parsed.hostname)
    +507            and SSRFProtection.is_blocked_hostname(parsed.hostname)
    +508        ):
    +509            raise ValueError(
    +510                f"Access to {parsed.hostname} is blocked (SSRF protection)"
    +511            )
    +512
    +513        return url
    +
    + + +

    Validate and sanitize URL.

    + +
    Parameters
      -
    • keys_config: List of key configurations (same as create_access_key kwargs)
    • -
    • fail_fast: If True, stop on first error. If False, continue and return errors.
    • +
    • url: URL to validate
    • +
    • allow_private_networks: Allow private/local network addresses
    • +
    • resolve_dns: Resolve hostname and block private/reserved IPs
    -
    Returns:
    +
    Returns
    -

    List of created keys or exceptions

    +

    Validated URL

    -
    Examples:
    +
    Raises
    -
    -
    -
    >>> async def main():
    -...     async with AsyncOutlineClient(
    -...         "https://example.com:1234/secret",
    -...         "ab12cd34..."
    -...     ) as client:
    -...         configs = [
    -...             {"name": "User1", "limit": DataLimit(bytes=1024**3)},
    -...             {"name": "User2", "port": 8388},
    -...         ]
    -...         res = await client.batch_create_access_keys(configs)
    -
    -
    -
    +
      +
    • ValueError: If URL is invalid
    • +
    -
    - +
    +
    - - async def - get_server_summary(self, metrics_since: str = '24h') -> dict[str, typing.Any]: +
    @staticmethod
    - + def + validate_string_not_empty(value: str, field_name: str) -> str: + +
    - -
    1101    async def get_server_summary(self, metrics_since: str = "24h") -> dict[str, Any]:
    -1102        """
    -1103        Get comprehensive server summary including info, metrics, and key count.
    -1104
    -1105        Args:
    -1106            metrics_since: Time range for experimental metrics (default: "24h")
    -1107
    -1108        Returns:
    -1109            Dictionary with server info, health status, and statistics
    -1110        """
    -1111        summary = {}
    -1112
    -1113        try:
    -1114            # Get basic server info
    -1115            server_info = await self.get_server_info()
    -1116            summary["server"] = server_info.model_dump() if isinstance(server_info, BaseModel) else server_info
    -1117
    -1118            # Get access keys count
    -1119            keys = await self.get_access_keys()
    -1120            key_list = keys.access_keys if isinstance(keys, BaseModel) else keys.get("accessKeys", [])
    -1121            summary["access_keys_count"] = len(key_list)
    -1122
    -1123            # Get metrics if available
    -1124            try:
    -1125                metrics_status = await self.get_metrics_status()
    -1126                if (isinstance(metrics_status, BaseModel) and metrics_status.metrics_enabled) or \
    -1127                        (isinstance(metrics_status, dict) and metrics_status.get("metricsEnabled")):
    -1128                    transfer_metrics = await self.get_transfer_metrics()
    -1129                    summary["transfer_metrics"] = transfer_metrics.model_dump() if isinstance(transfer_metrics,
    -1130                                                                                              BaseModel) else transfer_metrics
    -1131
    -1132                    # Try to get experimental metrics
    -1133                    try:
    -1134                        experimental_metrics = await self.get_experimental_metrics(metrics_since)
    -1135                        summary["experimental_metrics"] = experimental_metrics.model_dump() if isinstance(
    -1136                            experimental_metrics,
    -1137                            BaseModel) else experimental_metrics
    -1138                    except Exception:
    -1139                        summary["experimental_metrics"] = None
    -1140            except Exception:
    -1141                summary["transfer_metrics"] = None
    -1142                summary["experimental_metrics"] = None
    -1143
    -1144            summary["healthy"] = True
    -1145
    -1146        except Exception as e:
    -1147            summary["healthy"] = False
    -1148            summary["error"] = str(e)
    -1149
    -1150        return summary
    -
    - - -

    Get comprehensive server summary including info, metrics, and key count.

    + +
    515    @staticmethod
    +516    def validate_string_not_empty(value: str, field_name: str) -> str:
    +517        """Validate string is not empty.
    +518
    +519        :param value: String value
    +520        :param field_name: Field name for error messages
    +521        :return: Stripped string
    +522        :raises ValueError: If string is empty
    +523        """
    +524        if not value or not value.strip():
    +525            raise ValueError(f"{field_name} cannot be empty")
    +526        return value.strip()
    +
    -
    Arguments:
    + +

    Validate string is not empty.

    + +
    Parameters
      -
    • metrics_since: Time range for experimental metrics (default: "24h")
    • +
    • value: String value
    • +
    • field_name: Field name for error messages
    -
    Returns:
    +
    Returns
    -

    Dictionary with server info, health status, and statistics

    +

    Stripped string

    + +
    Raises
    + +
      +
    • ValueError: If string is empty
    • +
    -
    - +
    +
    - +
    @staticmethod
    + def - configure_logging(self, level: str = 'INFO', format_string: Optional[str] = None) -> None: - - - -
    - -
    1154    def configure_logging(self, level: str = "INFO", format_string: Optional[str] = None) -> None:
    -1155        """
    -1156        Configure logging for the client.
    -1157
    -1158        Args:
    -1159            level: Logging level (DEBUG, INFO, WARNING, ERROR)
    -1160            format_string: Custom format string for log messages
    -1161        """
    -1162        self._enable_logging = True
    -1163
    -1164        # Clear existing handlers
    -1165        logger.handlers.clear()
    -1166
    -1167        handler = logging.StreamHandler()
    -1168        if format_string:
    -1169            formatter = logging.Formatter(format_string)
    -1170        else:
    -1171            formatter = logging.Formatter(
    -1172                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    -1173            )
    -1174        handler.setFormatter(formatter)
    -1175        logger.addHandler(handler)
    -1176        logger.setLevel(getattr(logging, level.upper()))
    -
    - - -

    Configure logging for the client.

    + validate_non_negative(value: DataLimit | int, name: str) -> int: -
    Arguments:
    + -
      -
    • level: Logging level (DEBUG, INFO, WARNING, ERROR)
    • -
    • format_string: Custom format string for log messages
    • -
    -
    +
    + +
    551    @staticmethod
    +552    def validate_non_negative(value: DataLimit | int, name: str) -> int:
    +553        """Validate integer is non-negative.
    +554
    +555        :param value: Integer value
    +556        :param name: Field name for error messages
    +557        :return: Validated value
    +558        :raises ValueError: If value is negative
    +559        """
    +560        from .models import DataLimit
    +561
    +562        raw_value = value.bytes if isinstance(value, DataLimit) else value
    +563        if raw_value < 0:
    +564            raise ValueError(f"{name} must be non-negative, got {raw_value}")
    +565        return raw_value
    +
    + + +

    Validate integer is non-negative.

    +
    Parameters
    -
    -
    - -
    - is_healthy: bool +
      +
    • value: Integer value
    • +
    • name: Field name for error messages
    • +
    - +
    Returns
    -
    - -
    1178    @property
    -1179    def is_healthy(self) -> bool:
    -1180        """Check if the last health check passed."""
    -1181        return self._is_healthy
    -
    +
    +

    Validated value

    +
    +
    Raises
    -

    Check if the last health check passed.

    +
      +
    • ValueError: If value is negative
    • +
    -
    - -
    - session: Optional[aiohttp.client.ClientSession] +
    + +
    +
    @staticmethod
    - + def + validate_since(value: str) -> str: + +
    - -
    1183    @property
    -1184    def session(self) -> Optional[aiohttp.ClientSession]:
    -1185        """Access the current client session."""
    -1186        return self._session
    +    
    +            
    567    @staticmethod
    +568    def validate_since(value: str) -> str:
    +569        """Validate experimental metrics 'since' parameter.
    +570
    +571        Accepts:
    +572        - Relative durations: 24h, 7d, 30m, 15s
    +573        - ISO-8601 timestamps (e.g., 2024-01-01T00:00:00Z)
    +574
    +575        :param value: Since parameter
    +576        :return: Sanitized since value
    +577        :raises ValueError: If value is invalid
    +578        """
    +579        if not value or not value.strip():
    +580            raise ValueError("'since' parameter cannot be empty")
    +581
    +582        sanitized = value.strip()
    +583
    +584        # Relative format (number + suffix)
    +585        if len(sanitized) >= 2 and sanitized[-1] in {"h", "d", "m", "s"}:
    +586            number = sanitized[:-1]
    +587            if number.isdigit():
    +588                return sanitized
    +589
    +590        # ISO-8601 timestamp (allow trailing Z)
    +591        iso_value = sanitized.replace("Z", "+00:00")
    +592        try:
    +593            datetime.fromisoformat(iso_value)
    +594            return sanitized
    +595        except ValueError:
    +596            raise ValueError(
    +597                "'since' must be a relative duration (e.g., '24h', '7d') "
    +598                "or ISO-8601 timestamp"
    +599            ) from None
     
    -

    Access the current client session.

    -
    +

    Validate experimental metrics 'since' parameter.

    +

    Accepts:

    -
    -
    - -
    - api_url: str +
      +
    • Relative durations: 24h, 7d, 30m, 15s
    • +
    • ISO-8601 timestamps (e.g., 2024-01-01T00:00:00Z)
    • +
    - +
    Parameters
    -
    - -
    1188    @property
    -1189    def api_url(self) -> str:
    -1190        """Get the API URL (without sensitive parts)."""
    -1191        from urllib.parse import urlparse
    -1192        parsed = urlparse(self._api_url)
    -1193        return f"{parsed.scheme}://{parsed.netloc}"
    -
    +
      +
    • value: Since parameter
    • +
    + +
    Returns
    +
    +

    Sanitized since value

    +
    -

    Get the API URL (without sensitive parts).

    +
    Raises
    + +
      +
    • ValueError: If value is invalid
    • +
    -
    -
    - -
    - - class - OutlineError(builtins.Exception): +
    + +
    +
    @classmethod
    +
    @lru_cache(maxsize=256)
    - + def + validate_key_id(cls, key_id: str) -> str: + +
    - -
    19class OutlineError(Exception):
    -20    """Base exception for Outline client errors."""
    +    
    +            
    601    @classmethod
    +602    @lru_cache(maxsize=256)
    +603    def validate_key_id(cls, key_id: str) -> str:
    +604        """Enhanced key_id validation.
    +605
    +606        :param key_id: Key ID to validate
    +607        :return: Validated key ID
    +608        :raises ValueError: If key ID is invalid
    +609        """
    +610        clean_id = cls.validate_string_not_empty(key_id, "key_id")
    +611        cls._validate_length(clean_id, Constants.MAX_KEY_ID_LENGTH, "key_id")
    +612        cls._validate_no_null_bytes(clean_id, "key_id")
    +613
    +614        try:
    +615            decoded = urllib.parse.unquote(clean_id)
    +616            double_decoded = urllib.parse.unquote(decoded)
    +617
    +618            # Check all variants for malicious characters
    +619            for variant in [clean_id, decoded, double_decoded]:
    +620                if any(c in variant for c in {".", "/", "\\", "%", "\x00"}):
    +621                    raise ValueError(
    +622                        "key_id contains invalid characters (., /, \\, %, null)"
    +623                    )
    +624        except Exception as e:
    +625            raise ValueError(f"Invalid key_id encoding: {e}") from e
    +626
    +627        # Strict whitelist approach
    +628        allowed_chars = frozenset(
    +629            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
    +630        )
    +631        if not all(c in allowed_chars for c in clean_id):
    +632            raise ValueError("key_id must be alphanumeric, dashes, underscores only")
    +633
    +634        return clean_id
     
    -

    Base exception for Outline client errors.

    -
    +

    Enhanced key_id validation.

    +
    Parameters
    -
    -
    - -
    - - class - APIError(pyoutlineapi.OutlineError): +
      +
    • key_id: Key ID to validate
    • +
    - +
    Returns
    -
    - -
    23class APIError(OutlineError):
    -24    """Raised when API requests fail."""
    -25
    -26    def __init__(
    -27        self,
    -28        message: str,
    -29        status_code: Optional[int] = None,
    -30        attempt: Optional[int] = None,
    -31    ) -> None:
    -32        super().__init__(message)
    -33        self.status_code = status_code
    -34        self.attempt = attempt
    -35
    -36    def __str__(self) -> str:
    -37        msg = super().__str__()
    -38        if self.attempt is not None:
    -39            msg = f"[Attempt {self.attempt}] {msg}"
    -40        return msg
    -
    +
    +

    Validated key ID

    +
    +
    Raises
    -

    Raised when API requests fail.

    +
      +
    • ValueError: If key ID is invalid
    • +
    -
    - +
    +
    +
    - - APIError( message: str, status_code: Optional[int] = None, attempt: Optional[int] = None) +
    @staticmethod
    +
    @lru_cache(maxsize=256)
    - + def + sanitize_url_for_logging(url: str) -> str: + +
    - -
    26    def __init__(
    -27        self,
    -28        message: str,
    -29        status_code: Optional[int] = None,
    -30        attempt: Optional[int] = None,
    -31    ) -> None:
    -32        super().__init__(message)
    -33        self.status_code = status_code
    -34        self.attempt = attempt
    +    
    +            
    636    @staticmethod
    +637    @lru_cache(maxsize=256)
    +638    def sanitize_url_for_logging(url: str) -> str:
    +639        """Remove secret path from URL for safe logging.
    +640
    +641        :param url: URL to sanitize
    +642        :return: Sanitized URL
    +643        """
    +644        try:
    +645            parsed = urlparse(url)
    +646            return f"{parsed.scheme}://{parsed.netloc}/***"
    +647        except Exception:
    +648            return "***INVALID_URL***"
     
    - +

    Remove secret path from URL for safe logging.

    -
    -
    -
    - status_code +
    Parameters
    - -
    - - - +
      +
    • url: URL to sanitize
    • +
    -
    -
    -
    - attempt +
    Returns
    + +
    +

    Sanitized URL

    +
    +
    - -
    - - -
    -
    -
    - -
    - - class - AccessKey(pydantic.main.BaseModel): +
    + +
    +
    @staticmethod
    +
    @lru_cache(maxsize=512)
    - + def + sanitize_endpoint_for_logging(endpoint: str) -> str: + +
    - -
    34class AccessKey(BaseModel):
    -35    """Access key details."""
    -36
    -37    id: str = Field(description="Access key identifier")
    -38    name: Optional[str] = Field(None, description="Access key name")
    -39    password: str = Field(description="Access key password")
    -40    port: int = Field(gt=0, lt=65536, description="Port number")
    -41    method: str = Field(description="Encryption method")
    -42    access_url: str = Field(alias="accessUrl", description="Complete access URL")
    -43    data_limit: Optional[DataLimit] = Field(
    -44        None, alias="dataLimit", description="Data limit for this key"
    -45    )
    +    
    +            
    650    @staticmethod
    +651    @lru_cache(maxsize=512)
    +652    def sanitize_endpoint_for_logging(endpoint: str) -> str:
    +653        """Sanitize endpoint for safe logging.
    +654
    +655        :param endpoint: Endpoint to sanitize
    +656        :return: Sanitized endpoint
    +657        """
    +658        if not endpoint:
    +659            return "***EMPTY***"
    +660
    +661        parts = endpoint.split("/")
    +662        sanitized = [part if len(part) <= 20 else "***" for part in parts]
    +663        return "/".join(sanitized)
     
    -

    Access key details.

    -
    - - -
    -
    - id: str +

    Sanitize endpoint for safe logging.

    - -
    - - - +
    Parameters
    -
    -
    -
    - name: Optional[str] +
      +
    • endpoint: Endpoint to sanitize
    • +
    - -
    - - - +
    Returns
    -
    -
    -
    - password: str +
    +

    Sanitized endpoint

    +
    +
    - -
    - - -
    -
    -
    - port: int +
    +
    +
    + __author__: Final[str] = +'Denis Rozhnovskiy'
    - + - -
    -
    - method: str +
    +
    +
    + __email__: Final[str] = +'pytelemonbot@mail.ru'
    - + - -
    -
    - access_url: str +
    +
    +
    + __license__: Final[str] = +'MIT'
    - + - -
    -
    - data_limit: Optional[DataLimit] +
    +
    +
    + __version__: str = +'0.4.0'
    - - + - - -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} - - -
    - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    -
    - -
    -
    - -
    +
    + +
    - class - AccessKeyCreateRequest(pydantic.main.BaseModel): + def + audited( *, log_success: bool = True, log_failure: bool = True) -> Callable[[Callable[~P, ~R]], Callable[~P, ~R]]: - +
    - -
    194class AccessKeyCreateRequest(BaseModel):
    -195    """
    -196    Request parameters for creating an access key.
    -197    Per OpenAPI: /access-keys POST request body
    -198    """
    -199
    -200    name: Optional[str] = Field(None, description="Access key name")
    -201    method: Optional[str] = Field(None, description="Encryption method")
    -202    password: Optional[str] = Field(None, description="Access key password")
    -203    port: Optional[int] = Field(None, gt=0, lt=65536, description="Port number")
    -204    limit: Optional[DataLimit] = Field(None, description="Data limit for this key")
    +    
    +            
    575def audited(
    +576    *,
    +577    log_success: bool = True,
    +578    log_failure: bool = True,
    +579) -> Callable[[Callable[P, R]], Callable[P, R]]:
    +580    """Audit logging decorator with zero-config smart extraction.
    +581
    +582    Automatically extracts ALL information from function signature and execution:
    +583    - Action name: from function name
    +584    - Resource: from result.id, first parameter, or function analysis
    +585    - Details: from function signature (excluding None and defaults)
    +586    - Correlation ID: from instance._correlation_id if available
    +587    - Success/failure: from exception handling
    +588
    +589    Usage:
    +590        @audited()
    +591        async def create_access_key(self, name: str, port: int = 8080) -> AccessKey:
    +592            # action: "create_access_key"
    +593            # resource: result.id
    +594            # details: {"name": "...", "port": 8080} (if not default)
    +595            ...
    +596
    +597        @audited(log_success=False)
    +598        async def critical_operation(self, resource_id: str) -> bool:
    +599            # Only logs failures for alerting
    +600            ...
    +601
    +602    :param log_success: Log successful operations (default: True)
    +603    :param log_failure: Log failed operations (default: True)
    +604    :return: Decorated function with automatic audit logging
    +605    """
    +606
    +607    def decorator(func: Callable[P, R]) -> Callable[P, R]:
    +608        # Determine if function is async at decoration time
    +609        is_async = inspect.iscoroutinefunction(func)
    +610
    +611        if is_async:
    +612            async_func = cast("Callable[P, Awaitable[object]]", func)
    +613
    +614            @wraps(func)
    +615            async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> object:
    +616                # Check for audit logger on instance
    +617                instance = args[0] if args else None
    +618                audit_logger = getattr(instance, "_audit_logger", None)
    +619
    +620                # No logger? Execute without audit
    +621                if audit_logger is None:
    +622                    return await async_func(*args, **kwargs)
    +623
    +624                result: object | None = None
    +625
    +626                try:
    +627                    result = await async_func(*args, **kwargs)
    +628                except Exception as e:
    +629                    if log_failure:
    +630                        ctx = AuditContext.from_call(
    +631                            func=func,
    +632                            instance=instance,
    +633                            args=args,
    +634                            kwargs=kwargs,
    +635                            result=result,
    +636                            exception=e,
    +637                        )
    +638                        task = asyncio.create_task(
    +639                            audit_logger.alog_action(
    +640                                action=ctx.action,
    +641                                resource=ctx.resource,
    +642                                details=ctx.details,
    +643                                correlation_id=ctx.correlation_id,
    +644                            )
    +645                        )
    +646                        task.add_done_callback(lambda t: t.exception())
    +647                    raise
    +648                else:
    +649                    if log_success:
    +650                        ctx = AuditContext.from_call(
    +651                            func=func,
    +652                            instance=instance,
    +653                            args=args,
    +654                            kwargs=kwargs,
    +655                            result=result,
    +656                            exception=None,
    +657                        )
    +658                        task = asyncio.create_task(
    +659                            audit_logger.alog_action(
    +660                                action=ctx.action,
    +661                                resource=ctx.resource,
    +662                                details=ctx.details,
    +663                                correlation_id=ctx.correlation_id,
    +664                            )
    +665                        )
    +666                        task.add_done_callback(lambda t: t.exception())
    +667                    return result
    +668
    +669            return cast("Callable[P, R]", async_wrapper)
    +670
    +671        else:
    +672            sync_func = cast("Callable[P, object]", func)
    +673
    +674            @wraps(func)
    +675            def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> object:
    +676                # Check for audit logger on instance
    +677                instance = args[0] if args else None
    +678                audit_logger = getattr(instance, "_audit_logger", None)
    +679
    +680                # No logger? Execute without audit
    +681                if audit_logger is None:
    +682                    return sync_func(*args, **kwargs)
    +683
    +684                result: object | None = None
    +685
    +686                try:
    +687                    result = sync_func(*args, **kwargs)
    +688                except Exception as e:
    +689                    if log_failure:
    +690                        ctx = AuditContext.from_call(
    +691                            func=func,
    +692                            instance=instance,
    +693                            args=args,
    +694                            kwargs=kwargs,
    +695                            result=result,
    +696                            exception=e,
    +697                        )
    +698                        audit_logger.log_action(
    +699                            action=ctx.action,
    +700                            resource=ctx.resource,
    +701                            details=ctx.details,
    +702                            correlation_id=ctx.correlation_id,
    +703                        )
    +704                    raise
    +705                else:
    +706                    if log_success:
    +707                        ctx = AuditContext.from_call(
    +708                            func=func,
    +709                            instance=instance,
    +710                            args=args,
    +711                            kwargs=kwargs,
    +712                            result=result,
    +713                            exception=None,
    +714                        )
    +715                        audit_logger.log_action(
    +716                            action=ctx.action,
    +717                            resource=ctx.resource,
    +718                            details=ctx.details,
    +719                            correlation_id=ctx.correlation_id,
    +720                        )
    +721                    return result
    +722
    +723            return cast("Callable[P, R]", sync_wrapper)
    +724
    +725    return decorator
     
    -

    Request parameters for creating an access key. -Per OpenAPI: /access-keys POST request body

    -
    - - -
    -
    - name: Optional[str] - - -
    - - - - -
    -
    -
    - method: Optional[str] - - -
    - - - +

    Audit logging decorator with zero-config smart extraction.

    -
    -
    -
    - password: Optional[str] +

    Automatically extracts ALL information from function signature and execution:

    - -
    - - - +
      +
    • Action name: from function name
    • +
    • Resource: from result.id, first parameter, or function analysis
    • +
    • Details: from function signature (excluding None and defaults)
    • +
    • Correlation ID: from instance._correlation_id if available
    • +
    • Success/failure: from exception handling
    • +
    -
    -
    -
    - port: Optional[int] +
    Usage:
    - -
    - - - +
    +

    @audited() + async def create_access_key(self, name: str, port: int = 8080) -> AccessKey: + # action: "create_access_key" + # resource: result.id + # details: {"name": "...", "port": 8080} (if not default) + ...

    + +

    @audited(log_success=False) + async def critical_operation(self, resource_id: str) -> bool: + # Only logs failures for alerting + ...

    +
    -
    -
    -
    - limit: Optional[DataLimit] +
    Parameters
    - -
    - - - +
      +
    • log_success: Log successful operations (default: True)
    • +
    • log_failure: Log failed operations (default: True)
    • +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    Returns
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    +

    Decorated function with automatic audit logging

    +
    -
    -
    - -
    +
    + +
    - class - AccessKeyList(pydantic.main.BaseModel): + def + build_config_overrides( **kwargs: int | str | bool | float | None) -> dict[str, int | str | bool | float | None]: - +
    - -
    48class AccessKeyList(BaseModel):
    -49    """List of access keys."""
    -50
    -51    access_keys: list[AccessKey] = Field(alias="accessKeys")
    +    
    +            
    722def build_config_overrides(
    +723    **kwargs: int | str | bool | float | None,
    +724) -> dict[str, int | str | bool | float | None]:
    +725    """Build configuration overrides dictionary from kwargs.
    +726
    +727    DRY implementation - single source of truth for config building.
    +728
    +729    :param kwargs: Configuration parameters
    +730    :return: Dictionary containing only non-None values
    +731
    +732    Example:
    +733        >>> overrides = build_config_overrides(timeout=20, enable_logging=True)
    +734        >>> # Returns: {'timeout': 20, 'enable_logging': True}
    +735    """
    +736    valid_keys = ConfigOverrides.__annotations__.keys()
    +737    return {k: v for k, v in kwargs.items() if k in valid_keys and v is not None}
     
    -

    List of access keys.

    -
    +

    Build configuration overrides dictionary from kwargs.

    +

    DRY implementation - single source of truth for config building.

    -
    -
    - access_keys: list[AccessKey] +
    Parameters
    - -
    - - - +
      +
    • kwargs: Configuration parameters
    • +
    + +
    Returns
    + +
    +

    Dictionary containing only non-None values

    +
    + +
    Example:
    + +
    +
    +
    >>> overrides = build_config_overrides(timeout=20, enable_logging=True)
    +>>> # Returns: {'timeout': 20, 'enable_logging': True}
    +
    +
    +
    +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} + +
    +
    +
    + correlation_id = +<ContextVar name='correlation_id' default=''>
    - + + -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    -
    - -
    -
    - -
    +
    + +
    - class - AccessKeyNameRequest(pydantic.main.BaseModel): + def + create_client( api_url: str, cert_sha256: str, *, audit_logger: AuditLogger | None = None, metrics: MetricsCollector | None = None, **overrides: Unpack[ConfigOverrides]) -> AsyncOutlineClient: - +
    - -
    225class AccessKeyNameRequest(BaseModel):
    -226    """Request for renaming access key."""
    -227
    -228    name: str = Field(description="New access key name")
    +    
    +            
    892def create_client(
    +893    api_url: str,
    +894    cert_sha256: str,
    +895    *,
    +896    audit_logger: AuditLogger | None = None,
    +897    metrics: MetricsCollector | None = None,
    +898        **overrides: Unpack[ConfigOverrides],
    +899) -> AsyncOutlineClient:
    +900    """Create client with minimal parameters.
    +901
    +902    Convenience function for quick client creation without
    +903    explicit configuration object. Uses modern **overrides approach.
    +904
    +905    :param api_url: API URL with secret path
    +906    :param cert_sha256: SHA-256 certificate fingerprint
    +907    :param audit_logger: Custom audit logger (optional)
    +908    :param metrics: Custom metrics collector (optional)
    +909    :param overrides: Configuration overrides (timeout, retry_attempts, etc.)
    +910    :return: Configured client instance (use with async context manager)
    +911    :raises ConfigurationError: If parameters are invalid
    +912
    +913    Example (advanced, prefer from_env for production):
    +914        >>> async with AsyncOutlineClient.from_env() as client:
    +915        ...     info = await client.get_server_info()
    +916    """
    +917    return AsyncOutlineClient(
    +918        api_url=api_url,
    +919        cert_sha256=cert_sha256,
    +920        audit_logger=audit_logger,
    +921        metrics=metrics,
    +922        **overrides,
    +923    )
     
    -

    Request for renaming access key.

    -
    +

    Create client with minimal parameters.

    +

    Convenience function for quick client creation without +explicit configuration object. Uses modern **overrides approach.

    -
    -
    - name: str +
    Parameters
    - -
    - - - +
      +
    • api_url: API URL with secret path
    • +
    • cert_sha256: SHA-256 certificate fingerprint
    • +
    • audit_logger: Custom audit logger (optional)
    • +
    • metrics: Custom metrics collector (optional)
    • +
    • overrides: Configuration overrides (timeout, retry_attempts, etc.)
    • +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    Returns
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    +

    Configured client instance (use with async context manager)

    +
    + +
    Raises
    + +
      +
    • ConfigurationError: If parameters are invalid
    • +
    + +

    Example (advanced, prefer from_env for production):

    + +
    +
    +
    +

    async with AsyncOutlineClient.from_env() as client: + ... info = await client.get_server_info()

    +
    +
    +
    -
    -
    - -
    +
    + +
    - class - DataLimit(pydantic.main.BaseModel): + def + create_env_template(path: str | pathlib._local.Path = '.env.example') -> None: - +
    - -
    21class DataLimit(BaseModel):
    -22    """Data transfer limit configuration."""
    -23
    -24    bytes: int = Field(ge=0, description="Data limit in bytes")
    -25
    -26    @classmethod
    -27    @field_validator("bytes")
    -28    def validate_bytes(cls, v: int) -> int:
    -29        if v < 0:
    -30            raise ValueError("bytes must be non-negative")
    -31        return v
    +    
    +            
    588def create_env_template(path: str | Path = ".env.example") -> None:
    +589    """Create .env template file (optimized I/O).
    +590
    +591    Performance: Uses cached template and efficient Path operations
    +592
    +593    :param path: Path to template file
    +594    """
    +595    # Pattern matching for path handling
    +596    match path:
    +597        case str():
    +598            target_path = Path(path)
    +599        case Path():
    +600            target_path = path
    +601        case _:
    +602            raise TypeError(f"path must be str or Path, got {type(path).__name__}")
    +603
    +604    # Use cached template
    +605    template = _get_env_template()
    +606    target_path.write_text(template, encoding="utf-8")
    +607
    +608    _log_if_enabled(
    +609        logging.INFO,
    +610        f"Created configuration template: {target_path}",
    +611    )
     
    -

    Data transfer limit configuration.

    -
    +

    Create .env template file (optimized I/O).

    +

    Performance: Uses cached template and efficient Path operations

    -
    -
    - bytes: int +
    Parameters
    - -
    - - - +
      +
    • path: Path to template file
    • +
    +
    -
    -
    - -
    -
    @classmethod
    -
    @field_validator('bytes')
    +
    +
    + +
    + def - validate_bytes(unknown): + create_multi_server_manager( configs: Sequence[OutlineClientConfig], *, audit_logger: AuditLogger | None = None, metrics: MetricsCollector | None = None, default_timeout: float = 5.0) -> MultiServerManager: - +
    - -
    26    @classmethod
    -27    @field_validator("bytes")
    -28    def validate_bytes(cls, v: int) -> int:
    -29        if v < 0:
    -30            raise ValueError("bytes must be non-negative")
    -31        return v
    +    
    +            
    926def create_multi_server_manager(
    +927    configs: Sequence[OutlineClientConfig],
    +928    *,
    +929    audit_logger: AuditLogger | None = None,
    +930    metrics: MetricsCollector | None = None,
    +931    default_timeout: float = _DEFAULT_SERVER_TIMEOUT,
    +932) -> MultiServerManager:
    +933    """Create multiserver manager with configurations.
    +934
    +935    Convenience function for creating a manager for multiple servers.
    +936
    +937    :param configs: Sequence of server configurations
    +938    :param audit_logger: Shared audit logger
    +939    :param metrics: Shared metrics collector
    +940    :param default_timeout: Default operation timeout
    +941    :return: MultiServerManager instance (use with async context manager)
    +942    :raises ConfigurationError: If configurations are invalid
    +943
    +944    Example:
    +945        >>> configs = [
    +946        ...     OutlineClientConfig.create_minimal("https://s1.com/path", "a" * 64),
    +947        ...     OutlineClientConfig.create_minimal("https://s2.com/path", "b" * 64),
    +948        ... ]
    +949        >>> async with create_multi_server_manager(configs) as manager:
    +950        ...     health = await manager.health_check_all()
    +951    """
    +952    return MultiServerManager(
    +953        configs=configs,
    +954        audit_logger=audit_logger,
    +955        metrics=metrics,
    +956        default_timeout=default_timeout,
    +957    )
     
    -

    Wrap a classmethod, staticmethod, property or unbound function -and act as a descriptor that allows us to detect decorated items -from the class' attributes.

    +

    Create multiserver manager with configurations.

    -

    This class' __get__ returns the wrapped item's __get__ result, -which makes it transparent for classmethods and staticmethods.

    +

    Convenience function for creating a manager for multiple servers.

    -
    Attributes:
    +
    Parameters
      -
    • wrapped: The decorator that has to be wrapped.
    • -
    • decorator_info: The decorator info.
    • -
    • shim: A wrapper function to wrap V1 style function.
    • +
    • configs: Sequence of server configurations
    • +
    • audit_logger: Shared audit logger
    • +
    • metrics: Shared metrics collector
    • +
    • default_timeout: Default operation timeout
    -
    +
    Returns
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    +

    MultiServerManager instance (use with async context manager)

    +
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    Raises
    + +
      +
    • ConfigurationError: If configurations are invalid
    • +
    + +
    Example:
    + +
    +
    +
    >>> configs = [
    +...     OutlineClientConfig.create_minimal("https://s1.com/path", "a" * 64),
    +...     OutlineClientConfig.create_minimal("https://s2.com/path", "b" * 64),
    +... ]
    +>>> async with create_multi_server_manager(configs) as manager:
    +...     health = await manager.health_check_all()
    +
    +
    +
    -
    -
    - -
    +
    + +
    - class - DataLimitRequest(pydantic.main.BaseModel): + def + format_error_chain(error: Exception) -> list[dict[str, typing.Any]]: - +
    - -
    231class DataLimitRequest(BaseModel):
    -232    """Request for setting data limit."""
    -233
    -234    limit: DataLimit = Field(description="Data limit configuration")
    +    
    +            
    602def format_error_chain(error: Exception) -> list[dict[str, Any]]:
    +603    """Format exception chain for structured logging.
    +604
    +605    Args:
    +606        error: Exception to format
    +607
    +608    Returns:
    +609        List of error dictionaries ordered from root to leaf
    +610
    +611    Example:
    +612        >>> try:
    +613        ...     raise ValueError("Inner") from KeyError("Outer")
    +614        ... except Exception as e:
    +615        ...     chain = format_error_chain(e)
    +616        ...     len(chain)  # 2
    +617    """
    +618    # Pre-allocate with reasonable size hint (most chains are 1-3 errors)
    +619    chain: list[dict[str, Any]] = []
    +620    current: BaseException | None = error
    +621
    +622    while current is not None:
    +623        chain.append(get_safe_error_dict(current))
    +624        current = current.__cause__ or current.__context__
    +625
    +626    return chain
     
    -

    Request for setting data limit.

    -
    +

    Format exception chain for structured logging.

    +
    Arguments:
    -
    -
    - limit: DataLimit +
      +
    • error: Exception to format
    • +
    - -
    - - - +
    Returns:
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    +

    List of error dictionaries ordered from root to leaf

    +
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    Example:
    + +
    +
    +
    >>> try:
    +...     raise ValueError("Inner") from KeyError("Outer")
    +... except Exception as e:
    +...     chain = format_error_chain(e)
    +...     len(chain)  # 2
    +
    +
    +
    -
    -
    - -
    +
    + +
    - class - ErrorResponse(pydantic.main.BaseModel): + def + get_audit_logger() -> AuditLogger | None: - +
    - -
    253class ErrorResponse(BaseModel):
    -254    """
    -255    Error response structure.
    -256    Per OpenAPI: 404 and 400 responses
    -257    """
    -258
    -259    code: str = Field(description="Error code")
    -260    message: str = Field(description="Error message")
    +    
    +            
    778def get_audit_logger() -> AuditLogger | None:
    +779    """Get audit logger from current context.
    +780
    +781    :return: Audit logger instance or None
    +782    """
    +783    return _audit_logger_context.get()
     
    -

    Error response structure. -Per OpenAPI: 404 and 400 responses

    +

    Get audit logger from current context.

    + +
    Returns
    + +
    +

    Audit logger instance or None

    +
    -
    -
    - code: str +
    +
    + +
    + + def + get_or_create_audit_logger(instance_id: int | None = None) -> AuditLogger: + + -
    - - - + +
    786def get_or_create_audit_logger(instance_id: int | None = None) -> AuditLogger:
    +787    """Get or create audit logger with weak reference caching.
    +788
    +789    :param instance_id: Instance ID for caching (optional)
    +790    :return: Audit logger instance
    +791    """
    +792    # Try context first
    +793    ctx_logger = _audit_logger_context.get()
    +794    if ctx_logger is not None:
    +795        return ctx_logger
    +796
    +797    # Try cache if instance_id provided
    +798    if instance_id is not None:
    +799        cached = _logger_cache.get(instance_id)
    +800        if cached is not None:
    +801            return cached
    +802
    +803    # Create new logger
    +804    logger_instance = DefaultAuditLogger()
    +805
    +806    # Cache if instance_id provided
    +807    if instance_id is not None:
    +808        _logger_cache[instance_id] = cast(AuditLogger, logger_instance)
    +809
    +810    return cast(AuditLogger, logger_instance)
    +
    -
    -
    -
    - message: str - -
    - - - +

    Get or create audit logger with weak reference caching.

    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    Parameters
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
      +
    • instance_id: Instance ID for caching (optional)
    • +
    + +
    Returns
    + +
    +

    Audit logger instance

    +
    -
    -
    - -
    +
    + +
    - class - ExperimentalMetrics(pydantic.main.BaseModel): + def + get_retry_delay(error: Exception) -> float | None: - +
    - -
    151class ExperimentalMetrics(BaseModel):
    -152    """
    -153    Experimental metrics data structure.
    -154    Per OpenAPI: /experimental/server/metrics endpoint
    -155    """
    -156
    -157    server: ServerExperimentalMetric = Field(description="Server metrics")
    -158    access_keys: list[AccessKeyMetric] = Field(
    -159        alias="accessKeys", description="Access key metrics"
    -160    )
    +    
    +            
    500def get_retry_delay(error: Exception) -> float | None:
    +501    """Get suggested retry delay for an error.
    +502
    +503    Args:
    +504        error: Exception to check
    +505
    +506    Returns:
    +507        Retry delay in seconds, or None if not retryable
    +508
    +509    Example:
    +510        >>> error = OutlineTimeoutError("Timeout")
    +511        >>> get_retry_delay(error)  # 2.0
    +512    """
    +513    if not isinstance(error, OutlineError):
    +514        return None
    +515    if not error.is_retryable:
    +516        return None
    +517    return error.default_retry_delay
     
    -

    Experimental metrics data structure. -Per OpenAPI: /experimental/server/metrics endpoint

    -
    - +

    Get suggested retry delay for an error.

    -
    -
    - server: pyoutlineapi.models.ServerExperimentalMetric +
    Arguments:
    - -
    - - - +
      +
    • error: Exception to check
    • +
    -
    -
    -
    - access_keys: list[pyoutlineapi.models.AccessKeyMetric] +
    Returns:
    - -
    - - - +
    +

    Retry delay in seconds, or None if not retryable

    +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    Example:
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    +
    +
    >>> error = OutlineTimeoutError("Timeout")
    +>>> get_retry_delay(error)  # 2.0
    +
    +
    +
    -
    -
    - -
    +
    + +
    - class - HostnameRequest(pydantic.main.BaseModel): + def + get_safe_error_dict(error: BaseException) -> dict[str, typing.Any]: - +
    - -
    213class HostnameRequest(BaseModel):
    -214    """Request for changing hostname."""
    -215
    -216    hostname: str = Field(description="New hostname or IP address")
    +    
    +            
    538def get_safe_error_dict(error: BaseException) -> dict[str, Any]:
    +539    """Extract safe error information for logging.
    +540
    +541    Returns only safe information without sensitive data.
    +542
    +543    Args:
    +544        error: Exception to convert
    +545
    +546    Returns:
    +547        Safe error dictionary suitable for logging
    +548
    +549    Example:
    +550        >>> error = APIError("Not found", status_code=404)
    +551        >>> get_safe_error_dict(error)
    +552        {'type': 'APIError', 'message': 'Not found', 'status_code': 404, ...}
    +553    """
    +554    result: dict[str, Any] = {
    +555        "type": type(error).__name__,
    +556        "message": str(error),
    +557    }
    +558
    +559    if not isinstance(error, OutlineError):
    +560        return result
    +561
    +562    result.update(
    +563        {
    +564            "retryable": error.is_retryable,
    +565            "retry_delay": error.default_retry_delay,
    +566            "safe_details": error.safe_details,
    +567        }
    +568    )
    +569
    +570    match error:
    +571        case APIError():
    +572            result["status_code"] = error.status_code
    +573            # Only compute these if status_code is not None
    +574            if error.status_code is not None:
    +575                result["is_client_error"] = error.is_client_error
    +576                result["is_server_error"] = error.is_server_error
    +577        case CircuitOpenError():
    +578            result["retry_after"] = error.retry_after
    +579        case ConfigurationError():
    +580            if error.field is not None:
    +581                result["field"] = error.field
    +582            result["security_issue"] = error.security_issue
    +583        case ValidationError():
    +584            if error.field is not None:
    +585                result["field"] = error.field
    +586            if error.model is not None:
    +587                result["model"] = error.model
    +588        case OutlineConnectionError():
    +589            if error.host is not None:
    +590                result["host"] = error.host
    +591            if error.port is not None:
    +592                result["port"] = error.port
    +593        case OutlineTimeoutError():
    +594            if error.timeout is not None:
    +595                result["timeout"] = error.timeout
    +596            if error.operation is not None:
    +597                result["operation"] = error.operation
    +598
    +599    return result
     
    -

    Request for changing hostname.

    +

    Extract safe error information for logging.

    + +

    Returns only safe information without sensitive data.

    + +
    Arguments:
    + +
      +
    • error: Exception to convert
    • +
    + +
    Returns:
    + +
    +

    Safe error dictionary suitable for logging

    +
    + +
    Example:
    + +
    +
    +
    >>> error = APIError("Not found", status_code=404)
    +>>> get_safe_error_dict(error)
    +{'type': 'APIError', 'message': 'Not found', 'status_code': 404, ...}
    +
    +
    +
    -
    -
    - hostname: str +
    +
    + +
    + + def + get_version() -> str: + + -
    - - - + +
    258def get_version() -> str:
    +259    """Get package version string.
    +260
    +261    :return: Package version
    +262    """
    +263    return __version__
    +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +

    Get package version string.

    + +
    Returns
    + +
    +

    Package version

    +
    -
    -
    - -
    +
    + +
    - class - MetricsEnabledRequest(pydantic.main.BaseModel): + def + is_json_serializable( value: object) -> TypeGuard[Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]]: - +
    - -
    237class MetricsEnabledRequest(BaseModel):
    -238    """Request for enabling/disabling metrics."""
    -239
    -240    metrics_enabled: bool = Field(
    -241        alias="metricsEnabled", description="Enable or disable metrics"
    -242    )
    +    
    +            
    372def is_json_serializable(value: object) -> TypeGuard[JsonValue]:
    +373    """Type guard for JSON-serializable values.
    +374
    +375    :param value: Value to check
    +376    :return: True if value is JSON-serializable
    +377    """
    +378    if value is None or isinstance(value, str | int | float | bool):
    +379        return True
    +380    if isinstance(value, dict):
    +381        return all(
    +382            isinstance(k, str) and is_json_serializable(v) for k, v in value.items()
    +383        )
    +384    if isinstance(value, list):
    +385        return all(is_json_serializable(item) for item in value)
    +386    return False
     
    -

    Request for enabling/disabling metrics.

    -
    - +

    Type guard for JSON-serializable values.

    -
    -
    - metrics_enabled: bool +
    Parameters
    - -
    - - - +
      +
    • value: Value to check
    • +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    Returns
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    +

    True if value is JSON-serializable

    +
    -
    -
    - -
    +
    + +
    - class - MetricsStatusResponse(pydantic.main.BaseModel): + def + is_retryable(error: Exception) -> bool: - +
    - -
    245class MetricsStatusResponse(BaseModel):
    -246    """Response for /metrics/enabled endpoint."""
    -247
    -248    metrics_enabled: bool = Field(
    -249        alias="metricsEnabled", description="Current metrics status"
    -250    )
    +    
    +            
    520def is_retryable(error: Exception) -> bool:
    +521    """Check if error should be retried.
    +522
    +523    Args:
    +524        error: Exception to check
    +525
    +526    Returns:
    +527        True if error is retryable
    +528
    +529    Example:
    +530        >>> error = APIError("Server error", status_code=503)
    +531        >>> is_retryable(error)  # True
    +532    """
    +533    if isinstance(error, OutlineError):
    +534        return error.is_retryable
    +535    return False
     
    -

    Response for /metrics/enabled endpoint.

    -
    +

    Check if error should be retried.

    +
    Arguments:
    -
    -
    - metrics_enabled: bool +
      +
    • error: Exception to check
    • +
    - -
    - - - +
    Returns:
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    +

    True if error is retryable

    +
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    Example:
    + +
    +
    +
    >>> error = APIError("Server error", status_code=503)
    +>>> is_retryable(error)  # True
    +
    +
    +
    -
    -
    - -
    +
    + +
    - class - PortRequest(pydantic.main.BaseModel): + def + is_valid_bytes(value: object) -> TypeGuard[int]: - +
    - -
    219class PortRequest(BaseModel):
    -220    """Request for changing default port."""
    -221
    -222    port: int = Field(gt=0, lt=65536, description="New default port")
    +    
    +            
    363def is_valid_bytes(value: object) -> TypeGuard[int]:
    +364    """Type guard for valid byte counts.
    +365
    +366    :param value: Value to check
    +367    :return: True if value is valid bytes
    +368    """
    +369    return isinstance(value, int) and value >= 0
     
    -

    Request for changing default port.

    -
    - +

    Type guard for valid byte counts.

    -
    -
    - port: int +
    Parameters
    - -
    - - - +
      +
    • value: Value to check
    • +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    Returns
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    +

    True if value is valid bytes

    +
    -
    -
    - -
    +
    + +
    - class - Server(pydantic.main.BaseModel): + def + is_valid_port(value: object) -> TypeGuard[int]: - +
    - -
    163class Server(BaseModel):
    -164    """
    -165    Server information.
    -166    Per OpenAPI: /server endpoint schema
    -167    """
    -168
    -169    name: str = Field(description="Server name")
    -170    server_id: str = Field(alias="serverId", description="Unique server identifier")
    -171    metrics_enabled: bool = Field(
    -172        alias="metricsEnabled", description="Metrics sharing status"
    -173    )
    -174    created_timestamp_ms: int = Field(
    -175        alias="createdTimestampMs", description="Creation timestamp in milliseconds"
    -176    )
    -177    version: str = Field(description="Server version")
    -178    port_for_new_access_keys: int = Field(
    -179        alias="portForNewAccessKeys",
    -180        gt=0,
    -181        lt=65536,
    -182        description="Default port for new keys",
    -183    )
    -184    hostname_for_access_keys: Optional[str] = Field(
    -185        None, alias="hostnameForAccessKeys", description="Hostname for access keys"
    -186    )
    -187    access_key_data_limit: Optional[DataLimit] = Field(
    -188        None,
    -189        alias="accessKeyDataLimit",
    -190        description="Global data limit for access keys",
    -191    )
    -
    - - -

    Server information. -Per OpenAPI: /server endpoint schema

    -
    + +
    354def is_valid_port(value: object) -> TypeGuard[int]:
    +355    """Type guard for valid port numbers.
    +356
    +357    :param value: Value to check
    +358    :return: True if value is valid port
    +359    """
    +360    return isinstance(value, int) and Constants.MIN_PORT <= value <= Constants.MAX_PORT
    +
    -
    -
    - name: str +

    Type guard for valid port numbers.

    - -
    - - - +
    Parameters
    -
    -
    -
    - server_id: str +
      +
    • value: Value to check
    • +
    - -
    - - - +
    Returns
    -
    -
    -
    - metrics_enabled: bool +
    +

    True if value is valid port

    +
    +
    - -
    - - - -
    -
    -
    - created_timestamp_ms: int +
    +
    + +
    + + def + load_config( environment: str = 'custom', **overrides: int | str | bool | float) -> OutlineClientConfig: + + -
    - - - + +
    614def load_config(
    +615    environment: str = "custom",
    +616    **overrides: ConfigValue,
    +617) -> OutlineClientConfig:
    +618    """Load configuration for environment (optimized lookup).
    +619
    +620    :param environment: Environment name (development, production, custom)
    +621    :param overrides: Configuration parameters to override
    +622    :return: Configuration instance
    +623    :raises ValueError: If environment name is invalid
    +624
    +625    Example:
    +626        >>> config = load_config("production", timeout=20)
    +627    """
    +628    env_lower = environment.lower()
    +629
    +630    # Fast validation with frozenset
    +631    if env_lower not in _VALID_ENVIRONMENTS:
    +632        valid_envs = ", ".join(sorted(_VALID_ENVIRONMENTS))
    +633        raise ValueError(f"Invalid environment '{environment}'. Valid: {valid_envs}")
    +634
    +635    # Pattern matching for config selection (Python 3.10+)
    +636    config_class: type[OutlineClientConfig]
    +637    match env_lower:
    +638        case "development" | "dev":
    +639            config_class = DevelopmentConfig
    +640        case "production" | "prod":
    +641            config_class = ProductionConfig
    +642        case "custom":
    +643            config_class = OutlineClientConfig
    +644        case _:  # Should never reach due to validation above
    +645            config_class = OutlineClientConfig
    +646
    +647    # Optimized override filtering
    +648    valid_keys = frozenset(ConfigOverrides.__annotations__.keys())
    +649    filtered_overrides = cast(
    +650        ConfigOverrides,
    +651        {k: v for k, v in overrides.items() if k in valid_keys},
    +652    )
    +653
    +654    return config_class(  # type: ignore[call-arg, unused-ignore]
    +655        **filtered_overrides
    +656    )
    +
    -
    -
    -
    - version: str - -
    - - - +

    Load configuration for environment (optimized lookup).

    -
    -
    -
    - port_for_new_access_keys: int +
    Parameters
    - -
    - - - +
      +
    • environment: Environment name (development, production, custom)
    • +
    • overrides: Configuration parameters to override
    • +
    -
    -
    -
    - hostname_for_access_keys: Optional[str] +
    Returns
    - -
    - - - +
    +

    Configuration instance

    +
    -
    -
    -
    - access_key_data_limit: Optional[DataLimit] +
    Raises
    - -
    - - - +
      +
    • ValueError: If environment name is invalid
    • +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} +
    Example:
    - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +
    +
    +
    >>> config = load_config("production", timeout=20)
    +
    +
    +
    -
    -
    - -
    +
    + +
    - class - ServerMetrics(pydantic.main.BaseModel): + def + mask_sensitive_data( data: Mapping[str, typing.Any], *, sensitive_keys: frozenset[str] | None = None, _depth: int = 0) -> dict[str, typing.Any]: - +
    - -
    54class ServerMetrics(BaseModel):
    -55    """
    -56    Server metrics data for data transferred per access key.
    -57    Per OpenAPI: /metrics/transfer endpoint
    -58    """
    -59
    -60    bytes_transferred_by_user_id: dict[str, int] = Field(
    -61        alias="bytesTransferredByUserId",
    -62        description="Data transferred by each access key ID",
    -63    )
    +    
    +            
    756def mask_sensitive_data(
    +757    data: Mapping[str, Any],
    +758    *,
    +759    sensitive_keys: frozenset[str] | None = None,
    +760    _depth: int = 0,
    +761) -> dict[str, Any]:
    +762    """Sensitive data masking with lazy copying and optimized recursion.
    +763
    +764    Uses lazy copying - only creates new dict when needed.
    +765    Includes recursion depth protection.
    +766
    +767    :param data: Data dictionary to mask
    +768    :param sensitive_keys: Set of sensitive key names (case-insensitive matching)
    +769    :param _depth: Current recursion depth (internal)
    +770    :return: Masked data dictionary (may be same object if no sensitive data found)
    +771    """
    +772    # Guard against infinite recursion
    +773    if _depth > Constants.MAX_RECURSION_DEPTH:
    +774        return {"_error": "Max recursion depth exceeded"}
    +775
    +776    keys_to_mask = sensitive_keys or DEFAULT_SENSITIVE_KEYS
    +777    keys_lower = {k.lower() for k in keys_to_mask}
    +778
    +779    masked: dict[str, Any] | None = None
    +780
    +781    for key, value in data.items():
    +782        # Check if key is sensitive
    +783        if key.lower() in keys_lower:
    +784            if masked is None:
    +785                masked = dict(data)
    +786            masked[key] = "***MASKED***"
    +787            continue
    +788
    +789        # Recursively handle nested dicts
    +790        if isinstance(value, dict):
    +791            nested = mask_sensitive_data(
    +792                value, sensitive_keys=keys_to_mask, _depth=_depth + 1
    +793            )
    +794            if nested is not value:
    +795                if masked is None:
    +796                    masked = dict(data)
    +797                masked[key] = nested
    +798
    +799        # Handle lists containing dicts
    +800        elif isinstance(value, list):
    +801            new_list: list[Any] = []
    +802            list_modified = False
    +803
    +804            for item in value:
    +805                if isinstance(item, dict):
    +806                    masked_item = mask_sensitive_data(
    +807                        item, sensitive_keys=keys_to_mask, _depth=_depth + 1
    +808                    )
    +809                    if masked_item is not item:
    +810                        list_modified = True
    +811                    new_list.append(masked_item)
    +812                else:
    +813                    new_list.append(item)
    +814
    +815            if list_modified:
    +816                if masked is None:
    +817                    masked = dict(data)
    +818                masked[key] = new_list
    +819
    +820    return masked if masked is not None else dict(data)
     
    -

    Server metrics data for data transferred per access key. -Per OpenAPI: /metrics/transfer endpoint

    +

    Sensitive data masking with lazy copying and optimized recursion.

    + +

    Uses lazy copying - only creates new dict when needed. +Includes recursion depth protection.

    + +
    Parameters
    + +
      +
    • data: Data dictionary to mask
    • +
    • sensitive_keys: Set of sensitive key names (case-insensitive matching)
    • +
    • _depth: Current recursion depth (internal)
    • +
    + +
    Returns
    + +
    +

    Masked data dictionary (may be same object if no sensitive data found)

    +
    -
    -
    - bytes_transferred_by_user_id: dict[str, int] +
    +
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +

    Print information about available type aliases for advanced usage.

    -
    -
    - -
    +
    + +
    - class - ServerNameRequest(pydantic.main.BaseModel): + def + quick_setup() -> None: - +
    - -
    207class ServerNameRequest(BaseModel):
    -208    """Request for renaming server."""
    -209
    -210    name: str = Field(description="New server name")
    +    
    +            
    266def quick_setup() -> None:
    +267    """Create configuration template file for quick setup.
    +268
    +269    Creates `.env.example` file with all available configuration options.
    +270    """
    +271    create_env_template()
    +272    print("✅ Created .env.example")
    +273    print("📝 Edit the file with your server details")
    +274    print("🚀 Then use: AsyncOutlineClient.from_env()")
     
    -

    Request for renaming server.

    +

    Create configuration template file for quick setup.

    + +

    Creates .env.example file with all available configuration options.

    -
    -
    - name: str +
    +
    + +
    + + def + set_audit_logger(logger_instance: AuditLogger) -> None: + + -
    - - - + +
    767def set_audit_logger(logger_instance: AuditLogger) -> None:
    +768    """Set audit logger for current async context.
    +769
    +770    Thread-safe and async-safe using contextvars.
    +771    Preferred over global state for high-load applications.
    +772
    +773    :param logger_instance: Audit logger instance
    +774    """
    +775    _audit_logger_context.set(logger_instance)
    +
    -
    -
    -
    - model_config: ClassVar[pydantic.config.ConfigDict] = -{} - -
    - - -

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    +

    Set audit logger for current async context.

    + +

    Thread-safe and async-safe using contextvars. +Preferred over global state for high-load applications.

    + +
    Parameters
    + +
      +
    • logger_instance: Audit logger instance
    • +
    -
    diff --git a/docs/search.js b/docs/search.js index 5cdc2ec..539f89e 100644 --- a/docs/search.js +++ b/docs/search.js @@ -1,6 +1,6 @@ window.pdocSearch = (function(){ /** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oPyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    \n\n

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru\nAll rights reserved.

    \n\n

    This software is licensed under the MIT License.

    \n\n
    You can find the full license text at:
    \n\n
    \n

    https://opensource.org/licenses/MIT

    \n
    \n\n
    Source code repository:
    \n\n
    \n

    https://github.com/orenlab/pyoutlineapi

    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

    Asynchronous client for the Outline VPN Server API.

    \n\n
    Arguments:
    \n\n
      \n
    • api_url: Base URL for the Outline server API
    • \n
    • cert_sha256: SHA-256 fingerprint of the server's TLS certificate
    • \n
    • json_format: Return raw JSON instead of Pydantic models
    • \n
    • timeout: Request timeout in seconds
    • \n
    • retry_attempts: Number of retry attempts connecting to the API
    • \n
    • enable_logging: Enable debug logging for API calls
    • \n
    • user_agent: Custom user agent string
    • \n
    • max_connections: Maximum number of connections in the pool
    • \n
    • rate_limit_delay: Minimum delay between requests (seconds)
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34...",\n...         enable_logging=True\n...     ) as client:\n...         server_info = await client.get_server_info()\n...         print(f"Server: {server_info.name}")\n...\n...     # Or use as context manager factory\n...     async with AsyncOutlineClient.create(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.get_server_info()\n
    \n
    \n
    \n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\tjson_format: bool = False,\ttimeout: int = 30,\tretry_attempts: int = 3,\tenable_logging: bool = False,\tuser_agent: Optional[str] = None,\tmax_connections: int = 10,\trate_limit_delay: float = 0.0)"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create", "kind": "function", "doc": "

    Factory method that returns an async context manager.

    \n\n

    This is the recommended way to create clients for one-off operations.

    \n", "signature": "(\tcls,\tapi_url: str,\tcert_sha256: str,\t**kwargs) -> AsyncGenerator[pyoutlineapi.client.AsyncOutlineClient, NoneType]:", "funcdef": "def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.health_check", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.health_check", "kind": "function", "doc": "

    Perform a health check on the Outline server.

    \n\n
    Arguments:
    \n\n
      \n
    • force: Force health check even if recently performed
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if server is healthy

    \n
    \n", "signature": "(self, force: bool = False) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_info", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_info", "kind": "function", "doc": "

    Get server information.

    \n\n
    Returns:
    \n\n
    \n

    Server information including name, ID, and configuration.

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         server = await client.get_server_info()\n...         print(f"Server {server.name} running version {server.version}")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.Server]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_server", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_server", "kind": "function", "doc": "

    Rename the server.

    \n\n
    Arguments:
    \n\n
      \n
    • name: New server name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         success = await client.rename_server("My VPN Server")\n...         if success:\n...             print("Server renamed successfully")\n
    \n
    \n
    \n", "signature": "(self, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_hostname", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_hostname", "kind": "function", "doc": "

    Set server hostname for access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • hostname: New hostname or IP address
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If hostname is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_hostname("vpn.example.com")\n...         # Or use IP address\n...         await client.set_hostname("203.0.113.1")\n
    \n
    \n
    \n", "signature": "(self, hostname: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_default_port", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_default_port", "kind": "function", "doc": "

    Set default port for new access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • port: Port number (1025-65535)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If port is invalid or in use
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.set_default_port(8388)\n
    \n
    \n
    \n", "signature": "(self, port: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_metrics_status", "kind": "function", "doc": "

    Get whether metrics collection is enabled.

    \n\n
    Returns:
    \n\n
    \n

    Current metrics collection status

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         status = await client.get_metrics_status()\n...         if status.metrics_enabled:\n...             print("Metrics collection is enabled")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.MetricsStatusResponse]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_metrics_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_metrics_status", "kind": "function", "doc": "

    Enable or disable metrics collection.

    \n\n
    Arguments:
    \n\n
      \n
    • enabled: Whether to enable metrics
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Enable metrics\n...         await client.set_metrics_status(True)\n...         # Check new status\n...         status = await client.get_metrics_status()\n
    \n
    \n
    \n", "signature": "(self, enabled: bool) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_transfer_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_transfer_metrics", "kind": "function", "doc": "

    Get transfer metrics for all access keys.

    \n\n
    Returns:
    \n\n
    \n

    Transfer metrics data for each access key

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         metrics = await client.get_transfer_metrics()\n...         for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items():\n...             print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.ServerMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_experimental_metrics", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_experimental_metrics", "kind": "function", "doc": "

    Get experimental server metrics.

    \n\n
    Arguments:
    \n\n
      \n
    • since: Required time range filter (e.g., \"24h\", \"7d\", \"30d\", or ISO timestamp)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Detailed server and access key metrics

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Get metrics for the last 24 hours\n...         metrics = await client.get_experimental_metrics("24h")\n...         print(f"Server tunnel time: {metrics.server.tunnel_time.seconds}s")\n...         print(f"Server data transferred: {metrics.server.data_transferred.bytes} bytes")\n...\n...         # Get metrics for the last 7 days\n...         metrics = await client.get_experimental_metrics("7d")\n...\n...         # Get metrics since specific timestamp\n...         metrics = await client.get_experimental_metrics("2024-01-01T00:00:00Z")\n
    \n
    \n
    \n", "signature": "(\tself,\tsince: str) -> Union[dict[str, Any], pyoutlineapi.models.ExperimentalMetrics]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key", "kind": "function", "doc": "

    Create a new access key.

    \n\n
    Arguments:
    \n\n
      \n
    • name: Optional key name
    • \n
    • password: Optional password
    • \n
    • port: Optional port number (1-65535)
    • \n
    • method: Optional encryption method
    • \n
    • limit: Optional data transfer limit
    • \n
    \n\n
    Returns:
    \n\n
    \n

    New access key details

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Create basic key\n...         key = await client.create_access_key(name="User 1")\n...\n...         # Create key with data limit\n...         lim = DataLimit(bytes=5 * 1024**3)  # 5 GB\n...         key = await client.create_access_key(\n...             name="Limited User",\n...             port=8388,\n...             limit=lim\n...         )\n...         print(f"Created key: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.create_access_key_with_id", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create_access_key_with_id", "kind": "function", "doc": "

    Create a new access key with specific ID.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Specific ID for the access key
    • \n
    • name: Optional key name
    • \n
    • password: Optional password
    • \n
    • port: Optional port number (1-65535)
    • \n
    • method: Optional encryption method
    • \n
    • limit: Optional data transfer limit
    • \n
    \n\n
    Returns:
    \n\n
    \n

    New access key details

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.create_access_key_with_id(\n...             "my-custom-id",\n...             name="Custom Key"\n...         )\n
    \n
    \n
    \n", "signature": "(\tself,\tkey_id: str,\t*,\tname: Optional[str] = None,\tpassword: Optional[str] = None,\tport: Optional[int] = None,\tmethod: Optional[str] = None,\tlimit: Optional[pyoutlineapi.models.DataLimit] = None) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_keys", "kind": "function", "doc": "

    Get all access keys.

    \n\n
    Returns:
    \n\n
    \n

    List of all access keys

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         keys = await client.get_access_keys()\n...         for key in keys.access_keys:\n...             print(f"Key {key.id}: {key.name or 'unnamed'}")\n...             if key.data_limit:\n...                 print(f"  Limit: {key.data_limit.bytes / 1024**3:.1f} GB")\n
    \n
    \n
    \n", "signature": "(self) -> Union[dict[str, Any], pyoutlineapi.models.AccessKeyList]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_access_key", "kind": "function", "doc": "

    Get specific access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Access key details

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         key = await client.get_access_key("1")\n...         print(f"Port: {key.port}")\n...         print(f"URL: {key.access_url}")\n
    \n
    \n
    \n", "signature": "(\tself,\tkey_id: str) -> Union[dict[str, Any], pyoutlineapi.models.AccessKey]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.rename_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.rename_access_key", "kind": "function", "doc": "

    Rename access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • name: New name
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Rename key\n...         await client.rename_access_key("1", "Alice")\n...\n...         # Verify new name\n...         key = await client.get_access_key("1")\n...         assert key.name == "Alice"\n
    \n
    \n
    \n", "signature": "(self, key_id: str, name: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.delete_access_key", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.delete_access_key", "kind": "function", "doc": "

    Delete access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         if await client.delete_access_key("1"):\n...             print("Key deleted")\n
    \n
    \n
    \n", "signature": "(self, key_id: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_access_key_data_limit", "kind": "function", "doc": "

    Set data transfer limit for access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    • bytes_limit: Limit in bytes (must be non-negative)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist or limit is invalid
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 5 GB limit\n...         limit = 5 * 1024**3  # 5 GB in bytes\n...         await client.set_access_key_data_limit("1", limit)\n...\n...         # Verify limit\n...         key = await client.get_access_key("1")\n...         assert key.data_limit and key.data_limit.bytes == limit\n
    \n
    \n
    \n", "signature": "(self, key_id: str, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_access_key_data_limit", "kind": "function", "doc": "

    Remove data transfer limit from access key.

    \n\n
    Arguments:
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Raises:
    \n\n
      \n
    • APIError: If key doesn't exist
    • \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.remove_access_key_data_limit("1")\n
    \n
    \n
    \n", "signature": "(self, key_id: str) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.set_global_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.set_global_data_limit", "kind": "function", "doc": "

    Set global data transfer limit for all access keys.

    \n\n
    Arguments:
    \n\n
      \n
    • bytes_limit: Limit in bytes (must be non-negative)
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         # Set 100 GB global limit\n...         await client.set_global_data_limit(100 * 1024**3)\n
    \n
    \n
    \n", "signature": "(self, bytes_limit: int) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.remove_global_data_limit", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.remove_global_data_limit", "kind": "function", "doc": "

    Remove global data transfer limit.

    \n\n
    Returns:
    \n\n
    \n

    True if successful

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         await client.remove_global_data_limit()\n
    \n
    \n
    \n", "signature": "(self) -> bool:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.batch_create_access_keys", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.batch_create_access_keys", "kind": "function", "doc": "

    Create multiple access keys in batch.

    \n\n
    Arguments:
    \n\n
      \n
    • keys_config: List of key configurations (same as create_access_key kwargs)
    • \n
    • fail_fast: If True, stop on first error. If False, continue and return errors.
    • \n
    \n\n
    Returns:
    \n\n
    \n

    List of created keys or exceptions

    \n
    \n\n
    Examples:
    \n\n
    \n
    \n
    >>> async def main():\n...     async with AsyncOutlineClient(\n...         "https://example.com:1234/secret",\n...         "ab12cd34..."\n...     ) as client:\n...         configs = [\n...             {"name": "User1", "limit": DataLimit(bytes=1024**3)},\n...             {"name": "User2", "port": 8388},\n...         ]\n...         res = await client.batch_create_access_keys(configs)\n
    \n
    \n
    \n", "signature": "(\tself,\tkeys_config: list[dict[str, typing.Any]],\tfail_fast: bool = True) -> list[typing.Union[pyoutlineapi.models.AccessKey, Exception]]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_summary", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_summary", "kind": "function", "doc": "

    Get comprehensive server summary including info, metrics, and key count.

    \n\n
    Arguments:
    \n\n
      \n
    • metrics_since: Time range for experimental metrics (default: \"24h\")
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Dictionary with server info, health status, and statistics

    \n
    \n", "signature": "(self, metrics_since: str = '24h') -> dict[str, typing.Any]:", "funcdef": "async def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.configure_logging", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.configure_logging", "kind": "function", "doc": "

    Configure logging for the client.

    \n\n
    Arguments:
    \n\n
      \n
    • level: Logging level (DEBUG, INFO, WARNING, ERROR)
    • \n
    • format_string: Custom format string for log messages
    • \n
    \n", "signature": "(self, level: str = 'INFO', format_string: Optional[str] = None) -> None:", "funcdef": "def"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.is_healthy", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.is_healthy", "kind": "variable", "doc": "

    Check if the last health check passed.

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.session", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.session", "kind": "variable", "doc": "

    Access the current client session.

    \n", "annotation": ": Optional[aiohttp.client.ClientSession]"}, {"fullname": "pyoutlineapi.AsyncOutlineClient.api_url", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.api_url", "kind": "variable", "doc": "

    Get the API URL (without sensitive parts).

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

    Base exception for Outline client errors.

    \n", "bases": "builtins.Exception"}, {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

    Raised when API requests fail.

    \n", "bases": "pyoutlineapi.exceptions.OutlineError"}, {"fullname": "pyoutlineapi.APIError.__init__", "modulename": "pyoutlineapi", "qualname": "APIError.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tmessage: str,\tstatus_code: Optional[int] = None,\tattempt: Optional[int] = None)"}, {"fullname": "pyoutlineapi.APIError.status_code", "modulename": "pyoutlineapi", "qualname": "APIError.status_code", "kind": "variable", "doc": "

    \n"}, {"fullname": "pyoutlineapi.APIError.attempt", "modulename": "pyoutlineapi", "qualname": "APIError.attempt", "kind": "variable", "doc": "

    \n"}, {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

    Access key details.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKey.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKey.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

    Request parameters for creating an access key.\nPer OpenAPI: /access-keys POST request body

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[int]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

    List of access keys.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKey]"}, {"fullname": "pyoutlineapi.AccessKeyList.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyNameRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyNameRequest", "kind": "class", "doc": "

    Request for renaming access key.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.AccessKeyNameRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyNameRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.AccessKeyNameRequest.model_config", "modulename": "pyoutlineapi", "qualname": "AccessKeyNameRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

    Data transfer limit configuration.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.DataLimit.validate_bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.validate_bytes", "kind": "function", "doc": "

    Wrap a classmethod, staticmethod, property or unbound function\nand act as a descriptor that allows us to detect decorated items\nfrom the class' attributes.

    \n\n

    This class' __get__ returns the wrapped item's __get__ result,\nwhich makes it transparent for classmethods and staticmethods.

    \n\n
    Attributes:
    \n\n
      \n
    • wrapped: The decorator that has to be wrapped.
    • \n
    • decorator_info: The decorator info.
    • \n
    • shim: A wrapper function to wrap V1 style function.
    • \n
    \n", "signature": "(unknown):", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimitRequest", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest", "kind": "class", "doc": "

    Request for setting data limit.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.DataLimitRequest.limit", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataLimit"}, {"fullname": "pyoutlineapi.DataLimitRequest.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

    Error response structure.\nPer OpenAPI: 404 and 400 responses

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ErrorResponse.model_config", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

    Experimental metrics data structure.\nPer OpenAPI: /experimental/server/metrics endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.ServerExperimentalMetric"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]"}, {"fullname": "pyoutlineapi.ExperimentalMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.HostnameRequest", "modulename": "pyoutlineapi", "qualname": "HostnameRequest", "kind": "class", "doc": "

    Request for changing hostname.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.HostnameRequest.hostname", "modulename": "pyoutlineapi", "qualname": "HostnameRequest.hostname", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.HostnameRequest.model_config", "modulename": "pyoutlineapi", "qualname": "HostnameRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsEnabledRequest", "modulename": "pyoutlineapi", "qualname": "MetricsEnabledRequest", "kind": "class", "doc": "

    Request for enabling/disabling metrics.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsEnabledRequest.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsEnabledRequest.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsEnabledRequest.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsEnabledRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

    Response for /metrics/enabled endpoint.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.MetricsStatusResponse.model_config", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.PortRequest", "modulename": "pyoutlineapi", "qualname": "PortRequest", "kind": "class", "doc": "

    Request for changing default port.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.PortRequest.port", "modulename": "pyoutlineapi", "qualname": "PortRequest.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.PortRequest.model_config", "modulename": "pyoutlineapi", "qualname": "PortRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

    Server information.\nPer OpenAPI: /server endpoint schema

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[str]"}, {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[pyoutlineapi.models.DataLimit]"}, {"fullname": "pyoutlineapi.Server.model_config", "modulename": "pyoutlineapi", "qualname": "Server.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

    Server metrics data for data transferred per access key.\nPer OpenAPI: /metrics/transfer endpoint

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, int]"}, {"fullname": "pyoutlineapi.ServerMetrics.model_config", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerNameRequest", "modulename": "pyoutlineapi", "qualname": "ServerNameRequest", "kind": "class", "doc": "

    Request for renaming server.

    \n", "bases": "pydantic.main.BaseModel"}, {"fullname": "pyoutlineapi.ServerNameRequest.name", "modulename": "pyoutlineapi", "qualname": "ServerNameRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "pyoutlineapi.ServerNameRequest.model_config", "modulename": "pyoutlineapi", "qualname": "ServerNameRequest.model_config", "kind": "variable", "doc": "

    Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

    \n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}]; + /** pdoc search index */const docs = {"version": "0.9.5", "fields": ["qualname", "fullname", "annotation", "default_value", "signature", "bases", "doc"], "ref": "fullname", "documentStore": {"docs": {"pyoutlineapi": {"fullname": "pyoutlineapi", "modulename": "pyoutlineapi", "kind": "module", "doc": "

    PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.

    \n\n

    Copyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru\nAll rights reserved.

    \n\n

    This software is licensed under the MIT License.

    \n\n
    You can find the full license text at:
    \n\n
    \n

    https://opensource.org/licenses/MIT

    \n
    \n\n
    Source code repository:
    \n\n
    \n

    https://github.com/orenlab/pyoutlineapi

    \n
    \n\n

    Quick Start:

    \n\n
    \n
    from pyoutlineapi import AsyncOutlineClient\n\n# From environment variables\nasync with AsyncOutlineClient.from_env() as client:\n    server = await client.get_server_info()\n    print(f"Server: {server.name}")\n\n# Prefer from_env for production usage\nasync with AsyncOutlineClient.from_env() as client:\n    keys = await client.get_access_keys()\n
    \n
    \n\n

    Advanced Usage - Type Hints:

    \n\n
    \n
    from pyoutlineapi import (\n    AsyncOutlineClient,\n    AuditLogger,\n    AuditDetails,\n    MetricsCollector,\n    MetricsTags,\n)\n\nclass CustomAuditLogger:\n    def log_action(\n        self,\n        action: str,\n        resource: str,\n        *,\n        user: str | None = None,\n        details: AuditDetails | None = None,\n        correlation_id: str | None = None,\n    ) -> None:\n        print(f"[AUDIT] {action} on {resource}")\n\nasync with AsyncOutlineClient.from_env(\n    audit_logger=CustomAuditLogger(),\n) as client:\n    await client.create_access_key(name="test")\n
    \n
    \n"}, "pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"fullname": "pyoutlineapi.DEFAULT_SENSITIVE_KEYS", "modulename": "pyoutlineapi", "qualname": "DEFAULT_SENSITIVE_KEYS", "kind": "variable", "doc": "

    \n", "default_value": "frozenset({'secret', 'accessUrl', 'cert_sha256', 'api_key', 'api_url', 'access_url', 'authorization', 'token', 'password', 'apikey', 'apiKey', 'apiUrl', 'certSha256'})"}, "pyoutlineapi.APIError": {"fullname": "pyoutlineapi.APIError", "modulename": "pyoutlineapi", "qualname": "APIError", "kind": "class", "doc": "

    HTTP API request failure.

    \n\n

    Automatically determines retry eligibility based on HTTP status code.

    \n\n
    Attributes:
    \n\n
      \n
    • status_code: HTTP status code (if available)
    • \n
    • endpoint: API endpoint that failed
    • \n
    • response_data: Raw response data (may contain sensitive info)
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = APIError("Not found", status_code=404, endpoint="/server")\n>>> error.is_client_error  # True\n>>> error.is_retryable  # False\n
    \n
    \n
    \n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.APIError.__init__": {"fullname": "pyoutlineapi.APIError.__init__", "modulename": "pyoutlineapi", "qualname": "APIError.__init__", "kind": "function", "doc": "

    Initialize API error with sanitized endpoint.

    \n\n
    Arguments:
    \n\n
      \n
    • message: Error message
    • \n
    • status_code: HTTP status code
    • \n
    • endpoint: API endpoint (will be sanitized)
    • \n
    • response_data: Response data (may contain sensitive info)
    • \n
    \n", "signature": "(\tmessage: str,\t*,\tstatus_code: int | None = None,\tendpoint: str | None = None,\tresponse_data: dict[str, typing.Any] | None = None)"}, "pyoutlineapi.APIError.status_code": {"fullname": "pyoutlineapi.APIError.status_code", "modulename": "pyoutlineapi", "qualname": "APIError.status_code", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.APIError.endpoint": {"fullname": "pyoutlineapi.APIError.endpoint", "modulename": "pyoutlineapi", "qualname": "APIError.endpoint", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.APIError.response_data": {"fullname": "pyoutlineapi.APIError.response_data", "modulename": "pyoutlineapi", "qualname": "APIError.response_data", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.APIError.is_retryable": {"fullname": "pyoutlineapi.APIError.is_retryable", "modulename": "pyoutlineapi", "qualname": "APIError.is_retryable", "kind": "variable", "doc": "

    Check if error is retryable based on status code.

    \n", "annotation": ": bool"}, "pyoutlineapi.APIError.is_client_error": {"fullname": "pyoutlineapi.APIError.is_client_error", "modulename": "pyoutlineapi", "qualname": "APIError.is_client_error", "kind": "variable", "doc": "

    Check if error is a client error (4xx status).

    \n\n
    Returns:
    \n\n
    \n

    True if status code is 400-499

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.APIError.is_server_error": {"fullname": "pyoutlineapi.APIError.is_server_error", "modulename": "pyoutlineapi", "qualname": "APIError.is_server_error", "kind": "variable", "doc": "

    Check if error is a server error (5xx status).

    \n\n
    Returns:
    \n\n
    \n

    True if status code is 500-599

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.APIError.is_rate_limit_error": {"fullname": "pyoutlineapi.APIError.is_rate_limit_error", "modulename": "pyoutlineapi", "qualname": "APIError.is_rate_limit_error", "kind": "variable", "doc": "

    Check if error is a rate limit error (429 status).

    \n\n
    Returns:
    \n\n
    \n

    True if status code is 429

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.AccessKey": {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "

    Access key model matching API schema with optimized properties.

    \n\n

    SCHEMA: Based on OpenAPI /access-keys endpoint

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.AccessKey.id": {"fullname": "pyoutlineapi.AccessKey.id", "modulename": "pyoutlineapi", "qualname": "AccessKey.id", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKey.name": {"fullname": "pyoutlineapi.AccessKey.name", "modulename": "pyoutlineapi", "qualname": "AccessKey.name", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.AccessKey.password": {"fullname": "pyoutlineapi.AccessKey.password", "modulename": "pyoutlineapi", "qualname": "AccessKey.password", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKey.port": {"fullname": "pyoutlineapi.AccessKey.port", "modulename": "pyoutlineapi", "qualname": "AccessKey.port", "kind": "variable", "doc": "

    Port number (1-65535)

    \n", "annotation": ": Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])]", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKey.method": {"fullname": "pyoutlineapi.AccessKey.method", "modulename": "pyoutlineapi", "qualname": "AccessKey.method", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKey.access_url": {"fullname": "pyoutlineapi.AccessKey.access_url", "modulename": "pyoutlineapi", "qualname": "AccessKey.access_url", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKey.data_limit": {"fullname": "pyoutlineapi.AccessKey.data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataLimit | None", "default_value": "None"}, "pyoutlineapi.AccessKey.validate_name": {"fullname": "pyoutlineapi.AccessKey.validate_name", "modulename": "pyoutlineapi", "qualname": "AccessKey.validate_name", "kind": "function", "doc": "

    Handle empty names from API.

    \n\n
    Parameters
    \n\n
      \n
    • v: Name value
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated name or None

    \n
    \n", "signature": "(cls, v: str | None) -> str | None:", "funcdef": "def"}, "pyoutlineapi.AccessKey.validate_id": {"fullname": "pyoutlineapi.AccessKey.validate_id", "modulename": "pyoutlineapi", "qualname": "AccessKey.validate_id", "kind": "function", "doc": "

    Validate key ID.

    \n\n
    Parameters
    \n\n
      \n
    • v: Key ID
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated key ID

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If ID is invalid
    • \n
    \n", "signature": "(cls, v: str) -> str:", "funcdef": "def"}, "pyoutlineapi.AccessKey.has_data_limit": {"fullname": "pyoutlineapi.AccessKey.has_data_limit", "modulename": "pyoutlineapi", "qualname": "AccessKey.has_data_limit", "kind": "variable", "doc": "

    Check if key has data limit (optimized None check).

    \n\n
    Returns
    \n\n
    \n

    True if data limit exists

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.AccessKey.display_name": {"fullname": "pyoutlineapi.AccessKey.display_name", "modulename": "pyoutlineapi", "qualname": "AccessKey.display_name", "kind": "variable", "doc": "

    Get display name with optimized conditional.

    \n\n
    Returns
    \n\n
    \n

    Display name

    \n
    \n", "annotation": ": str"}, "pyoutlineapi.AccessKeyCreateRequest": {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "

    Request model for creating access keys.

    \n\n

    SCHEMA: Based on POST /access-keys request body

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.AccessKeyCreateRequest.name": {"fullname": "pyoutlineapi.AccessKeyCreateRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.AccessKeyCreateRequest.method": {"fullname": "pyoutlineapi.AccessKeyCreateRequest.method", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.method", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.AccessKeyCreateRequest.password": {"fullname": "pyoutlineapi.AccessKeyCreateRequest.password", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.password", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.AccessKeyCreateRequest.port": {"fullname": "pyoutlineapi.AccessKeyCreateRequest.port", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.port", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])]]", "default_value": "None"}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"fullname": "pyoutlineapi.AccessKeyCreateRequest.limit", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataLimit | None", "default_value": "None"}, "pyoutlineapi.AccessKeyList": {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "

    List of access keys with optimized utility methods.

    \n\n

    SCHEMA: Based on GET /access-keys response

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.AccessKeyList.access_keys": {"fullname": "pyoutlineapi.AccessKeyList.access_keys", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKey]", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKeyList.count": {"fullname": "pyoutlineapi.AccessKeyList.count", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.count", "kind": "variable", "doc": "

    Get number of access keys (cached).

    \n\n

    NOTE: Cached because list is immutable after creation

    \n\n
    Returns
    \n\n
    \n

    Key count

    \n
    \n", "annotation": ": int"}, "pyoutlineapi.AccessKeyList.is_empty": {"fullname": "pyoutlineapi.AccessKeyList.is_empty", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.is_empty", "kind": "variable", "doc": "

    Check if list is empty (uses cached count).

    \n\n
    Returns
    \n\n
    \n

    True if no keys

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.AccessKeyList.get_by_id": {"fullname": "pyoutlineapi.AccessKeyList.get_by_id", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.get_by_id", "kind": "function", "doc": "

    Get key by ID with early return optimization.

    \n\n
    Parameters
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns
    \n\n
    \n

    Access key or None if not found

    \n
    \n", "signature": "(self, key_id: str) -> pyoutlineapi.models.AccessKey | None:", "funcdef": "def"}, "pyoutlineapi.AccessKeyList.get_by_name": {"fullname": "pyoutlineapi.AccessKeyList.get_by_name", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.get_by_name", "kind": "function", "doc": "

    Get keys by name with optimized list comprehension.

    \n\n
    Parameters
    \n\n
      \n
    • name: Key name
    • \n
    \n\n
    Returns
    \n\n
    \n

    List of matching keys (may be multiple)

    \n
    \n", "signature": "(self, name: str) -> list[pyoutlineapi.models.AccessKey]:", "funcdef": "def"}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"fullname": "pyoutlineapi.AccessKeyList.filter_with_limits", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.filter_with_limits", "kind": "function", "doc": "

    Get keys with data limits (optimized comprehension).

    \n\n
    Returns
    \n\n
    \n

    List of keys with limits

    \n
    \n", "signature": "(self) -> list[pyoutlineapi.models.AccessKey]:", "funcdef": "def"}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"fullname": "pyoutlineapi.AccessKeyList.filter_without_limits", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.filter_without_limits", "kind": "function", "doc": "

    Get keys without data limits (optimized comprehension).

    \n\n
    Returns
    \n\n
    \n

    List of keys without limits

    \n
    \n", "signature": "(self) -> list[pyoutlineapi.models.AccessKey]:", "funcdef": "def"}, "pyoutlineapi.AccessKeyMetric": {"fullname": "pyoutlineapi.AccessKeyMetric", "modulename": "pyoutlineapi", "qualname": "AccessKeyMetric", "kind": "class", "doc": "

    Per-key experimental metrics.

    \n\n

    SCHEMA: Based on experimental metrics accessKeys array item

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"fullname": "pyoutlineapi.AccessKeyMetric.access_key_id", "modulename": "pyoutlineapi", "qualname": "AccessKeyMetric.access_key_id", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"fullname": "pyoutlineapi.AccessKeyMetric.tunnel_time", "modulename": "pyoutlineapi", "qualname": "AccessKeyMetric.tunnel_time", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.TunnelTime", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"fullname": "pyoutlineapi.AccessKeyMetric.data_transferred", "modulename": "pyoutlineapi", "qualname": "AccessKeyMetric.data_transferred", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataTransferred", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKeyMetric.connection": {"fullname": "pyoutlineapi.AccessKeyMetric.connection", "modulename": "pyoutlineapi", "qualname": "AccessKeyMetric.connection", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.ConnectionInfo", "default_value": "PydanticUndefined"}, "pyoutlineapi.AccessKeyNameRequest": {"fullname": "pyoutlineapi.AccessKeyNameRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyNameRequest", "kind": "class", "doc": "

    Request model for renaming access key.

    \n\n

    SCHEMA: Based on PUT /access-keys/{id}/name request body

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.AccessKeyNameRequest.name": {"fullname": "pyoutlineapi.AccessKeyNameRequest.name", "modulename": "pyoutlineapi", "qualname": "AccessKeyNameRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.AsyncOutlineClient": {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "

    High-performance async client for Outline VPN Server API.

    \n", "bases": "pyoutlineapi.base_client.BaseHTTPClient, pyoutlineapi.api_mixins.ServerMixin, pyoutlineapi.api_mixins.AccessKeyMixin, pyoutlineapi.api_mixins.DataLimitMixin, pyoutlineapi.api_mixins.MetricsMixin"}, "pyoutlineapi.AsyncOutlineClient.__init__": {"fullname": "pyoutlineapi.AsyncOutlineClient.__init__", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.__init__", "kind": "function", "doc": "

    Initialize Outline client with modern configuration approach.

    \n\n

    Uses structural pattern matching for configuration resolution.

    \n\n
    Parameters
    \n\n
      \n
    • config: Client configuration object
    • \n
    • api_url: API URL (alternative to config)
    • \n
    • cert_sha256: Certificate fingerprint (alternative to config)
    • \n
    • audit_logger: Custom audit logger
    • \n
    • metrics: Custom metrics collector
    • \n
    • overrides: Configuration overrides (timeout, retry_attempts, etc.)
    • \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If configuration is invalid
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> async with AsyncOutlineClient.from_env() as client:\n...     info = await client.get_server_info()\n
    \n
    \n
    \n", "signature": "(\tconfig: pyoutlineapi.config.OutlineClientConfig | None = None,\t*,\tapi_url: str | None = None,\tcert_sha256: str | None = None,\taudit_logger: pyoutlineapi.audit.AuditLogger | None = None,\tmetrics: pyoutlineapi.base_client.MetricsCollector | None = None,\t**overrides: Unpack[pyoutlineapi.common_types.ConfigOverrides])"}, "pyoutlineapi.AsyncOutlineClient.config": {"fullname": "pyoutlineapi.AsyncOutlineClient.config", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.config", "kind": "variable", "doc": "

    Get immutable copy of configuration.

    \n\n
    Returns
    \n\n
    \n

    Deep copy of configuration

    \n
    \n", "annotation": ": pyoutlineapi.config.OutlineClientConfig"}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"fullname": "pyoutlineapi.AsyncOutlineClient.get_sanitized_config", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_sanitized_config", "kind": "variable", "doc": "

    Delegate to config's sanitized representation.

    \n\n

    See: OutlineClientConfig.get_sanitized_config().

    \n\n
    Returns
    \n\n
    \n

    Sanitized configuration from underlying config object

    \n
    \n", "annotation": ": dict[str, typing.Any]"}, "pyoutlineapi.AsyncOutlineClient.json_format": {"fullname": "pyoutlineapi.AsyncOutlineClient.json_format", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.json_format", "kind": "variable", "doc": "

    Get JSON format preference.

    \n\n
    Returns
    \n\n
    \n

    True if raw JSON format is preferred

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.AsyncOutlineClient.create": {"fullname": "pyoutlineapi.AsyncOutlineClient.create", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create", "kind": "function", "doc": "

    Create and initialize client as async context manager.

    \n\n

    Automatically handles initialization and cleanup.\nRecommended way to create clients in async contexts.

    \n\n
    Parameters
    \n\n
      \n
    • api_url: API URL
    • \n
    • cert_sha256: Certificate fingerprint
    • \n
    • config: Configuration object
    • \n
    • audit_logger: Custom audit logger
    • \n
    • metrics: Custom metrics collector
    • \n
    • overrides: Configuration overrides (timeout, retry_attempts, etc.)\n:yield: Initialized client instance
    • \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If configuration is invalid
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> async with AsyncOutlineClient.from_env() as client:\n...     keys = await client.get_access_keys()\n
    \n
    \n
    \n", "signature": "(\tcls,\tapi_url: str | None = None,\tcert_sha256: str | None = None,\t*,\tconfig: pyoutlineapi.config.OutlineClientConfig | None = None,\taudit_logger: pyoutlineapi.audit.AuditLogger | None = None,\tmetrics: pyoutlineapi.base_client.MetricsCollector | None = None,\t**overrides: Unpack[pyoutlineapi.common_types.ConfigOverrides]) -> AsyncGenerator[pyoutlineapi.client.AsyncOutlineClient, None]:", "funcdef": "def"}, "pyoutlineapi.AsyncOutlineClient.from_env": {"fullname": "pyoutlineapi.AsyncOutlineClient.from_env", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.from_env", "kind": "function", "doc": "

    Create client from environment variables.

    \n\n

    Reads configuration from environment or .env file.\nModern approach using **overrides for runtime configuration.

    \n\n
    Parameters
    \n\n
      \n
    • env_file: Path to environment file (.env)
    • \n
    • audit_logger: Custom audit logger
    • \n
    • metrics: Custom metrics collector
    • \n
    • overrides: Configuration overrides (timeout, enable_logging, etc.)
    • \n
    \n\n
    Returns
    \n\n
    \n

    Configured client instance

    \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If environment configuration is invalid
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> async with AsyncOutlineClient.from_env(\n...     env_file=".env.production",\n...     timeout=20,\n... ) as client:\n...     info = await client.get_server_info()\n
    \n
    \n
    \n", "signature": "(\tcls,\t*,\tenv_file: str | pathlib._local.Path | None = None,\taudit_logger: pyoutlineapi.audit.AuditLogger | None = None,\tmetrics: pyoutlineapi.base_client.MetricsCollector | None = None,\t**overrides: Unpack[pyoutlineapi.common_types.ConfigOverrides]) -> pyoutlineapi.client.AsyncOutlineClient:", "funcdef": "def"}, "pyoutlineapi.AsyncOutlineClient.health_check": {"fullname": "pyoutlineapi.AsyncOutlineClient.health_check", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.health_check", "kind": "function", "doc": "

    Perform basic health check.

    \n\n

    Non-intrusive check that tests server connectivity without\nmodifying any state. Returns comprehensive health metrics.

    \n\n
    Returns
    \n\n
    \n

    Health check result dictionary with response time

    \n
    \n\n
    Example result:
    \n\n
    \n

    {\n \"timestamp\": 1234567890.123,\n \"healthy\": True,\n \"response_time_ms\": 45.2,\n \"connected\": True,\n \"circuit_state\": \"closed\",\n \"active_requests\": 2,\n \"rate_limit_available\": 98\n }

    \n
    \n", "signature": "(self) -> dict[str, typing.Any]:", "funcdef": "async def"}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"fullname": "pyoutlineapi.AsyncOutlineClient.get_server_summary", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_server_summary", "kind": "function", "doc": "

    Get comprehensive server overview.

    \n\n

    Aggregates multiple API calls into a single summary.\nContinues on partial failures to return maximum information.\nExecutes non-dependent calls concurrently for performance.

    \n\n
    Returns
    \n\n
    \n

    Server summary dictionary with aggregated data

    \n
    \n\n
    Example result:
    \n\n
    \n

    {\n \"timestamp\": 1234567890.123,\n \"healthy\": True,\n \"server\": {...},\n \"access_keys_count\": 10,\n \"metrics_enabled\": True,\n \"transfer_metrics\": {...},\n \"client_status\": {...},\n \"errors\": []\n }

    \n
    \n", "signature": "(self) -> dict[str, typing.Any]:", "funcdef": "async def"}, "pyoutlineapi.AsyncOutlineClient.get_status": {"fullname": "pyoutlineapi.AsyncOutlineClient.get_status", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.get_status", "kind": "function", "doc": "

    Get current client status (synchronous).

    \n\n

    Returns immediate status without making API calls.\nUseful for monitoring and debugging.

    \n\n
    Returns
    \n\n
    \n

    Status dictionary with all client metrics

    \n
    \n\n
    Example result:
    \n\n
    \n

    {\n \"connected\": True,\n \"circuit_state\": \"closed\",\n \"active_requests\": 2,\n \"rate_limit\": {\n \"limit\": 100,\n \"available\": 98,\n \"active\": 2\n },\n \"circuit_metrics\": {...}\n }

    \n
    \n", "signature": "(self) -> dict[str, typing.Any]:", "funcdef": "def"}, "pyoutlineapi.AuditContext": {"fullname": "pyoutlineapi.AuditContext", "modulename": "pyoutlineapi", "qualname": "AuditContext", "kind": "class", "doc": "

    Immutable audit context extracted from function call.

    \n\n

    Uses structural pattern matching and signature inspection for smart extraction.

    \n"}, "pyoutlineapi.AuditContext.__init__": {"fullname": "pyoutlineapi.AuditContext.__init__", "modulename": "pyoutlineapi", "qualname": "AuditContext.__init__", "kind": "function", "doc": "

    \n", "signature": "(\taction: str,\tresource: str,\tsuccess: bool,\tdetails: dict[str, typing.Any] = <factory>,\tcorrelation_id: str | None = None)"}, "pyoutlineapi.AuditContext.action": {"fullname": "pyoutlineapi.AuditContext.action", "modulename": "pyoutlineapi", "qualname": "AuditContext.action", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, "pyoutlineapi.AuditContext.resource": {"fullname": "pyoutlineapi.AuditContext.resource", "modulename": "pyoutlineapi", "qualname": "AuditContext.resource", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, "pyoutlineapi.AuditContext.success": {"fullname": "pyoutlineapi.AuditContext.success", "modulename": "pyoutlineapi", "qualname": "AuditContext.success", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, "pyoutlineapi.AuditContext.details": {"fullname": "pyoutlineapi.AuditContext.details", "modulename": "pyoutlineapi", "qualname": "AuditContext.details", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, typing.Any]"}, "pyoutlineapi.AuditContext.correlation_id": {"fullname": "pyoutlineapi.AuditContext.correlation_id", "modulename": "pyoutlineapi", "qualname": "AuditContext.correlation_id", "kind": "variable", "doc": "

    \n", "annotation": ": str | None"}, "pyoutlineapi.AuditContext.from_call": {"fullname": "pyoutlineapi.AuditContext.from_call", "modulename": "pyoutlineapi", "qualname": "AuditContext.from_call", "kind": "function", "doc": "

    Build audit context from function call with intelligent extraction.

    \n\n
    Parameters
    \n\n
      \n
    • func: Function being audited
    • \n
    • instance: Instance (self) for methods
    • \n
    • args: Positional arguments
    • \n
    • kwargs: Keyword arguments
    • \n
    • result: Function result (if successful)
    • \n
    • exception: Exception (if failed)
    • \n
    \n\n
    Returns
    \n\n
    \n

    Complete audit context

    \n
    \n", "signature": "(\tcls,\tfunc: Callable[..., typing.Any],\tinstance: object,\targs: tuple[typing.Any, ...],\tkwargs: dict[str, typing.Any],\tresult: object = None,\texception: Exception | None = None) -> pyoutlineapi.audit.AuditContext:", "funcdef": "def"}, "pyoutlineapi.AuditLogger": {"fullname": "pyoutlineapi.AuditLogger", "modulename": "pyoutlineapi", "qualname": "AuditLogger", "kind": "class", "doc": "

    Protocol for audit logging implementations.

    \n\n

    Designed for async-first applications with sync fallback support.

    \n", "bases": "typing.Protocol"}, "pyoutlineapi.AuditLogger.__init__": {"fullname": "pyoutlineapi.AuditLogger.__init__", "modulename": "pyoutlineapi", "qualname": "AuditLogger.__init__", "kind": "function", "doc": "

    \n", "signature": "(*args, **kwargs)"}, "pyoutlineapi.AuditLogger.alog_action": {"fullname": "pyoutlineapi.AuditLogger.alog_action", "modulename": "pyoutlineapi", "qualname": "AuditLogger.alog_action", "kind": "function", "doc": "

    Log auditable action asynchronously (primary method).

    \n", "signature": "(\tself,\taction: str,\tresource: str,\t*,\tuser: str | None = None,\tdetails: dict[str, typing.Any] | None = None,\tcorrelation_id: str | None = None) -> None:", "funcdef": "async def"}, "pyoutlineapi.AuditLogger.log_action": {"fullname": "pyoutlineapi.AuditLogger.log_action", "modulename": "pyoutlineapi", "qualname": "AuditLogger.log_action", "kind": "function", "doc": "

    Log auditable action synchronously (fallback method).

    \n", "signature": "(\tself,\taction: str,\tresource: str,\t*,\tuser: str | None = None,\tdetails: dict[str, typing.Any] | None = None,\tcorrelation_id: str | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.AuditLogger.shutdown": {"fullname": "pyoutlineapi.AuditLogger.shutdown", "modulename": "pyoutlineapi", "qualname": "AuditLogger.shutdown", "kind": "function", "doc": "

    Gracefully shutdown logger.

    \n", "signature": "(self) -> None:", "funcdef": "async def"}, "pyoutlineapi.BandwidthData": {"fullname": "pyoutlineapi.BandwidthData", "modulename": "pyoutlineapi", "qualname": "BandwidthData", "kind": "class", "doc": "

    Bandwidth measurement data.

    \n\n

    SCHEMA: Based on experimental metrics bandwidth current/peak object

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.BandwidthData.data": {"fullname": "pyoutlineapi.BandwidthData.data", "modulename": "pyoutlineapi", "qualname": "BandwidthData.data", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.BandwidthDataValue", "default_value": "PydanticUndefined"}, "pyoutlineapi.BandwidthData.timestamp": {"fullname": "pyoutlineapi.BandwidthData.timestamp", "modulename": "pyoutlineapi", "qualname": "BandwidthData.timestamp", "kind": "variable", "doc": "

    \n", "annotation": ": Optional[Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in seconds', metadata=[Ge(ge=0)])]]", "default_value": "None"}, "pyoutlineapi.BandwidthDataValue": {"fullname": "pyoutlineapi.BandwidthDataValue", "modulename": "pyoutlineapi", "qualname": "BandwidthDataValue", "kind": "class", "doc": "

    Bandwidth data value.

    \n\n

    SCHEMA: Based on experimental metrics bandwidth data object

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.BandwidthDataValue.bytes": {"fullname": "pyoutlineapi.BandwidthDataValue.bytes", "modulename": "pyoutlineapi", "qualname": "BandwidthDataValue.bytes", "kind": "variable", "doc": "

    \n", "annotation": ": int", "default_value": "PydanticUndefined"}, "pyoutlineapi.BandwidthInfo": {"fullname": "pyoutlineapi.BandwidthInfo", "modulename": "pyoutlineapi", "qualname": "BandwidthInfo", "kind": "class", "doc": "

    Current and peak bandwidth information.

    \n\n

    SCHEMA: Based on experimental metrics bandwidth object

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.BandwidthInfo.current": {"fullname": "pyoutlineapi.BandwidthInfo.current", "modulename": "pyoutlineapi", "qualname": "BandwidthInfo.current", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.BandwidthData", "default_value": "PydanticUndefined"}, "pyoutlineapi.BandwidthInfo.peak": {"fullname": "pyoutlineapi.BandwidthInfo.peak", "modulename": "pyoutlineapi", "qualname": "BandwidthInfo.peak", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.BandwidthData", "default_value": "PydanticUndefined"}, "pyoutlineapi.CircuitConfig": {"fullname": "pyoutlineapi.CircuitConfig", "modulename": "pyoutlineapi", "qualname": "CircuitConfig", "kind": "class", "doc": "

    Circuit breaker configuration with validation.

    \n\n

    Immutable configuration to prevent runtime modification.\nUses slots for memory efficiency (~40 bytes per instance).

    \n"}, "pyoutlineapi.CircuitConfig.__init__": {"fullname": "pyoutlineapi.CircuitConfig.__init__", "modulename": "pyoutlineapi", "qualname": "CircuitConfig.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tfailure_threshold: int = 5,\trecovery_timeout: float = 60.0,\tsuccess_threshold: int = 2,\tcall_timeout: float = 10.0)"}, "pyoutlineapi.CircuitConfig.failure_threshold": {"fullname": "pyoutlineapi.CircuitConfig.failure_threshold", "modulename": "pyoutlineapi", "qualname": "CircuitConfig.failure_threshold", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.CircuitConfig.recovery_timeout": {"fullname": "pyoutlineapi.CircuitConfig.recovery_timeout", "modulename": "pyoutlineapi", "qualname": "CircuitConfig.recovery_timeout", "kind": "variable", "doc": "

    \n", "annotation": ": float"}, "pyoutlineapi.CircuitConfig.success_threshold": {"fullname": "pyoutlineapi.CircuitConfig.success_threshold", "modulename": "pyoutlineapi", "qualname": "CircuitConfig.success_threshold", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.CircuitConfig.call_timeout": {"fullname": "pyoutlineapi.CircuitConfig.call_timeout", "modulename": "pyoutlineapi", "qualname": "CircuitConfig.call_timeout", "kind": "variable", "doc": "

    \n", "annotation": ": float"}, "pyoutlineapi.CircuitMetrics": {"fullname": "pyoutlineapi.CircuitMetrics", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics", "kind": "class", "doc": "

    Circuit breaker metrics with efficient storage.

    \n\n

    Uses slots for memory efficiency (~80 bytes per instance).\nAll calculations are O(1) with no allocations.

    \n"}, "pyoutlineapi.CircuitMetrics.__init__": {"fullname": "pyoutlineapi.CircuitMetrics.__init__", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.__init__", "kind": "function", "doc": "

    \n", "signature": "(\ttotal_calls: int = 0,\tsuccessful_calls: int = 0,\tfailed_calls: int = 0,\tstate_changes: int = 0,\tlast_failure_time: float = 0.0,\tlast_success_time: float = 0.0)"}, "pyoutlineapi.CircuitMetrics.total_calls": {"fullname": "pyoutlineapi.CircuitMetrics.total_calls", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.total_calls", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.CircuitMetrics.successful_calls": {"fullname": "pyoutlineapi.CircuitMetrics.successful_calls", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.successful_calls", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.CircuitMetrics.failed_calls": {"fullname": "pyoutlineapi.CircuitMetrics.failed_calls", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.failed_calls", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.CircuitMetrics.state_changes": {"fullname": "pyoutlineapi.CircuitMetrics.state_changes", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.state_changes", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"fullname": "pyoutlineapi.CircuitMetrics.last_failure_time", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.last_failure_time", "kind": "variable", "doc": "

    \n", "annotation": ": float"}, "pyoutlineapi.CircuitMetrics.last_success_time": {"fullname": "pyoutlineapi.CircuitMetrics.last_success_time", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.last_success_time", "kind": "variable", "doc": "

    \n", "annotation": ": float"}, "pyoutlineapi.CircuitMetrics.success_rate": {"fullname": "pyoutlineapi.CircuitMetrics.success_rate", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.success_rate", "kind": "variable", "doc": "

    Calculate success rate (O(1), no allocations).

    \n\n
    Returns
    \n\n
    \n

    Success rate as decimal (0.0 to 1.0)

    \n
    \n", "annotation": ": float"}, "pyoutlineapi.CircuitMetrics.failure_rate": {"fullname": "pyoutlineapi.CircuitMetrics.failure_rate", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.failure_rate", "kind": "variable", "doc": "

    Calculate failure rate (O(1), no allocations).

    \n\n
    Returns
    \n\n
    \n

    Failure rate as decimal (0.0 to 1.0)

    \n
    \n", "annotation": ": float"}, "pyoutlineapi.CircuitMetrics.to_dict": {"fullname": "pyoutlineapi.CircuitMetrics.to_dict", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.to_dict", "kind": "function", "doc": "

    Convert metrics to dictionary for serialization.

    \n\n

    Pre-computes rates to avoid repeated calculations.

    \n\n
    Returns
    \n\n
    \n

    Dictionary representation

    \n
    \n", "signature": "(self) -> dict[str, int | float]:", "funcdef": "def"}, "pyoutlineapi.CircuitOpenError": {"fullname": "pyoutlineapi.CircuitOpenError", "modulename": "pyoutlineapi", "qualname": "CircuitOpenError", "kind": "class", "doc": "

    Circuit breaker is open due to repeated failures.

    \n\n

    Indicates temporary service unavailability. Clients should wait\nfor retry_after seconds before retrying.

    \n\n
    Attributes:
    \n\n
      \n
    • retry_after: Seconds to wait before retry
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = CircuitOpenError("Circuit open", retry_after=60.0)\n>>> error.is_retryable  # True\n>>> error.retry_after  # 60.0\n
    \n
    \n
    \n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.CircuitOpenError.__init__": {"fullname": "pyoutlineapi.CircuitOpenError.__init__", "modulename": "pyoutlineapi", "qualname": "CircuitOpenError.__init__", "kind": "function", "doc": "

    Initialize circuit open error.

    \n\n
    Arguments:
    \n\n
      \n
    • message: Error message
    • \n
    • retry_after: Seconds to wait before retry
    • \n
    \n\n
    Raises:
    \n\n
      \n
    • ValueError: If retry_after is negative
    • \n
    \n", "signature": "(message: str, *, retry_after: float = 60.0)"}, "pyoutlineapi.CircuitOpenError.retry_after": {"fullname": "pyoutlineapi.CircuitOpenError.retry_after", "modulename": "pyoutlineapi", "qualname": "CircuitOpenError.retry_after", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"fullname": "pyoutlineapi.CircuitOpenError.default_retry_delay", "modulename": "pyoutlineapi", "qualname": "CircuitOpenError.default_retry_delay", "kind": "variable", "doc": "

    Suggested delay before retry.

    \n", "annotation": ": float"}, "pyoutlineapi.CircuitState": {"fullname": "pyoutlineapi.CircuitState", "modulename": "pyoutlineapi", "qualname": "CircuitState", "kind": "class", "doc": "

    Circuit breaker states.

    \n\n

    CLOSED: Normal operation, requests pass through (hot path)\nOPEN: Failures exceeded threshold, requests blocked\nHALF_OPEN: Testing recovery, limited requests allowed

    \n", "bases": "enum.Enum"}, "pyoutlineapi.CircuitState.CLOSED": {"fullname": "pyoutlineapi.CircuitState.CLOSED", "modulename": "pyoutlineapi", "qualname": "CircuitState.CLOSED", "kind": "variable", "doc": "

    \n", "default_value": "<CircuitState.CLOSED: 1>"}, "pyoutlineapi.CircuitState.OPEN": {"fullname": "pyoutlineapi.CircuitState.OPEN", "modulename": "pyoutlineapi", "qualname": "CircuitState.OPEN", "kind": "variable", "doc": "

    \n", "default_value": "<CircuitState.OPEN: 2>"}, "pyoutlineapi.CircuitState.HALF_OPEN": {"fullname": "pyoutlineapi.CircuitState.HALF_OPEN", "modulename": "pyoutlineapi", "qualname": "CircuitState.HALF_OPEN", "kind": "variable", "doc": "

    \n", "default_value": "<CircuitState.HALF_OPEN: 3>"}, "pyoutlineapi.ConfigOverrides": {"fullname": "pyoutlineapi.ConfigOverrides", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides", "kind": "class", "doc": "

    Type-safe configuration overrides.

    \n\n

    All fields are optional, allowing selective parameter overriding\nwhile maintaining type safety.

    \n", "bases": "typing.TypedDict"}, "pyoutlineapi.ConfigOverrides.timeout": {"fullname": "pyoutlineapi.ConfigOverrides.timeout", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.timeout", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"fullname": "pyoutlineapi.ConfigOverrides.retry_attempts", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.retry_attempts", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.ConfigOverrides.max_connections": {"fullname": "pyoutlineapi.ConfigOverrides.max_connections", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.max_connections", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.ConfigOverrides.rate_limit": {"fullname": "pyoutlineapi.ConfigOverrides.rate_limit", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.rate_limit", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.ConfigOverrides.user_agent": {"fullname": "pyoutlineapi.ConfigOverrides.user_agent", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.user_agent", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"fullname": "pyoutlineapi.ConfigOverrides.enable_circuit_breaker", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.enable_circuit_breaker", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"fullname": "pyoutlineapi.ConfigOverrides.circuit_failure_threshold", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.circuit_failure_threshold", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"fullname": "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.circuit_recovery_timeout", "kind": "variable", "doc": "

    \n", "annotation": ": float"}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"fullname": "pyoutlineapi.ConfigOverrides.circuit_success_threshold", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.circuit_success_threshold", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"fullname": "pyoutlineapi.ConfigOverrides.circuit_call_timeout", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.circuit_call_timeout", "kind": "variable", "doc": "

    \n", "annotation": ": float"}, "pyoutlineapi.ConfigOverrides.enable_logging": {"fullname": "pyoutlineapi.ConfigOverrides.enable_logging", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.enable_logging", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, "pyoutlineapi.ConfigOverrides.json_format": {"fullname": "pyoutlineapi.ConfigOverrides.json_format", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.json_format", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"fullname": "pyoutlineapi.ConfigOverrides.allow_private_networks", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.allow_private_networks", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"fullname": "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf", "modulename": "pyoutlineapi", "qualname": "ConfigOverrides.resolve_dns_for_ssrf", "kind": "variable", "doc": "

    \n", "annotation": ": bool"}, "pyoutlineapi.ConfigurationError": {"fullname": "pyoutlineapi.ConfigurationError", "modulename": "pyoutlineapi", "qualname": "ConfigurationError", "kind": "class", "doc": "

    Invalid or missing configuration.

    \n\n
    Attributes:
    \n\n
      \n
    • field: Configuration field name that failed
    • \n
    • security_issue: Whether this is a security-related issue
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = ConfigurationError(\n...     "Missing API URL", field="api_url", security_issue=True\n... )\n
    \n
    \n
    \n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.ConfigurationError.__init__": {"fullname": "pyoutlineapi.ConfigurationError.__init__", "modulename": "pyoutlineapi", "qualname": "ConfigurationError.__init__", "kind": "function", "doc": "

    Initialize configuration error.

    \n\n
    Arguments:
    \n\n
      \n
    • message: Error message
    • \n
    • field: Configuration field name
    • \n
    • security_issue: Whether this is a security issue
    • \n
    \n", "signature": "(\tmessage: str,\t*,\tfield: str | None = None,\tsecurity_issue: bool = False)"}, "pyoutlineapi.ConfigurationError.field": {"fullname": "pyoutlineapi.ConfigurationError.field", "modulename": "pyoutlineapi", "qualname": "ConfigurationError.field", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.ConfigurationError.security_issue": {"fullname": "pyoutlineapi.ConfigurationError.security_issue", "modulename": "pyoutlineapi", "qualname": "ConfigurationError.security_issue", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.Constants": {"fullname": "pyoutlineapi.Constants", "modulename": "pyoutlineapi", "qualname": "Constants", "kind": "class", "doc": "

    Application-wide constants with security limits.

    \n"}, "pyoutlineapi.Constants.MIN_PORT": {"fullname": "pyoutlineapi.Constants.MIN_PORT", "modulename": "pyoutlineapi", "qualname": "Constants.MIN_PORT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "1"}, "pyoutlineapi.Constants.MAX_PORT": {"fullname": "pyoutlineapi.Constants.MAX_PORT", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_PORT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "65535"}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"fullname": "pyoutlineapi.Constants.MAX_NAME_LENGTH", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_NAME_LENGTH", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "255"}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"fullname": "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH", "modulename": "pyoutlineapi", "qualname": "Constants.CERT_FINGERPRINT_LENGTH", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "64"}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"fullname": "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_KEY_ID_LENGTH", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "255"}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"fullname": "pyoutlineapi.Constants.MAX_URL_LENGTH", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_URL_LENGTH", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "2048"}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"fullname": "pyoutlineapi.Constants.DEFAULT_TIMEOUT", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_TIMEOUT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "10"}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"fullname": "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_RETRY_ATTEMPTS", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "2"}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"fullname": "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_MIN_CONNECTIONS", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "1"}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"fullname": "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_MAX_CONNECTIONS", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "100"}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"fullname": "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_RETRY_DELAY", "kind": "variable", "doc": "

    \n", "annotation": ": Final[float]", "default_value": "1.0"}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"fullname": "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_MIN_TIMEOUT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "1"}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"fullname": "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_MAX_TIMEOUT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "300"}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"fullname": "pyoutlineapi.Constants.DEFAULT_USER_AGENT", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_USER_AGENT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[str]", "default_value": "'PyOutlineAPI/0.4.0'"}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"fullname": "pyoutlineapi.Constants.MAX_RECURSION_DEPTH", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_RECURSION_DEPTH", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "10"}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"fullname": "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_SNAPSHOT_SIZE_MB", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "10"}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"fullname": "pyoutlineapi.Constants.RETRY_STATUS_CODES", "modulename": "pyoutlineapi", "qualname": "Constants.RETRY_STATUS_CODES", "kind": "variable", "doc": "

    \n", "annotation": ": Final[frozenset[int]]", "default_value": "frozenset({500, 408, 502, 503, 504, 429})"}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"fullname": "pyoutlineapi.Constants.LOG_LEVEL_DEBUG", "modulename": "pyoutlineapi", "qualname": "Constants.LOG_LEVEL_DEBUG", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "10"}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"fullname": "pyoutlineapi.Constants.LOG_LEVEL_INFO", "modulename": "pyoutlineapi", "qualname": "Constants.LOG_LEVEL_INFO", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "20"}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"fullname": "pyoutlineapi.Constants.LOG_LEVEL_WARNING", "modulename": "pyoutlineapi", "qualname": "Constants.LOG_LEVEL_WARNING", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "30"}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"fullname": "pyoutlineapi.Constants.LOG_LEVEL_ERROR", "modulename": "pyoutlineapi", "qualname": "Constants.LOG_LEVEL_ERROR", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "40"}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"fullname": "pyoutlineapi.Constants.MAX_RESPONSE_SIZE", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_RESPONSE_SIZE", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "10485760"}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"fullname": "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_RESPONSE_CHUNK_SIZE", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "8192"}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"fullname": "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_RATE_LIMIT_RPS", "kind": "variable", "doc": "

    \n", "annotation": ": Final[float]", "default_value": "100.0"}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"fullname": "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_RATE_LIMIT_BURST", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "200"}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"fullname": "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT", "modulename": "pyoutlineapi", "qualname": "Constants.DEFAULT_RATE_LIMIT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "100"}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"fullname": "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_CONNECTIONS_PER_HOST", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "50"}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"fullname": "pyoutlineapi.Constants.DNS_CACHE_TTL", "modulename": "pyoutlineapi", "qualname": "Constants.DNS_CACHE_TTL", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "300"}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"fullname": "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO", "modulename": "pyoutlineapi", "qualname": "Constants.TIMEOUT_WARNING_RATIO", "kind": "variable", "doc": "

    \n", "annotation": ": Final[float]", "default_value": "0.8"}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"fullname": "pyoutlineapi.Constants.MAX_TIMEOUT", "modulename": "pyoutlineapi", "qualname": "Constants.MAX_TIMEOUT", "kind": "variable", "doc": "

    \n", "annotation": ": Final[int]", "default_value": "300"}, "pyoutlineapi.CredentialSanitizer": {"fullname": "pyoutlineapi.CredentialSanitizer", "modulename": "pyoutlineapi", "qualname": "CredentialSanitizer", "kind": "class", "doc": "

    Sanitize credentials from strings and exceptions.

    \n"}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"fullname": "pyoutlineapi.CredentialSanitizer.PATTERNS", "modulename": "pyoutlineapi", "qualname": "CredentialSanitizer.PATTERNS", "kind": "variable", "doc": "

    \n", "annotation": ": Final[list[tuple[re.Pattern[str], str]]]", "default_value": "[(re.compile('api[_-]?key["\\\\\\']?\\\\s*[:=]\\\\s*["\\\\\\']?([a-zA-Z0-9]{20,})', re.IGNORECASE), '***API_KEY***'), (re.compile('token["\\\\\\']?\\\\s*[:=]\\\\s*["\\\\\\']?([a-zA-Z0-9]{20,})', re.IGNORECASE), '***TOKEN***'), (re.compile('password["\\\\\\']?\\\\s*[:=]\\\\s*["\\\\\\']?([^\\\\s"\\\\\\']+)', re.IGNORECASE), '***PASSWORD***'), (re.compile('cert[_-]?sha256["\\\\\\']?\\\\s*[:=]\\\\s*["\\\\\\']?([a-f0-9]{64})', re.IGNORECASE), '***CERT***'), (re.compile('bearer\\\\s+([a-zA-Z0-9\\\\-._~+/]+=*)', re.IGNORECASE), 'Bearer ***TOKEN***'), (re.compile('access_url[\\'\\\\"]?\\\\s*[:=]\\\\s*[\\'\\\\"]?([^\\\\s\\'\\\\"]+)', re.IGNORECASE), '***ACCESS_URL***')]"}, "pyoutlineapi.CredentialSanitizer.sanitize": {"fullname": "pyoutlineapi.CredentialSanitizer.sanitize", "modulename": "pyoutlineapi", "qualname": "CredentialSanitizer.sanitize", "kind": "function", "doc": "

    Remove credentials from string.

    \n\n
    Parameters
    \n\n
      \n
    • text: Text that may contain credentials
    • \n
    \n\n
    Returns
    \n\n
    \n

    Sanitized text

    \n
    \n", "signature": "(cls, text: str) -> str:", "funcdef": "def"}, "pyoutlineapi.DataLimit": {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "

    Data transfer limit in bytes with unit conversions.

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel, pyoutlineapi.models.ByteConversionMixin"}, "pyoutlineapi.DataLimit.bytes": {"fullname": "pyoutlineapi.DataLimit.bytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.bytes", "kind": "variable", "doc": "

    Size in bytes

    \n", "annotation": ": Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Size in bytes', metadata=[Ge(ge=0)])]", "default_value": "PydanticUndefined"}, "pyoutlineapi.DataLimit.from_kilobytes": {"fullname": "pyoutlineapi.DataLimit.from_kilobytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.from_kilobytes", "kind": "function", "doc": "

    Create DataLimit from kilobytes.

    \n\n
    Parameters
    \n\n
      \n
    • kb: Size in kilobytes
    • \n
    \n\n
    Returns
    \n\n
    \n

    DataLimit instance

    \n
    \n", "signature": "(cls, kb: float) -> Self:", "funcdef": "def"}, "pyoutlineapi.DataLimit.from_megabytes": {"fullname": "pyoutlineapi.DataLimit.from_megabytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.from_megabytes", "kind": "function", "doc": "

    Create DataLimit from megabytes.

    \n\n
    Parameters
    \n\n
      \n
    • mb: Size in megabytes
    • \n
    \n\n
    Returns
    \n\n
    \n

    DataLimit instance

    \n
    \n", "signature": "(cls, mb: float) -> Self:", "funcdef": "def"}, "pyoutlineapi.DataLimit.from_gigabytes": {"fullname": "pyoutlineapi.DataLimit.from_gigabytes", "modulename": "pyoutlineapi", "qualname": "DataLimit.from_gigabytes", "kind": "function", "doc": "

    Create DataLimit from gigabytes.

    \n\n
    Parameters
    \n\n
      \n
    • gb: Size in gigabytes
    • \n
    \n\n
    Returns
    \n\n
    \n

    DataLimit instance

    \n
    \n", "signature": "(cls, gb: float) -> Self:", "funcdef": "def"}, "pyoutlineapi.DataLimitRequest": {"fullname": "pyoutlineapi.DataLimitRequest", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest", "kind": "class", "doc": "

    Request model for setting data limit.

    \n\n
    Note:
    \n\n
    \n

    The API expects the DataLimit object directly.\n Use to_payload() to produce the correct request body.

    \n
    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.DataLimitRequest.limit": {"fullname": "pyoutlineapi.DataLimitRequest.limit", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest.limit", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataLimit", "default_value": "PydanticUndefined"}, "pyoutlineapi.DataLimitRequest.to_payload": {"fullname": "pyoutlineapi.DataLimitRequest.to_payload", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest.to_payload", "kind": "function", "doc": "

    Convert to API request payload.

    \n\n
    Returns
    \n\n
    \n

    Payload dict with bytes field

    \n
    \n", "signature": "(self) -> dict[str, int]:", "funcdef": "def"}, "pyoutlineapi.DataTransferred": {"fullname": "pyoutlineapi.DataTransferred", "modulename": "pyoutlineapi", "qualname": "DataTransferred", "kind": "class", "doc": "

    Data transfer metric with byte conversions.

    \n\n

    SCHEMA: Based on experimental metrics dataTransferred object

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel, pyoutlineapi.models.ByteConversionMixin"}, "pyoutlineapi.DataTransferred.bytes": {"fullname": "pyoutlineapi.DataTransferred.bytes", "modulename": "pyoutlineapi", "qualname": "DataTransferred.bytes", "kind": "variable", "doc": "

    Size in bytes

    \n", "annotation": ": Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Size in bytes', metadata=[Ge(ge=0)])]", "default_value": "PydanticUndefined"}, "pyoutlineapi.DefaultAuditLogger": {"fullname": "pyoutlineapi.DefaultAuditLogger", "modulename": "pyoutlineapi", "qualname": "DefaultAuditLogger", "kind": "class", "doc": "

    Async audit logger with batching and backpressure handling.

    \n"}, "pyoutlineapi.DefaultAuditLogger.__init__": {"fullname": "pyoutlineapi.DefaultAuditLogger.__init__", "modulename": "pyoutlineapi", "qualname": "DefaultAuditLogger.__init__", "kind": "function", "doc": "

    Initialize audit logger with batching support.

    \n\n
    Parameters
    \n\n
      \n
    • queue_size: Maximum queue size (backpressure protection)
    • \n
    • batch_size: Maximum batch size for processing
    • \n
    • batch_timeout: Maximum time to wait for batch completion (seconds)
    • \n
    \n", "signature": "(\t*,\tqueue_size: int = 10000,\tbatch_size: int = 100,\tbatch_timeout: float = 1.0)"}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"fullname": "pyoutlineapi.DefaultAuditLogger.alog_action", "modulename": "pyoutlineapi", "qualname": "DefaultAuditLogger.alog_action", "kind": "function", "doc": "

    Log auditable action asynchronously with automatic batching.

    \n\n
    Parameters
    \n\n
      \n
    • action: Action being performed
    • \n
    • resource: Resource identifier
    • \n
    • user: User performing the action (optional)
    • \n
    • details: Additional structured details (optional)
    • \n
    • correlation_id: Request correlation ID (optional)
    • \n
    \n", "signature": "(\tself,\taction: str,\tresource: str,\t*,\tuser: str | None = None,\tdetails: dict[str, typing.Any] | None = None,\tcorrelation_id: str | None = None) -> None:", "funcdef": "async def"}, "pyoutlineapi.DefaultAuditLogger.log_action": {"fullname": "pyoutlineapi.DefaultAuditLogger.log_action", "modulename": "pyoutlineapi", "qualname": "DefaultAuditLogger.log_action", "kind": "function", "doc": "

    Log auditable action synchronously (fallback method).

    \n\n
    Parameters
    \n\n
      \n
    • action: Action being performed
    • \n
    • resource: Resource identifier
    • \n
    • user: User performing the action (optional)
    • \n
    • details: Additional structured details (optional)
    • \n
    • correlation_id: Request correlation ID (optional)
    • \n
    \n", "signature": "(\tself,\taction: str,\tresource: str,\t*,\tuser: str | None = None,\tdetails: dict[str, typing.Any] | None = None,\tcorrelation_id: str | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"fullname": "pyoutlineapi.DefaultAuditLogger.shutdown", "modulename": "pyoutlineapi", "qualname": "DefaultAuditLogger.shutdown", "kind": "function", "doc": "

    Gracefully shutdown audit logger with queue draining.

    \n\n
    Parameters
    \n\n
      \n
    • timeout: Maximum time to wait for queue to drain (seconds)
    • \n
    \n", "signature": "(self, *, timeout: float = 5.0) -> None:", "funcdef": "async def"}, "pyoutlineapi.DevelopmentConfig": {"fullname": "pyoutlineapi.DevelopmentConfig", "modulename": "pyoutlineapi", "qualname": "DevelopmentConfig", "kind": "class", "doc": "

    Development configuration with relaxed security.

    \n\n

    Optimized for local development and testing with:

    \n\n
      \n
    • Extended timeouts for debugging
    • \n
    • Detailed logging enabled by default
    • \n
    • Circuit breaker disabled for easier testing
    • \n
    \n", "bases": "pyoutlineapi.config.OutlineClientConfig"}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"fullname": "pyoutlineapi.DevelopmentConfig.enable_logging", "modulename": "pyoutlineapi", "qualname": "DevelopmentConfig.enable_logging", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "True"}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"fullname": "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker", "modulename": "pyoutlineapi", "qualname": "DevelopmentConfig.enable_circuit_breaker", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "False"}, "pyoutlineapi.DevelopmentConfig.timeout": {"fullname": "pyoutlineapi.DevelopmentConfig.timeout", "modulename": "pyoutlineapi", "qualname": "DevelopmentConfig.timeout", "kind": "variable", "doc": "

    \n", "annotation": ": int", "default_value": "30"}, "pyoutlineapi.ErrorResponse": {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "

    Error response with optimized string formatting.

    \n\n

    SCHEMA: Based on API error response format

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.ErrorResponse.code": {"fullname": "pyoutlineapi.ErrorResponse.code", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.code", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.ErrorResponse.message": {"fullname": "pyoutlineapi.ErrorResponse.message", "modulename": "pyoutlineapi", "qualname": "ErrorResponse.message", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.ExperimentalMetrics": {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "

    Experimental metrics with optimized lookup.

    \n\n

    SCHEMA: Based on GET /experimental/server/metrics response

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.ExperimentalMetrics.server": {"fullname": "pyoutlineapi.ExperimentalMetrics.server", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.server", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.ServerExperimentalMetric", "default_value": "PydanticUndefined"}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"fullname": "pyoutlineapi.ExperimentalMetrics.access_keys", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.AccessKeyMetric]", "default_value": "PydanticUndefined"}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"fullname": "pyoutlineapi.ExperimentalMetrics.get_key_metric", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics.get_key_metric", "kind": "function", "doc": "

    Get metrics for specific key with early return.

    \n\n
    Parameters
    \n\n
      \n
    • key_id: Access key ID
    • \n
    \n\n
    Returns
    \n\n
    \n

    Key metrics or None if not found

    \n
    \n", "signature": "(self, key_id: str) -> pyoutlineapi.models.AccessKeyMetric | None:", "funcdef": "def"}, "pyoutlineapi.HealthCheckResult": {"fullname": "pyoutlineapi.HealthCheckResult", "modulename": "pyoutlineapi", "qualname": "HealthCheckResult", "kind": "class", "doc": "

    Health check result with optimized diagnostics.

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.HealthCheckResult.healthy": {"fullname": "pyoutlineapi.HealthCheckResult.healthy", "modulename": "pyoutlineapi", "qualname": "HealthCheckResult.healthy", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "PydanticUndefined"}, "pyoutlineapi.HealthCheckResult.timestamp": {"fullname": "pyoutlineapi.HealthCheckResult.timestamp", "modulename": "pyoutlineapi", "qualname": "HealthCheckResult.timestamp", "kind": "variable", "doc": "

    \n", "annotation": ": float", "default_value": "PydanticUndefined"}, "pyoutlineapi.HealthCheckResult.checks": {"fullname": "pyoutlineapi.HealthCheckResult.checks", "modulename": "pyoutlineapi", "qualname": "HealthCheckResult.checks", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, dict[str, typing.Any]]", "default_value": "PydanticUndefined"}, "pyoutlineapi.HealthCheckResult.failed_checks": {"fullname": "pyoutlineapi.HealthCheckResult.failed_checks", "modulename": "pyoutlineapi", "qualname": "HealthCheckResult.failed_checks", "kind": "variable", "doc": "

    Get failed checks (cached for repeated access).

    \n\n
    Returns
    \n\n
    \n

    List of failed check names

    \n
    \n", "annotation": ": list[str]"}, "pyoutlineapi.HealthCheckResult.success_rate": {"fullname": "pyoutlineapi.HealthCheckResult.success_rate", "modulename": "pyoutlineapi", "qualname": "HealthCheckResult.success_rate", "kind": "variable", "doc": "

    Calculate success rate (uses cached failed_checks).

    \n\n
    Returns
    \n\n
    \n

    Success rate (0.0 to 1.0)

    \n
    \n", "annotation": ": float"}, "pyoutlineapi.HostnameRequest": {"fullname": "pyoutlineapi.HostnameRequest", "modulename": "pyoutlineapi", "qualname": "HostnameRequest", "kind": "class", "doc": "

    Request model for setting hostname.

    \n\n

    SCHEMA: Based on PUT /server/hostname-for-access-keys request body

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.HostnameRequest.hostname": {"fullname": "pyoutlineapi.HostnameRequest.hostname", "modulename": "pyoutlineapi", "qualname": "HostnameRequest.hostname", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.JsonDict": {"fullname": "pyoutlineapi.JsonDict", "modulename": "pyoutlineapi", "qualname": "JsonDict", "kind": "variable", "doc": "

    \n", "default_value": "dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]"}, "pyoutlineapi.JsonPayload": {"fullname": "pyoutlineapi.JsonPayload", "modulename": "pyoutlineapi", "qualname": "JsonPayload", "kind": "variable", "doc": "

    \n", "default_value": "dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]] | list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]] | None"}, "pyoutlineapi.LocationMetric": {"fullname": "pyoutlineapi.LocationMetric", "modulename": "pyoutlineapi", "qualname": "LocationMetric", "kind": "class", "doc": "

    Location-based usage metric.

    \n\n

    SCHEMA: Based on experimental metrics locations array item

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.LocationMetric.location": {"fullname": "pyoutlineapi.LocationMetric.location", "modulename": "pyoutlineapi", "qualname": "LocationMetric.location", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.LocationMetric.asn": {"fullname": "pyoutlineapi.LocationMetric.asn", "modulename": "pyoutlineapi", "qualname": "LocationMetric.asn", "kind": "variable", "doc": "

    \n", "annotation": ": int | None", "default_value": "None"}, "pyoutlineapi.LocationMetric.as_org": {"fullname": "pyoutlineapi.LocationMetric.as_org", "modulename": "pyoutlineapi", "qualname": "LocationMetric.as_org", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.LocationMetric.tunnel_time": {"fullname": "pyoutlineapi.LocationMetric.tunnel_time", "modulename": "pyoutlineapi", "qualname": "LocationMetric.tunnel_time", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.TunnelTime", "default_value": "PydanticUndefined"}, "pyoutlineapi.LocationMetric.data_transferred": {"fullname": "pyoutlineapi.LocationMetric.data_transferred", "modulename": "pyoutlineapi", "qualname": "LocationMetric.data_transferred", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataTransferred", "default_value": "PydanticUndefined"}, "pyoutlineapi.MetricsCollector": {"fullname": "pyoutlineapi.MetricsCollector", "modulename": "pyoutlineapi", "qualname": "MetricsCollector", "kind": "class", "doc": "

    Protocol for metrics collection.

    \n\n

    Allows dependency injection of custom metrics backends.

    \n", "bases": "typing.Protocol"}, "pyoutlineapi.MetricsCollector.__init__": {"fullname": "pyoutlineapi.MetricsCollector.__init__", "modulename": "pyoutlineapi", "qualname": "MetricsCollector.__init__", "kind": "function", "doc": "

    \n", "signature": "(*args, **kwargs)"}, "pyoutlineapi.MetricsCollector.increment": {"fullname": "pyoutlineapi.MetricsCollector.increment", "modulename": "pyoutlineapi", "qualname": "MetricsCollector.increment", "kind": "function", "doc": "

    Increment counter metric.

    \n", "signature": "(self, metric: str, *, tags: dict[str, str] | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.MetricsCollector.timing": {"fullname": "pyoutlineapi.MetricsCollector.timing", "modulename": "pyoutlineapi", "qualname": "MetricsCollector.timing", "kind": "function", "doc": "

    Record timing metric.

    \n", "signature": "(\tself,\tmetric: str,\tvalue: float,\t*,\ttags: dict[str, str] | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.MetricsCollector.gauge": {"fullname": "pyoutlineapi.MetricsCollector.gauge", "modulename": "pyoutlineapi", "qualname": "MetricsCollector.gauge", "kind": "function", "doc": "

    Set gauge metric.

    \n", "signature": "(\tself,\tmetric: str,\tvalue: float,\t*,\ttags: dict[str, str] | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.MetricsEnabledRequest": {"fullname": "pyoutlineapi.MetricsEnabledRequest", "modulename": "pyoutlineapi", "qualname": "MetricsEnabledRequest", "kind": "class", "doc": "

    Request model for enabling/disabling metrics.

    \n\n

    SCHEMA: Based on PUT /metrics/enabled request body

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"fullname": "pyoutlineapi.MetricsEnabledRequest.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsEnabledRequest.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "PydanticUndefined"}, "pyoutlineapi.MetricsStatusResponse": {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "

    Response model for metrics status.

    \n\n

    Returns current metrics sharing status.\nSCHEMA: Based on GET /metrics/enabled response

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"fullname": "pyoutlineapi.MetricsStatusResponse.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "PydanticUndefined"}, "pyoutlineapi.MetricsTags": {"fullname": "pyoutlineapi.MetricsTags", "modulename": "pyoutlineapi", "qualname": "MetricsTags", "kind": "variable", "doc": "

    \n", "default_value": "dict[str, str]"}, "pyoutlineapi.MultiServerManager": {"fullname": "pyoutlineapi.MultiServerManager", "modulename": "pyoutlineapi", "qualname": "MultiServerManager", "kind": "class", "doc": "

    High-performance manager for multiple Outline servers.

    \n\n

    Features:

    \n\n
      \n
    • Concurrent operations across all servers
    • \n
    • Health checking and automatic failover
    • \n
    • Aggregated metrics and status
    • \n
    • Graceful shutdown with cleanup
    • \n
    • Thread-safe operations
    • \n
    \n\n

    Limits:

    \n\n
      \n
    • Maximum 50 servers (configurable via _MAX_SERVERS)
    • \n
    • Automatic cleanup with weak references
    • \n
    \n"}, "pyoutlineapi.MultiServerManager.__init__": {"fullname": "pyoutlineapi.MultiServerManager.__init__", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.__init__", "kind": "function", "doc": "

    Initialize multiserver manager.

    \n\n
    Parameters
    \n\n
      \n
    • configs: Sequence of server configurations
    • \n
    • audit_logger: Shared audit logger for all servers
    • \n
    • metrics: Shared metrics collector for all servers
    • \n
    • default_timeout: Default timeout for operations (seconds)
    • \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If too many servers or invalid configs
    • \n
    \n", "signature": "(\tconfigs: Sequence[pyoutlineapi.config.OutlineClientConfig],\t*,\taudit_logger: pyoutlineapi.audit.AuditLogger | None = None,\tmetrics: pyoutlineapi.base_client.MetricsCollector | None = None,\tdefault_timeout: float = 5.0)"}, "pyoutlineapi.MultiServerManager.server_count": {"fullname": "pyoutlineapi.MultiServerManager.server_count", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.server_count", "kind": "variable", "doc": "

    Get total number of configured servers.

    \n\n
    Returns
    \n\n
    \n

    Number of servers

    \n
    \n", "annotation": ": int"}, "pyoutlineapi.MultiServerManager.active_servers": {"fullname": "pyoutlineapi.MultiServerManager.active_servers", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.active_servers", "kind": "variable", "doc": "

    Get number of active (connected) servers.

    \n\n
    Returns
    \n\n
    \n

    Number of active servers

    \n
    \n", "annotation": ": int"}, "pyoutlineapi.MultiServerManager.get_server_names": {"fullname": "pyoutlineapi.MultiServerManager.get_server_names", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.get_server_names", "kind": "function", "doc": "

    Get list of sanitized server URLs.

    \n\n

    URLs are sanitized to remove sensitive path information.

    \n\n
    Returns
    \n\n
    \n

    List of safe server identifiers

    \n
    \n", "signature": "(self) -> list[str]:", "funcdef": "def"}, "pyoutlineapi.MultiServerManager.get_client": {"fullname": "pyoutlineapi.MultiServerManager.get_client", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.get_client", "kind": "function", "doc": "

    Get client by server identifier or index.

    \n\n
    Parameters
    \n\n
      \n
    • server_identifier: Server URL (sanitized) or 0-based index
    • \n
    \n\n
    Returns
    \n\n
    \n

    Client instance

    \n
    \n\n
    Raises
    \n\n
      \n
    • KeyError: If server not found
    • \n
    • IndexError: If index out of range
    • \n
    \n", "signature": "(\tself,\tserver_identifier: str | int) -> pyoutlineapi.client.AsyncOutlineClient:", "funcdef": "def"}, "pyoutlineapi.MultiServerManager.get_all_clients": {"fullname": "pyoutlineapi.MultiServerManager.get_all_clients", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.get_all_clients", "kind": "function", "doc": "

    Get all active clients.

    \n\n
    Returns
    \n\n
    \n

    List of client instances

    \n
    \n", "signature": "(self) -> list[pyoutlineapi.client.AsyncOutlineClient]:", "funcdef": "def"}, "pyoutlineapi.MultiServerManager.health_check_all": {"fullname": "pyoutlineapi.MultiServerManager.health_check_all", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.health_check_all", "kind": "function", "doc": "

    Perform health check on all servers concurrently.

    \n\n
    Parameters
    \n\n
      \n
    • timeout: Timeout for each health check
    • \n
    \n\n
    Returns
    \n\n
    \n

    Dictionary mapping server IDs to health check results

    \n
    \n", "signature": "(self, timeout: float | None = None) -> dict[str, dict[str, typing.Any]]:", "funcdef": "async def"}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"fullname": "pyoutlineapi.MultiServerManager.get_healthy_servers", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.get_healthy_servers", "kind": "function", "doc": "

    Get list of healthy servers after health check.

    \n\n
    Parameters
    \n\n
      \n
    • timeout: Timeout for health checks
    • \n
    \n\n
    Returns
    \n\n
    \n

    List of healthy clients

    \n
    \n", "signature": "(\tself,\ttimeout: float | None = None) -> list[pyoutlineapi.client.AsyncOutlineClient]:", "funcdef": "async def"}, "pyoutlineapi.MultiServerManager.get_status_summary": {"fullname": "pyoutlineapi.MultiServerManager.get_status_summary", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.get_status_summary", "kind": "function", "doc": "

    Get aggregated status summary for all servers.

    \n\n

    Synchronous operation - no API calls made.

    \n\n
    Returns
    \n\n
    \n

    Status summary dictionary

    \n
    \n", "signature": "(self) -> dict[str, typing.Any]:", "funcdef": "def"}, "pyoutlineapi.NoOpAuditLogger": {"fullname": "pyoutlineapi.NoOpAuditLogger", "modulename": "pyoutlineapi", "qualname": "NoOpAuditLogger", "kind": "class", "doc": "

    Zero-overhead no-op audit logger.

    \n\n

    Implements AuditLogger protocol but performs no operations.\nUseful for disabling audit without code changes or performance impact.

    \n"}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"fullname": "pyoutlineapi.NoOpAuditLogger.alog_action", "modulename": "pyoutlineapi", "qualname": "NoOpAuditLogger.alog_action", "kind": "function", "doc": "

    No-op async log.

    \n", "signature": "(\tself,\taction: str,\tresource: str,\t*,\tuser: str | None = None,\tdetails: dict[str, typing.Any] | None = None,\tcorrelation_id: str | None = None) -> None:", "funcdef": "async def"}, "pyoutlineapi.NoOpAuditLogger.log_action": {"fullname": "pyoutlineapi.NoOpAuditLogger.log_action", "modulename": "pyoutlineapi", "qualname": "NoOpAuditLogger.log_action", "kind": "function", "doc": "

    No-op sync log.

    \n", "signature": "(\tself,\taction: str,\tresource: str,\t*,\tuser: str | None = None,\tdetails: dict[str, typing.Any] | None = None,\tcorrelation_id: str | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"fullname": "pyoutlineapi.NoOpAuditLogger.shutdown", "modulename": "pyoutlineapi", "qualname": "NoOpAuditLogger.shutdown", "kind": "function", "doc": "

    No-op shutdown.

    \n", "signature": "(self) -> None:", "funcdef": "async def"}, "pyoutlineapi.NoOpMetrics": {"fullname": "pyoutlineapi.NoOpMetrics", "modulename": "pyoutlineapi", "qualname": "NoOpMetrics", "kind": "class", "doc": "

    No-op metrics collector (zero-overhead default).

    \n\n

    Uses __slots__ to minimize memory footprint.

    \n"}, "pyoutlineapi.NoOpMetrics.increment": {"fullname": "pyoutlineapi.NoOpMetrics.increment", "modulename": "pyoutlineapi", "qualname": "NoOpMetrics.increment", "kind": "function", "doc": "

    No-op increment (zero overhead).

    \n", "signature": "(self, metric: str, *, tags: dict[str, str] | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.NoOpMetrics.timing": {"fullname": "pyoutlineapi.NoOpMetrics.timing", "modulename": "pyoutlineapi", "qualname": "NoOpMetrics.timing", "kind": "function", "doc": "

    No-op timing (zero overhead).

    \n", "signature": "(\tself,\tmetric: str,\tvalue: float,\t*,\ttags: dict[str, str] | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.NoOpMetrics.gauge": {"fullname": "pyoutlineapi.NoOpMetrics.gauge", "modulename": "pyoutlineapi", "qualname": "NoOpMetrics.gauge", "kind": "function", "doc": "

    No-op gauge (zero overhead).

    \n", "signature": "(\tself,\tmetric: str,\tvalue: float,\t*,\ttags: dict[str, str] | None = None) -> None:", "funcdef": "def"}, "pyoutlineapi.OutlineClientConfig": {"fullname": "pyoutlineapi.OutlineClientConfig", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig", "kind": "class", "doc": "

    Main configuration.

    \n", "bases": "pydantic_settings.main.BaseSettings"}, "pyoutlineapi.OutlineClientConfig.api_url": {"fullname": "pyoutlineapi.OutlineClientConfig.api_url", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.api_url", "kind": "variable", "doc": "

    Outline server API URL with secret path

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"fullname": "pyoutlineapi.OutlineClientConfig.cert_sha256", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.cert_sha256", "kind": "variable", "doc": "

    SHA-256 certificate fingerprint

    \n", "annotation": ": pydantic.types.SecretStr", "default_value": "PydanticUndefined"}, "pyoutlineapi.OutlineClientConfig.timeout": {"fullname": "pyoutlineapi.OutlineClientConfig.timeout", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.timeout", "kind": "variable", "doc": "

    Request timeout (seconds)

    \n", "annotation": ": int", "default_value": "10"}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"fullname": "pyoutlineapi.OutlineClientConfig.retry_attempts", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.retry_attempts", "kind": "variable", "doc": "

    Number of retries

    \n", "annotation": ": int", "default_value": "2"}, "pyoutlineapi.OutlineClientConfig.max_connections": {"fullname": "pyoutlineapi.OutlineClientConfig.max_connections", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.max_connections", "kind": "variable", "doc": "

    Connection pool size

    \n", "annotation": ": int", "default_value": "10"}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"fullname": "pyoutlineapi.OutlineClientConfig.rate_limit", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.rate_limit", "kind": "variable", "doc": "

    Max concurrent requests

    \n", "annotation": ": int", "default_value": "100"}, "pyoutlineapi.OutlineClientConfig.user_agent": {"fullname": "pyoutlineapi.OutlineClientConfig.user_agent", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.user_agent", "kind": "variable", "doc": "

    Custom user agent string

    \n", "annotation": ": str", "default_value": "'PyOutlineAPI/0.4.0'"}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"fullname": "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.enable_circuit_breaker", "kind": "variable", "doc": "

    Enable circuit breaker

    \n", "annotation": ": bool", "default_value": "True"}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"fullname": "pyoutlineapi.OutlineClientConfig.enable_logging", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.enable_logging", "kind": "variable", "doc": "

    Enable debug logging

    \n", "annotation": ": bool", "default_value": "False"}, "pyoutlineapi.OutlineClientConfig.json_format": {"fullname": "pyoutlineapi.OutlineClientConfig.json_format", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.json_format", "kind": "variable", "doc": "

    Return raw JSON

    \n", "annotation": ": bool", "default_value": "False"}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"fullname": "pyoutlineapi.OutlineClientConfig.allow_private_networks", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.allow_private_networks", "kind": "variable", "doc": "

    Allow private or local network addresses in api_url

    \n", "annotation": ": bool", "default_value": "True"}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"fullname": "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.resolve_dns_for_ssrf", "kind": "variable", "doc": "

    Resolve DNS for SSRF checks (strict mode)

    \n", "annotation": ": bool", "default_value": "False"}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"fullname": "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.circuit_failure_threshold", "kind": "variable", "doc": "

    Failures before opening

    \n", "annotation": ": int", "default_value": "5"}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"fullname": "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.circuit_recovery_timeout", "kind": "variable", "doc": "

    Recovery wait time (seconds)

    \n", "annotation": ": float", "default_value": "60.0"}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"fullname": "pyoutlineapi.OutlineClientConfig.circuit_success_threshold", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.circuit_success_threshold", "kind": "variable", "doc": "

    Successes needed to close

    \n", "annotation": ": int", "default_value": "2"}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"fullname": "pyoutlineapi.OutlineClientConfig.circuit_call_timeout", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.circuit_call_timeout", "kind": "variable", "doc": "

    Circuit call timeout (seconds)

    \n", "annotation": ": float", "default_value": "10.0"}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"fullname": "pyoutlineapi.OutlineClientConfig.validate_api_url", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.validate_api_url", "kind": "function", "doc": "

    Validate and normalize API URL with optimized regex.

    \n\n
    Parameters
    \n\n
      \n
    • v: URL to validate
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated URL

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If URL is invalid
    • \n
    \n", "signature": "(cls, v: str) -> str:", "funcdef": "def"}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"fullname": "pyoutlineapi.OutlineClientConfig.validate_cert", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.validate_cert", "kind": "function", "doc": "

    Validate certificate fingerprint with constant-time comparison.

    \n\n
    Parameters
    \n\n
      \n
    • v: Certificate fingerprint
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated fingerprint

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If fingerprint is invalid
    • \n
    \n", "signature": "(cls, v: pydantic.types.SecretStr) -> pydantic.types.SecretStr:", "funcdef": "def"}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"fullname": "pyoutlineapi.OutlineClientConfig.validate_user_agent", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.validate_user_agent", "kind": "function", "doc": "

    Validate user agent string with efficient control char check.

    \n\n
    Parameters
    \n\n
      \n
    • v: User agent to validate
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated user agent

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If user agent is invalid
    • \n
    \n", "signature": "(cls, v: str) -> str:", "funcdef": "def"}, "pyoutlineapi.OutlineClientConfig.validate_config": {"fullname": "pyoutlineapi.OutlineClientConfig.validate_config", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.validate_config", "kind": "function", "doc": "

    Additional validation after model creation with pattern matching.

    \n\n
    Returns
    \n\n
    \n

    Validated configuration instance

    \n
    \n", "signature": "(self) -> Self:", "funcdef": "def"}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"fullname": "pyoutlineapi.OutlineClientConfig.get_sanitized_config", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.get_sanitized_config", "kind": "variable", "doc": "

    Get configuration with sensitive data masked (cached).

    \n\n

    Safe for logging, debugging, and display.

    \n\n

    Performance: ~20x speedup with caching for repeated calls\nMemory: Single cached result per instance

    \n\n
    Returns
    \n\n
    \n

    Sanitized configuration dictionary

    \n
    \n", "annotation": ": dict[str, int | str | bool | float]"}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"fullname": "pyoutlineapi.OutlineClientConfig.model_copy_immutable", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.model_copy_immutable", "kind": "function", "doc": "

    Create immutable copy with overrides (optimized validation).

    \n\n
    Parameters
    \n\n
      \n
    • overrides: Configuration parameters to override
    • \n
    \n\n
    Returns
    \n\n
    \n

    Deep copy of configuration with applied updates

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If invalid override keys provided
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> new_config = config.model_copy_immutable(timeout=20)\n
    \n
    \n
    \n", "signature": "(\tself,\t**overrides: int | str | bool | float) -> pyoutlineapi.config.OutlineClientConfig:", "funcdef": "def"}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"fullname": "pyoutlineapi.OutlineClientConfig.circuit_config", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.circuit_config", "kind": "variable", "doc": "

    Get circuit breaker configuration if enabled.

    \n\n

    Returns None if circuit breaker is disabled, otherwise CircuitConfig instance.\nCached as property for performance.

    \n\n
    Returns
    \n\n
    \n

    Circuit config or None if disabled

    \n
    \n", "annotation": ": pyoutlineapi.circuit_breaker.CircuitConfig | None"}, "pyoutlineapi.OutlineClientConfig.from_env": {"fullname": "pyoutlineapi.OutlineClientConfig.from_env", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.from_env", "kind": "function", "doc": "

    Load configuration from environment with overrides.

    \n\n
    Parameters
    \n\n
      \n
    • env_file: Path to .env file
    • \n
    • overrides: Configuration parameters to override
    • \n
    \n\n
    Returns
    \n\n
    \n

    Configuration instance

    \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If environment configuration is invalid
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> config = OutlineClientConfig.from_env(\n...     env_file=".env.prod",\n...     timeout=20,\n...     enable_logging=True\n... )\n
    \n
    \n
    \n", "signature": "(\tcls,\tenv_file: str | pathlib._local.Path | None = None,\t**overrides: int | str | bool | float) -> pyoutlineapi.config.OutlineClientConfig:", "funcdef": "def"}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"fullname": "pyoutlineapi.OutlineClientConfig.create_minimal", "modulename": "pyoutlineapi", "qualname": "OutlineClientConfig.create_minimal", "kind": "function", "doc": "

    Create minimal configuration (optimized validation).

    \n\n
    Parameters
    \n\n
      \n
    • api_url: API URL
    • \n
    • cert_sha256: Certificate fingerprint
    • \n
    • overrides: Optional configuration parameters
    • \n
    \n\n
    Returns
    \n\n
    \n

    Configuration instance

    \n
    \n\n
    Raises
    \n\n
      \n
    • TypeError: If cert_sha256 is not str or SecretStr
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> config = OutlineClientConfig.create_minimal(\n...     api_url="https://server.com/path",\n...     cert_sha256="a" * 64,\n...     timeout=20\n... )\n
    \n
    \n
    \n", "signature": "(\tcls,\tapi_url: str,\tcert_sha256: str | pydantic.types.SecretStr,\t**overrides: int | str | bool | float) -> pyoutlineapi.config.OutlineClientConfig:", "funcdef": "def"}, "pyoutlineapi.OutlineConnectionError": {"fullname": "pyoutlineapi.OutlineConnectionError", "modulename": "pyoutlineapi", "qualname": "OutlineConnectionError", "kind": "class", "doc": "

    Network connection failure.

    \n\n
    Attributes:
    \n\n
      \n
    • host: Host that failed
    • \n
    • port: Port that failed
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = OutlineConnectionError(\n...     "Connection refused", host="server.com", port=443\n... )\n>>> error.is_retryable  # True\n
    \n
    \n
    \n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.OutlineConnectionError.__init__": {"fullname": "pyoutlineapi.OutlineConnectionError.__init__", "modulename": "pyoutlineapi", "qualname": "OutlineConnectionError.__init__", "kind": "function", "doc": "

    Initialize connection error.

    \n\n
    Arguments:
    \n\n
      \n
    • message: Error message
    • \n
    • host: Host that failed
    • \n
    • port: Port that failed
    • \n
    \n", "signature": "(message: str, *, host: str | None = None, port: int | None = None)"}, "pyoutlineapi.OutlineConnectionError.host": {"fullname": "pyoutlineapi.OutlineConnectionError.host", "modulename": "pyoutlineapi", "qualname": "OutlineConnectionError.host", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.OutlineConnectionError.port": {"fullname": "pyoutlineapi.OutlineConnectionError.port", "modulename": "pyoutlineapi", "qualname": "OutlineConnectionError.port", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.OutlineError": {"fullname": "pyoutlineapi.OutlineError", "modulename": "pyoutlineapi", "qualname": "OutlineError", "kind": "class", "doc": "

    Base exception for all PyOutlineAPI errors.

    \n\n

    Provides rich error context, retry guidance, and safe serialization\nwith automatic credential sanitization.

    \n\n
    Attributes:
    \n\n
      \n
    • is_retryable: Whether this error type should be retried
    • \n
    • default_retry_delay: Suggested delay before retry in seconds
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> try:\n...     raise OutlineError("Connection failed", details={"host": "server"})\n... except OutlineError as e:\n...     print(e.safe_details)  # {'host': 'server'}\n
    \n
    \n
    \n", "bases": "builtins.Exception"}, "pyoutlineapi.OutlineError.__init__": {"fullname": "pyoutlineapi.OutlineError.__init__", "modulename": "pyoutlineapi", "qualname": "OutlineError.__init__", "kind": "function", "doc": "

    Initialize exception with automatic credential sanitization.

    \n\n
    Arguments:
    \n\n
      \n
    • message: Error message (automatically sanitized)
    • \n
    • details: Internal details (may contain sensitive data)
    • \n
    • safe_details: Safe details for logging/display
    • \n
    \n\n
    Raises:
    \n\n
      \n
    • ValueError: If message exceeds maximum length after sanitization
    • \n
    \n", "signature": "(\tmessage: object,\t*,\tdetails: dict[str, typing.Any] | None = None,\tsafe_details: dict[str, typing.Any] | None = None)"}, "pyoutlineapi.OutlineError.details": {"fullname": "pyoutlineapi.OutlineError.details", "modulename": "pyoutlineapi", "qualname": "OutlineError.details", "kind": "variable", "doc": "

    Get internal error details (may contain sensitive data).

    \n\n
    Warning:
    \n\n
    \n

    Use with caution - may contain credentials or sensitive information.\n For logging, use safe_details instead.

    \n
    \n\n
    Returns:
    \n\n
    \n

    Copy of internal details dictionary

    \n
    \n", "annotation": ": dict[str, typing.Any]"}, "pyoutlineapi.OutlineError.safe_details": {"fullname": "pyoutlineapi.OutlineError.safe_details", "modulename": "pyoutlineapi", "qualname": "OutlineError.safe_details", "kind": "variable", "doc": "

    Get sanitized error details safe for logging.

    \n\n
    Returns:
    \n\n
    \n

    Copy of safe details dictionary

    \n
    \n", "annotation": ": dict[str, typing.Any]"}, "pyoutlineapi.OutlineError.is_retryable": {"fullname": "pyoutlineapi.OutlineError.is_retryable", "modulename": "pyoutlineapi", "qualname": "OutlineError.is_retryable", "kind": "variable", "doc": "

    Return whether this error type should be retried.

    \n", "annotation": ": bool"}, "pyoutlineapi.OutlineError.default_retry_delay": {"fullname": "pyoutlineapi.OutlineError.default_retry_delay", "modulename": "pyoutlineapi", "qualname": "OutlineError.default_retry_delay", "kind": "variable", "doc": "

    Return suggested delay before retry in seconds.

    \n", "annotation": ": float"}, "pyoutlineapi.OutlineTimeoutError": {"fullname": "pyoutlineapi.OutlineTimeoutError", "modulename": "pyoutlineapi", "qualname": "OutlineTimeoutError", "kind": "class", "doc": "

    Operation timeout.

    \n\n
    Attributes:
    \n\n
      \n
    • timeout: Timeout value in seconds
    • \n
    • operation: Operation that timed out
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = OutlineTimeoutError(\n...     "Request timeout", timeout=30.0, operation="get_server_info"\n... )\n>>> error.is_retryable  # True\n
    \n
    \n
    \n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.OutlineTimeoutError.__init__": {"fullname": "pyoutlineapi.OutlineTimeoutError.__init__", "modulename": "pyoutlineapi", "qualname": "OutlineTimeoutError.__init__", "kind": "function", "doc": "

    Initialize timeout error.

    \n\n
    Arguments:
    \n\n
      \n
    • message: Error message
    • \n
    • timeout: Timeout value in seconds
    • \n
    • operation: Operation that timed out
    • \n
    \n", "signature": "(\tmessage: str,\t*,\ttimeout: float | None = None,\toperation: str | None = None)"}, "pyoutlineapi.OutlineTimeoutError.timeout": {"fullname": "pyoutlineapi.OutlineTimeoutError.timeout", "modulename": "pyoutlineapi", "qualname": "OutlineTimeoutError.timeout", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.OutlineTimeoutError.operation": {"fullname": "pyoutlineapi.OutlineTimeoutError.operation", "modulename": "pyoutlineapi", "qualname": "OutlineTimeoutError.operation", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.PeakDeviceCount": {"fullname": "pyoutlineapi.PeakDeviceCount", "modulename": "pyoutlineapi", "qualname": "PeakDeviceCount", "kind": "class", "doc": "

    Peak device count with timestamp.

    \n\n

    SCHEMA: Based on experimental metrics connection peakDeviceCount object

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.PeakDeviceCount.data": {"fullname": "pyoutlineapi.PeakDeviceCount.data", "modulename": "pyoutlineapi", "qualname": "PeakDeviceCount.data", "kind": "variable", "doc": "

    \n", "annotation": ": int", "default_value": "PydanticUndefined"}, "pyoutlineapi.PeakDeviceCount.timestamp": {"fullname": "pyoutlineapi.PeakDeviceCount.timestamp", "modulename": "pyoutlineapi", "qualname": "PeakDeviceCount.timestamp", "kind": "variable", "doc": "

    Unix timestamp in seconds

    \n", "annotation": ": Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in seconds', metadata=[Ge(ge=0)])]", "default_value": "PydanticUndefined"}, "pyoutlineapi.PortRequest": {"fullname": "pyoutlineapi.PortRequest", "modulename": "pyoutlineapi", "qualname": "PortRequest", "kind": "class", "doc": "

    Request model for setting default port.

    \n\n

    SCHEMA: Based on PUT /server/port-for-new-access-keys request body

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.PortRequest.port": {"fullname": "pyoutlineapi.PortRequest.port", "modulename": "pyoutlineapi", "qualname": "PortRequest.port", "kind": "variable", "doc": "

    Port number (1-65535)

    \n", "annotation": ": Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])]", "default_value": "PydanticUndefined"}, "pyoutlineapi.ProductionConfig": {"fullname": "pyoutlineapi.ProductionConfig", "modulename": "pyoutlineapi", "qualname": "ProductionConfig", "kind": "class", "doc": "

    Production configuration with strict security.

    \n\n

    Enforces HTTPS and enables all safety features:

    \n\n
      \n
    • Circuit breaker enabled
    • \n
    • Logging disabled (performance)
    • \n
    • HTTPS enforcement
    • \n
    • Strict validation
    • \n
    \n", "bases": "pyoutlineapi.config.OutlineClientConfig"}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"fullname": "pyoutlineapi.ProductionConfig.enable_circuit_breaker", "modulename": "pyoutlineapi", "qualname": "ProductionConfig.enable_circuit_breaker", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "True"}, "pyoutlineapi.ProductionConfig.enable_logging": {"fullname": "pyoutlineapi.ProductionConfig.enable_logging", "modulename": "pyoutlineapi", "qualname": "ProductionConfig.enable_logging", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "False"}, "pyoutlineapi.ProductionConfig.enforce_security": {"fullname": "pyoutlineapi.ProductionConfig.enforce_security", "modulename": "pyoutlineapi", "qualname": "ProductionConfig.enforce_security", "kind": "function", "doc": "

    Enforce production security with optimized checks.

    \n\n
    Returns
    \n\n
    \n

    Validated configuration

    \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If HTTP is used in production
    • \n
    \n", "signature": "(self) -> Self:", "funcdef": "def"}, "pyoutlineapi.QueryParams": {"fullname": "pyoutlineapi.QueryParams", "modulename": "pyoutlineapi", "qualname": "QueryParams", "kind": "variable", "doc": "

    \n", "default_value": "dict[str, str | int | float | bool]"}, "pyoutlineapi.ResponseData": {"fullname": "pyoutlineapi.ResponseData", "modulename": "pyoutlineapi", "qualname": "ResponseData", "kind": "variable", "doc": "

    \n", "default_value": "dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]"}, "pyoutlineapi.ResponseParser": {"fullname": "pyoutlineapi.ResponseParser", "modulename": "pyoutlineapi", "qualname": "ResponseParser", "kind": "class", "doc": "

    High-performance utility class for parsing and validating API responses.

    \n"}, "pyoutlineapi.ResponseParser.parse": {"fullname": "pyoutlineapi.ResponseParser.parse", "modulename": "pyoutlineapi", "qualname": "ResponseParser.parse", "kind": "function", "doc": "

    Parse and validate response data with comprehensive error handling.

    \n\n

    Type-safe overloads ensure correct return type based on as_json parameter.

    \n\n
    Parameters
    \n\n
      \n
    • data: Raw response data from API
    • \n
    • model: Pydantic model class for validation
    • \n
    • as_json: Return raw JSON dict instead of model instance
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated model instance or JSON dict

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValidationError: If validation fails with detailed error info
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> data = {"name": "test", "id": 123}\n>>> # Type-safe: returns MyModel instance\n>>> result = ResponseParser.parse(data, MyModel, as_json=False)\n>>> # Type-safe: returns dict\n>>> json_result = ResponseParser.parse(data, MyModel, as_json=True)\n
    \n
    \n
    \n", "signature": "(\tdata: dict[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]],\tmodel: type[~T],\t*,\tas_json: bool = False) -> Union[~T, dict[str, Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]]]:", "funcdef": "def"}, "pyoutlineapi.ResponseParser.parse_simple": {"fullname": "pyoutlineapi.ResponseParser.parse_simple", "modulename": "pyoutlineapi", "qualname": "ResponseParser.parse_simple", "kind": "function", "doc": "

    Parse simple success/error responses efficiently.

    \n\n

    Handles various response formats with minimal overhead:

    \n\n
      \n
    • {\"success\": true/false}
    • \n
    • {\"error\": \"...\"} \u2192 False
    • \n
    • {\"message\": \"...\"} \u2192 False
    • \n
    • Empty dict \u2192 True (assumed success)
    • \n
    \n\n
    Parameters
    \n\n
      \n
    • data: Response data
    • \n
    \n\n
    Returns
    \n\n
    \n

    True if successful, False otherwise

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> ResponseParser.parse_simple({"success": True})\nTrue\n>>> ResponseParser.parse_simple({"error": "Something failed"})\nFalse\n>>> ResponseParser.parse_simple({})\nTrue\n
    \n
    \n
    \n", "signature": "(\tdata: Mapping[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]] | object) -> bool:", "funcdef": "def"}, "pyoutlineapi.ResponseParser.validate_response_structure": {"fullname": "pyoutlineapi.ResponseParser.validate_response_structure", "modulename": "pyoutlineapi", "qualname": "ResponseParser.validate_response_structure", "kind": "function", "doc": "

    Validate response structure without full parsing.

    \n\n

    Lightweight validation before expensive Pydantic validation.\nUseful for early rejection of malformed responses.

    \n\n
    Parameters
    \n\n
      \n
    • data: Response data to validate
    • \n
    • required_fields: Sequence of required field names
    • \n
    \n\n
    Returns
    \n\n
    \n

    True if structure is valid

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> data = {"id": 1, "name": "test"}\n>>> ResponseParser.validate_response_structure(data, ["id", "name"])\nTrue\n>>> ResponseParser.validate_response_structure(data, ["id", "missing"])\nFalse\n
    \n
    \n
    \n", "signature": "(\tdata: Mapping[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]] | object,\trequired_fields: Sequence[str] | None = None) -> bool:", "funcdef": "def"}, "pyoutlineapi.ResponseParser.extract_error_message": {"fullname": "pyoutlineapi.ResponseParser.extract_error_message", "modulename": "pyoutlineapi", "qualname": "ResponseParser.extract_error_message", "kind": "function", "doc": "

    Extract error message from response data efficiently.

    \n\n

    Checks common error field names in order of preference.\nUses pre-computed tuple for fast iteration.

    \n\n
    Parameters
    \n\n
      \n
    • data: Response data
    • \n
    \n\n
    Returns
    \n\n
    \n

    Error message or None if not found

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> ResponseParser.extract_error_message({"error": "Not found"})\n'Not found'\n>>> ResponseParser.extract_error_message({"message": "Failed"})\n'Failed'\n>>> ResponseParser.extract_error_message({"success": True})\nNone\n
    \n
    \n
    \n", "signature": "(\tdata: Mapping[str, typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[typing.Union[str, int, float, bool, NoneType, dict[str, typing.Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]] | object) -> str | None:", "funcdef": "def"}, "pyoutlineapi.ResponseParser.is_error_response": {"fullname": "pyoutlineapi.ResponseParser.is_error_response", "modulename": "pyoutlineapi", "qualname": "ResponseParser.is_error_response", "kind": "function", "doc": "

    Check if response indicates an error efficiently.

    \n\n

    Fast boolean check for error indicators in response.

    \n\n
    Parameters
    \n\n
      \n
    • data: Response data
    • \n
    \n\n
    Returns
    \n\n
    \n

    True if response indicates an error

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> ResponseParser.is_error_response({"error": "Failed"})\nTrue\n>>> ResponseParser.is_error_response({"success": False})\nTrue\n>>> ResponseParser.is_error_response({"success": True})\nFalse\n>>> ResponseParser.is_error_response({})\nFalse\n
    \n
    \n
    \n", "signature": "(data: Mapping[str, object] | object) -> bool:", "funcdef": "def"}, "pyoutlineapi.SecureIDGenerator": {"fullname": "pyoutlineapi.SecureIDGenerator", "modulename": "pyoutlineapi", "qualname": "SecureIDGenerator", "kind": "class", "doc": "

    Cryptographically secure ID generation.

    \n"}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"fullname": "pyoutlineapi.SecureIDGenerator.generate_correlation_id", "modulename": "pyoutlineapi", "qualname": "SecureIDGenerator.generate_correlation_id", "kind": "function", "doc": "

    Generate secure correlation ID with 128 bits entropy.

    \n\n

    Format: {timestamp_us}-{random_hex}

    \n\n
    Returns
    \n\n
    \n

    Correlation ID string

    \n
    \n", "signature": "() -> str:", "funcdef": "def"}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"fullname": "pyoutlineapi.SecureIDGenerator.generate_request_id", "modulename": "pyoutlineapi", "qualname": "SecureIDGenerator.generate_request_id", "kind": "function", "doc": "

    Generate secure request ID.

    \n\n

    Alias for correlation ID for API compatibility.

    \n\n
    Returns
    \n\n
    \n

    Request ID string

    \n
    \n", "signature": "() -> str:", "funcdef": "def"}, "pyoutlineapi.Server": {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "

    Server information model with optimized properties.

    \n\n

    SCHEMA: Based on GET /server response

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.Server.name": {"fullname": "pyoutlineapi.Server.name", "modulename": "pyoutlineapi", "qualname": "Server.name", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.Server.server_id": {"fullname": "pyoutlineapi.Server.server_id", "modulename": "pyoutlineapi", "qualname": "Server.server_id", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.Server.metrics_enabled": {"fullname": "pyoutlineapi.Server.metrics_enabled", "modulename": "pyoutlineapi", "qualname": "Server.metrics_enabled", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "PydanticUndefined"}, "pyoutlineapi.Server.created_timestamp_ms": {"fullname": "pyoutlineapi.Server.created_timestamp_ms", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_ms", "kind": "variable", "doc": "

    Unix timestamp in milliseconds

    \n", "annotation": ": Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in milliseconds', metadata=[Ge(ge=0)])]", "default_value": "PydanticUndefined"}, "pyoutlineapi.Server.port_for_new_access_keys": {"fullname": "pyoutlineapi.Server.port_for_new_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.port_for_new_access_keys", "kind": "variable", "doc": "

    Port number (1-65535)

    \n", "annotation": ": Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Port number (1-65535)', metadata=[Ge(ge=1), Le(le=65535)])]", "default_value": "PydanticUndefined"}, "pyoutlineapi.Server.hostname_for_access_keys": {"fullname": "pyoutlineapi.Server.hostname_for_access_keys", "modulename": "pyoutlineapi", "qualname": "Server.hostname_for_access_keys", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.Server.access_key_data_limit": {"fullname": "pyoutlineapi.Server.access_key_data_limit", "modulename": "pyoutlineapi", "qualname": "Server.access_key_data_limit", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataLimit | None", "default_value": "None"}, "pyoutlineapi.Server.version": {"fullname": "pyoutlineapi.Server.version", "modulename": "pyoutlineapi", "qualname": "Server.version", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.Server.validate_name": {"fullname": "pyoutlineapi.Server.validate_name", "modulename": "pyoutlineapi", "qualname": "Server.validate_name", "kind": "function", "doc": "

    Validate server name.

    \n\n
    Parameters
    \n\n
      \n
    • v: Server name
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated name

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If name is empty
    • \n
    \n", "signature": "(cls, v: str) -> str:", "funcdef": "def"}, "pyoutlineapi.Server.has_global_limit": {"fullname": "pyoutlineapi.Server.has_global_limit", "modulename": "pyoutlineapi", "qualname": "Server.has_global_limit", "kind": "variable", "doc": "

    Check if server has global data limit (optimized).

    \n\n
    Returns
    \n\n
    \n

    True if global limit exists

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.Server.created_timestamp_seconds": {"fullname": "pyoutlineapi.Server.created_timestamp_seconds", "modulename": "pyoutlineapi", "qualname": "Server.created_timestamp_seconds", "kind": "variable", "doc": "

    Get creation timestamp in seconds (cached).

    \n\n

    NOTE: Cached because timestamp is immutable

    \n\n
    Returns
    \n\n
    \n

    Timestamp in seconds

    \n
    \n", "annotation": ": float"}, "pyoutlineapi.ServerExperimentalMetric": {"fullname": "pyoutlineapi.ServerExperimentalMetric", "modulename": "pyoutlineapi", "qualname": "ServerExperimentalMetric", "kind": "class", "doc": "

    Server-level experimental metrics.

    \n\n

    SCHEMA: Based on experimental metrics server object

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"fullname": "pyoutlineapi.ServerExperimentalMetric.tunnel_time", "modulename": "pyoutlineapi", "qualname": "ServerExperimentalMetric.tunnel_time", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.TunnelTime", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"fullname": "pyoutlineapi.ServerExperimentalMetric.data_transferred", "modulename": "pyoutlineapi", "qualname": "ServerExperimentalMetric.data_transferred", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.DataTransferred", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"fullname": "pyoutlineapi.ServerExperimentalMetric.bandwidth", "modulename": "pyoutlineapi", "qualname": "ServerExperimentalMetric.bandwidth", "kind": "variable", "doc": "

    \n", "annotation": ": pyoutlineapi.models.BandwidthInfo", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerExperimentalMetric.locations": {"fullname": "pyoutlineapi.ServerExperimentalMetric.locations", "modulename": "pyoutlineapi", "qualname": "ServerExperimentalMetric.locations", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyoutlineapi.models.LocationMetric]", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerMetrics": {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "

    Transfer metrics with optimized aggregations.

    \n\n

    SCHEMA: Based on GET /metrics/transfer response

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"fullname": "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.bytes_transferred_by_user_id", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, int]", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerMetrics.total_bytes": {"fullname": "pyoutlineapi.ServerMetrics.total_bytes", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.total_bytes", "kind": "variable", "doc": "

    Calculate total bytes with caching.

    \n\n
    Returns
    \n\n
    \n

    Total bytes transferred

    \n
    \n", "annotation": ": int"}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"fullname": "pyoutlineapi.ServerMetrics.total_gigabytes", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.total_gigabytes", "kind": "variable", "doc": "

    Get total in gigabytes (uses cached total_bytes).

    \n\n
    Returns
    \n\n
    \n

    Total GB transferred

    \n
    \n", "annotation": ": float"}, "pyoutlineapi.ServerMetrics.user_count": {"fullname": "pyoutlineapi.ServerMetrics.user_count", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.user_count", "kind": "variable", "doc": "

    Get number of users (cached).

    \n\n
    Returns
    \n\n
    \n

    Number of users

    \n
    \n", "annotation": ": int"}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"fullname": "pyoutlineapi.ServerMetrics.get_user_bytes", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.get_user_bytes", "kind": "function", "doc": "

    Get bytes for specific user (O(1) dict lookup).

    \n\n
    Parameters
    \n\n
      \n
    • user_id: User/key ID
    • \n
    \n\n
    Returns
    \n\n
    \n

    Bytes transferred or 0 if not found

    \n
    \n", "signature": "(self, user_id: str) -> int:", "funcdef": "def"}, "pyoutlineapi.ServerMetrics.top_users": {"fullname": "pyoutlineapi.ServerMetrics.top_users", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.top_users", "kind": "function", "doc": "

    Get top users by bytes transferred (optimized sorting).

    \n\n
    Parameters
    \n\n
      \n
    • limit: Number of top users to return
    • \n
    \n\n
    Returns
    \n\n
    \n

    List of (user_id, bytes) tuples

    \n
    \n", "signature": "(self, limit: int = 10) -> list[tuple[str, int]]:", "funcdef": "def"}, "pyoutlineapi.ServerNameRequest": {"fullname": "pyoutlineapi.ServerNameRequest", "modulename": "pyoutlineapi", "qualname": "ServerNameRequest", "kind": "class", "doc": "

    Request model for renaming server.

    \n\n

    SCHEMA: Based on PUT /name request body

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.ServerNameRequest.name": {"fullname": "pyoutlineapi.ServerNameRequest.name", "modulename": "pyoutlineapi", "qualname": "ServerNameRequest.name", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerSummary": {"fullname": "pyoutlineapi.ServerSummary", "modulename": "pyoutlineapi", "qualname": "ServerSummary", "kind": "class", "doc": "

    Server summary with optimized aggregations.

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel"}, "pyoutlineapi.ServerSummary.server": {"fullname": "pyoutlineapi.ServerSummary.server", "modulename": "pyoutlineapi", "qualname": "ServerSummary.server", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, typing.Any]", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerSummary.access_keys_count": {"fullname": "pyoutlineapi.ServerSummary.access_keys_count", "modulename": "pyoutlineapi", "qualname": "ServerSummary.access_keys_count", "kind": "variable", "doc": "

    \n", "annotation": ": int", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerSummary.healthy": {"fullname": "pyoutlineapi.ServerSummary.healthy", "modulename": "pyoutlineapi", "qualname": "ServerSummary.healthy", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "PydanticUndefined"}, "pyoutlineapi.ServerSummary.transfer_metrics": {"fullname": "pyoutlineapi.ServerSummary.transfer_metrics", "modulename": "pyoutlineapi", "qualname": "ServerSummary.transfer_metrics", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, int] | None", "default_value": "None"}, "pyoutlineapi.ServerSummary.experimental_metrics": {"fullname": "pyoutlineapi.ServerSummary.experimental_metrics", "modulename": "pyoutlineapi", "qualname": "ServerSummary.experimental_metrics", "kind": "variable", "doc": "

    \n", "annotation": ": dict[str, typing.Any] | None", "default_value": "None"}, "pyoutlineapi.ServerSummary.error": {"fullname": "pyoutlineapi.ServerSummary.error", "modulename": "pyoutlineapi", "qualname": "ServerSummary.error", "kind": "variable", "doc": "

    \n", "annotation": ": str | None", "default_value": "None"}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"fullname": "pyoutlineapi.ServerSummary.total_bytes_transferred", "modulename": "pyoutlineapi", "qualname": "ServerSummary.total_bytes_transferred", "kind": "variable", "doc": "

    Get total bytes with early return optimization.

    \n\n
    Returns
    \n\n
    \n

    Total bytes or 0 if no metrics

    \n
    \n", "annotation": ": int"}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"fullname": "pyoutlineapi.ServerSummary.total_gigabytes_transferred", "modulename": "pyoutlineapi", "qualname": "ServerSummary.total_gigabytes_transferred", "kind": "variable", "doc": "

    Get total GB (uses total_bytes_transferred).

    \n\n
    Returns
    \n\n
    \n

    Total GB or 0.0 if no metrics

    \n
    \n", "annotation": ": float"}, "pyoutlineapi.ServerSummary.has_errors": {"fullname": "pyoutlineapi.ServerSummary.has_errors", "modulename": "pyoutlineapi", "qualname": "ServerSummary.has_errors", "kind": "variable", "doc": "

    Check if summary has errors (optimized None check).

    \n\n
    Returns
    \n\n
    \n

    True if errors present

    \n
    \n", "annotation": ": bool"}, "pyoutlineapi.TimestampMs": {"fullname": "pyoutlineapi.TimestampMs", "modulename": "pyoutlineapi", "qualname": "TimestampMs", "kind": "variable", "doc": "

    \n", "default_value": "typing.Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in milliseconds', metadata=[Ge(ge=0)])]"}, "pyoutlineapi.TimestampSec": {"fullname": "pyoutlineapi.TimestampSec", "modulename": "pyoutlineapi", "qualname": "TimestampSec", "kind": "variable", "doc": "

    \n", "default_value": "typing.Annotated[int, FieldInfo(annotation=NoneType, required=True, description='Unix timestamp in seconds', metadata=[Ge(ge=0)])]"}, "pyoutlineapi.TunnelTime": {"fullname": "pyoutlineapi.TunnelTime", "modulename": "pyoutlineapi", "qualname": "TunnelTime", "kind": "class", "doc": "

    Tunnel time metric with time conversions.

    \n\n

    SCHEMA: Based on experimental metrics tunnelTime object

    \n", "bases": "pyoutlineapi.common_types.BaseValidatedModel, pyoutlineapi.models.TimeConversionMixin"}, "pyoutlineapi.TunnelTime.seconds": {"fullname": "pyoutlineapi.TunnelTime.seconds", "modulename": "pyoutlineapi", "qualname": "TunnelTime.seconds", "kind": "variable", "doc": "

    \n", "annotation": ": int", "default_value": "PydanticUndefined"}, "pyoutlineapi.ValidationError": {"fullname": "pyoutlineapi.ValidationError", "modulename": "pyoutlineapi", "qualname": "ValidationError", "kind": "class", "doc": "

    Data validation failure.

    \n\n

    Raised when data fails validation against expected schema.

    \n\n
    Attributes:
    \n\n
      \n
    • field: Field name that failed validation
    • \n
    • model: Model name
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = ValidationError(\n...     "Invalid port number", field="port", model="ServerConfig"\n... )\n
    \n
    \n
    \n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.ValidationError.__init__": {"fullname": "pyoutlineapi.ValidationError.__init__", "modulename": "pyoutlineapi", "qualname": "ValidationError.__init__", "kind": "function", "doc": "

    Initialize validation error.

    \n\n
    Arguments:
    \n\n
      \n
    • message: Error message
    • \n
    • field: Field name that failed validation
    • \n
    • model: Model name
    • \n
    \n", "signature": "(message: str, *, field: str | None = None, model: str | None = None)"}, "pyoutlineapi.ValidationError.field": {"fullname": "pyoutlineapi.ValidationError.field", "modulename": "pyoutlineapi", "qualname": "ValidationError.field", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.ValidationError.model": {"fullname": "pyoutlineapi.ValidationError.model", "modulename": "pyoutlineapi", "qualname": "ValidationError.model", "kind": "variable", "doc": "

    \n"}, "pyoutlineapi.Validators": {"fullname": "pyoutlineapi.Validators", "modulename": "pyoutlineapi", "qualname": "Validators", "kind": "class", "doc": "

    Input validation utilities with security hardening.

    \n"}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"fullname": "pyoutlineapi.Validators.validate_cert_fingerprint", "modulename": "pyoutlineapi", "qualname": "Validators.validate_cert_fingerprint", "kind": "function", "doc": "

    Validate and normalize certificate fingerprint.

    \n\n
    Parameters
    \n\n
      \n
    • fingerprint: SHA-256 fingerprint
    • \n
    \n\n
    Returns
    \n\n
    \n

    Normalized fingerprint (lowercase, no separators)

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If format is invalid
    • \n
    \n", "signature": "(fingerprint: pydantic.types.SecretStr) -> pydantic.types.SecretStr:", "funcdef": "def"}, "pyoutlineapi.Validators.validate_port": {"fullname": "pyoutlineapi.Validators.validate_port", "modulename": "pyoutlineapi", "qualname": "Validators.validate_port", "kind": "function", "doc": "

    Validate port number.

    \n\n
    Parameters
    \n\n
      \n
    • port: Port number
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated port

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If port is out of range
    • \n
    \n", "signature": "(port: int) -> int:", "funcdef": "def"}, "pyoutlineapi.Validators.validate_name": {"fullname": "pyoutlineapi.Validators.validate_name", "modulename": "pyoutlineapi", "qualname": "Validators.validate_name", "kind": "function", "doc": "

    Validate name field.

    \n\n
    Parameters
    \n\n
      \n
    • name: Name to validate
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated name

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If name is invalid
    • \n
    \n", "signature": "(name: str) -> str:", "funcdef": "def"}, "pyoutlineapi.Validators.validate_url": {"fullname": "pyoutlineapi.Validators.validate_url", "modulename": "pyoutlineapi", "qualname": "Validators.validate_url", "kind": "function", "doc": "

    Validate and sanitize URL.

    \n\n
    Parameters
    \n\n
      \n
    • url: URL to validate
    • \n
    • allow_private_networks: Allow private/local network addresses
    • \n
    • resolve_dns: Resolve hostname and block private/reserved IPs
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated URL

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If URL is invalid
    • \n
    \n", "signature": "(\turl: str,\t*,\tallow_private_networks: bool = True,\tresolve_dns: bool = False) -> str:", "funcdef": "def"}, "pyoutlineapi.Validators.validate_string_not_empty": {"fullname": "pyoutlineapi.Validators.validate_string_not_empty", "modulename": "pyoutlineapi", "qualname": "Validators.validate_string_not_empty", "kind": "function", "doc": "

    Validate string is not empty.

    \n\n
    Parameters
    \n\n
      \n
    • value: String value
    • \n
    • field_name: Field name for error messages
    • \n
    \n\n
    Returns
    \n\n
    \n

    Stripped string

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If string is empty
    • \n
    \n", "signature": "(value: str, field_name: str) -> str:", "funcdef": "def"}, "pyoutlineapi.Validators.validate_non_negative": {"fullname": "pyoutlineapi.Validators.validate_non_negative", "modulename": "pyoutlineapi", "qualname": "Validators.validate_non_negative", "kind": "function", "doc": "

    Validate integer is non-negative.

    \n\n
    Parameters
    \n\n
      \n
    • value: Integer value
    • \n
    • name: Field name for error messages
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated value

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If value is negative
    • \n
    \n", "signature": "(value: pyoutlineapi.models.DataLimit | int, name: str) -> int:", "funcdef": "def"}, "pyoutlineapi.Validators.validate_since": {"fullname": "pyoutlineapi.Validators.validate_since", "modulename": "pyoutlineapi", "qualname": "Validators.validate_since", "kind": "function", "doc": "

    Validate experimental metrics 'since' parameter.

    \n\n

    Accepts:

    \n\n
      \n
    • Relative durations: 24h, 7d, 30m, 15s
    • \n
    • ISO-8601 timestamps (e.g., 2024-01-01T00:00:00Z)
    • \n
    \n\n
    Parameters
    \n\n
      \n
    • value: Since parameter
    • \n
    \n\n
    Returns
    \n\n
    \n

    Sanitized since value

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If value is invalid
    • \n
    \n", "signature": "(value: str) -> str:", "funcdef": "def"}, "pyoutlineapi.Validators.validate_key_id": {"fullname": "pyoutlineapi.Validators.validate_key_id", "modulename": "pyoutlineapi", "qualname": "Validators.validate_key_id", "kind": "function", "doc": "

    Enhanced key_id validation.

    \n\n
    Parameters
    \n\n
      \n
    • key_id: Key ID to validate
    • \n
    \n\n
    Returns
    \n\n
    \n

    Validated key ID

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If key ID is invalid
    • \n
    \n", "signature": "(cls, key_id: str) -> str:", "funcdef": "def"}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"fullname": "pyoutlineapi.Validators.sanitize_url_for_logging", "modulename": "pyoutlineapi", "qualname": "Validators.sanitize_url_for_logging", "kind": "function", "doc": "

    Remove secret path from URL for safe logging.

    \n\n
    Parameters
    \n\n
      \n
    • url: URL to sanitize
    • \n
    \n\n
    Returns
    \n\n
    \n

    Sanitized URL

    \n
    \n", "signature": "(url: str) -> str:", "funcdef": "def"}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"fullname": "pyoutlineapi.Validators.sanitize_endpoint_for_logging", "modulename": "pyoutlineapi", "qualname": "Validators.sanitize_endpoint_for_logging", "kind": "function", "doc": "

    Sanitize endpoint for safe logging.

    \n\n
    Parameters
    \n\n
      \n
    • endpoint: Endpoint to sanitize
    • \n
    \n\n
    Returns
    \n\n
    \n

    Sanitized endpoint

    \n
    \n", "signature": "(endpoint: str) -> str:", "funcdef": "def"}, "pyoutlineapi.audited": {"fullname": "pyoutlineapi.audited", "modulename": "pyoutlineapi", "qualname": "audited", "kind": "function", "doc": "

    Audit logging decorator with zero-config smart extraction.

    \n\n

    Automatically extracts ALL information from function signature and execution:

    \n\n
      \n
    • Action name: from function name
    • \n
    • Resource: from result.id, first parameter, or function analysis
    • \n
    • Details: from function signature (excluding None and defaults)
    • \n
    • Correlation ID: from instance._correlation_id if available
    • \n
    • Success/failure: from exception handling
    • \n
    \n\n
    Usage:
    \n\n
    \n

    @audited()\n async def create_access_key(self, name: str, port: int = 8080) -> AccessKey:\n # action: \"create_access_key\"\n # resource: result.id\n # details: {\"name\": \"...\", \"port\": 8080} (if not default)\n ...

    \n \n

    @audited(log_success=False)\n async def critical_operation(self, resource_id: str) -> bool:\n # Only logs failures for alerting\n ...

    \n
    \n\n
    Parameters
    \n\n
      \n
    • log_success: Log successful operations (default: True)
    • \n
    • log_failure: Log failed operations (default: True)
    • \n
    \n\n
    Returns
    \n\n
    \n

    Decorated function with automatic audit logging

    \n
    \n", "signature": "(\t*,\tlog_success: bool = True,\tlog_failure: bool = True) -> Callable[[Callable[~P, ~R]], Callable[~P, ~R]]:", "funcdef": "def"}, "pyoutlineapi.build_config_overrides": {"fullname": "pyoutlineapi.build_config_overrides", "modulename": "pyoutlineapi", "qualname": "build_config_overrides", "kind": "function", "doc": "

    Build configuration overrides dictionary from kwargs.

    \n\n

    DRY implementation - single source of truth for config building.

    \n\n
    Parameters
    \n\n
      \n
    • kwargs: Configuration parameters
    • \n
    \n\n
    Returns
    \n\n
    \n

    Dictionary containing only non-None values

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> overrides = build_config_overrides(timeout=20, enable_logging=True)\n>>> # Returns: {'timeout': 20, 'enable_logging': True}\n
    \n
    \n
    \n", "signature": "(\t**kwargs: int | str | bool | float | None) -> dict[str, int | str | bool | float | None]:", "funcdef": "def"}, "pyoutlineapi.correlation_id": {"fullname": "pyoutlineapi.correlation_id", "modulename": "pyoutlineapi", "qualname": "correlation_id", "kind": "variable", "doc": "

    \n", "default_value": "<ContextVar name='correlation_id' default=''>"}, "pyoutlineapi.create_client": {"fullname": "pyoutlineapi.create_client", "modulename": "pyoutlineapi", "qualname": "create_client", "kind": "function", "doc": "

    Create client with minimal parameters.

    \n\n

    Convenience function for quick client creation without\nexplicit configuration object. Uses modern **overrides approach.

    \n\n
    Parameters
    \n\n
      \n
    • api_url: API URL with secret path
    • \n
    • cert_sha256: SHA-256 certificate fingerprint
    • \n
    • audit_logger: Custom audit logger (optional)
    • \n
    • metrics: Custom metrics collector (optional)
    • \n
    • overrides: Configuration overrides (timeout, retry_attempts, etc.)
    • \n
    \n\n
    Returns
    \n\n
    \n

    Configured client instance (use with async context manager)

    \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If parameters are invalid
    • \n
    \n\n

    Example (advanced, prefer from_env for production):

    \n\n
    \n
    \n
    \n

    async with AsyncOutlineClient.from_env() as client:\n ... info = await client.get_server_info()

    \n
    \n
    \n
    \n", "signature": "(\tapi_url: str,\tcert_sha256: str,\t*,\taudit_logger: pyoutlineapi.audit.AuditLogger | None = None,\tmetrics: pyoutlineapi.base_client.MetricsCollector | None = None,\t**overrides: Unpack[pyoutlineapi.common_types.ConfigOverrides]) -> pyoutlineapi.client.AsyncOutlineClient:", "funcdef": "def"}, "pyoutlineapi.create_env_template": {"fullname": "pyoutlineapi.create_env_template", "modulename": "pyoutlineapi", "qualname": "create_env_template", "kind": "function", "doc": "

    Create .env template file (optimized I/O).

    \n\n

    Performance: Uses cached template and efficient Path operations

    \n\n
    Parameters
    \n\n
      \n
    • path: Path to template file
    • \n
    \n", "signature": "(path: str | pathlib._local.Path = '.env.example') -> None:", "funcdef": "def"}, "pyoutlineapi.create_multi_server_manager": {"fullname": "pyoutlineapi.create_multi_server_manager", "modulename": "pyoutlineapi", "qualname": "create_multi_server_manager", "kind": "function", "doc": "

    Create multiserver manager with configurations.

    \n\n

    Convenience function for creating a manager for multiple servers.

    \n\n
    Parameters
    \n\n
      \n
    • configs: Sequence of server configurations
    • \n
    • audit_logger: Shared audit logger
    • \n
    • metrics: Shared metrics collector
    • \n
    • default_timeout: Default operation timeout
    • \n
    \n\n
    Returns
    \n\n
    \n

    MultiServerManager instance (use with async context manager)

    \n
    \n\n
    Raises
    \n\n
      \n
    • ConfigurationError: If configurations are invalid
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> configs = [\n...     OutlineClientConfig.create_minimal("https://s1.com/path", "a" * 64),\n...     OutlineClientConfig.create_minimal("https://s2.com/path", "b" * 64),\n... ]\n>>> async with create_multi_server_manager(configs) as manager:\n...     health = await manager.health_check_all()\n
    \n
    \n
    \n", "signature": "(\tconfigs: Sequence[pyoutlineapi.config.OutlineClientConfig],\t*,\taudit_logger: pyoutlineapi.audit.AuditLogger | None = None,\tmetrics: pyoutlineapi.base_client.MetricsCollector | None = None,\tdefault_timeout: float = 5.0) -> pyoutlineapi.client.MultiServerManager:", "funcdef": "def"}, "pyoutlineapi.format_error_chain": {"fullname": "pyoutlineapi.format_error_chain", "modulename": "pyoutlineapi", "qualname": "format_error_chain", "kind": "function", "doc": "

    Format exception chain for structured logging.

    \n\n
    Arguments:
    \n\n
      \n
    • error: Exception to format
    • \n
    \n\n
    Returns:
    \n\n
    \n

    List of error dictionaries ordered from root to leaf

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> try:\n...     raise ValueError("Inner") from KeyError("Outer")\n... except Exception as e:\n...     chain = format_error_chain(e)\n...     len(chain)  # 2\n
    \n
    \n
    \n", "signature": "(error: Exception) -> list[dict[str, typing.Any]]:", "funcdef": "def"}, "pyoutlineapi.get_audit_logger": {"fullname": "pyoutlineapi.get_audit_logger", "modulename": "pyoutlineapi", "qualname": "get_audit_logger", "kind": "function", "doc": "

    Get audit logger from current context.

    \n\n
    Returns
    \n\n
    \n

    Audit logger instance or None

    \n
    \n", "signature": "() -> pyoutlineapi.audit.AuditLogger | None:", "funcdef": "def"}, "pyoutlineapi.get_or_create_audit_logger": {"fullname": "pyoutlineapi.get_or_create_audit_logger", "modulename": "pyoutlineapi", "qualname": "get_or_create_audit_logger", "kind": "function", "doc": "

    Get or create audit logger with weak reference caching.

    \n\n
    Parameters
    \n\n
      \n
    • instance_id: Instance ID for caching (optional)
    • \n
    \n\n
    Returns
    \n\n
    \n

    Audit logger instance

    \n
    \n", "signature": "(instance_id: int | None = None) -> pyoutlineapi.audit.AuditLogger:", "funcdef": "def"}, "pyoutlineapi.get_retry_delay": {"fullname": "pyoutlineapi.get_retry_delay", "modulename": "pyoutlineapi", "qualname": "get_retry_delay", "kind": "function", "doc": "

    Get suggested retry delay for an error.

    \n\n
    Arguments:
    \n\n
      \n
    • error: Exception to check
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Retry delay in seconds, or None if not retryable

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = OutlineTimeoutError("Timeout")\n>>> get_retry_delay(error)  # 2.0\n
    \n
    \n
    \n", "signature": "(error: Exception) -> float | None:", "funcdef": "def"}, "pyoutlineapi.get_safe_error_dict": {"fullname": "pyoutlineapi.get_safe_error_dict", "modulename": "pyoutlineapi", "qualname": "get_safe_error_dict", "kind": "function", "doc": "

    Extract safe error information for logging.

    \n\n

    Returns only safe information without sensitive data.

    \n\n
    Arguments:
    \n\n
      \n
    • error: Exception to convert
    • \n
    \n\n
    Returns:
    \n\n
    \n

    Safe error dictionary suitable for logging

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = APIError("Not found", status_code=404)\n>>> get_safe_error_dict(error)\n{'type': 'APIError', 'message': 'Not found', 'status_code': 404, ...}\n
    \n
    \n
    \n", "signature": "(error: BaseException) -> dict[str, typing.Any]:", "funcdef": "def"}, "pyoutlineapi.get_version": {"fullname": "pyoutlineapi.get_version", "modulename": "pyoutlineapi", "qualname": "get_version", "kind": "function", "doc": "

    Get package version string.

    \n\n
    Returns
    \n\n
    \n

    Package version

    \n
    \n", "signature": "() -> str:", "funcdef": "def"}, "pyoutlineapi.is_json_serializable": {"fullname": "pyoutlineapi.is_json_serializable", "modulename": "pyoutlineapi", "qualname": "is_json_serializable", "kind": "function", "doc": "

    Type guard for JSON-serializable values.

    \n\n
    Parameters
    \n\n
      \n
    • value: Value to check
    • \n
    \n\n
    Returns
    \n\n
    \n

    True if value is JSON-serializable

    \n
    \n", "signature": "(\tvalue: object) -> TypeGuard[Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), list[Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]]]], list[Union[str, int, float, bool, NoneType, dict[str, Union[str, int, float, bool, NoneType, ForwardRef('JsonDict'), ForwardRef('JsonList')]], ForwardRef('JsonList')]]]]:", "funcdef": "def"}, "pyoutlineapi.is_retryable": {"fullname": "pyoutlineapi.is_retryable", "modulename": "pyoutlineapi", "qualname": "is_retryable", "kind": "function", "doc": "

    Check if error should be retried.

    \n\n
    Arguments:
    \n\n
      \n
    • error: Exception to check
    • \n
    \n\n
    Returns:
    \n\n
    \n

    True if error is retryable

    \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> error = APIError("Server error", status_code=503)\n>>> is_retryable(error)  # True\n
    \n
    \n
    \n", "signature": "(error: Exception) -> bool:", "funcdef": "def"}, "pyoutlineapi.is_valid_bytes": {"fullname": "pyoutlineapi.is_valid_bytes", "modulename": "pyoutlineapi", "qualname": "is_valid_bytes", "kind": "function", "doc": "

    Type guard for valid byte counts.

    \n\n
    Parameters
    \n\n
      \n
    • value: Value to check
    • \n
    \n\n
    Returns
    \n\n
    \n

    True if value is valid bytes

    \n
    \n", "signature": "(value: object) -> TypeGuard[int]:", "funcdef": "def"}, "pyoutlineapi.is_valid_port": {"fullname": "pyoutlineapi.is_valid_port", "modulename": "pyoutlineapi", "qualname": "is_valid_port", "kind": "function", "doc": "

    Type guard for valid port numbers.

    \n\n
    Parameters
    \n\n
      \n
    • value: Value to check
    • \n
    \n\n
    Returns
    \n\n
    \n

    True if value is valid port

    \n
    \n", "signature": "(value: object) -> TypeGuard[int]:", "funcdef": "def"}, "pyoutlineapi.load_config": {"fullname": "pyoutlineapi.load_config", "modulename": "pyoutlineapi", "qualname": "load_config", "kind": "function", "doc": "

    Load configuration for environment (optimized lookup).

    \n\n
    Parameters
    \n\n
      \n
    • environment: Environment name (development, production, custom)
    • \n
    • overrides: Configuration parameters to override
    • \n
    \n\n
    Returns
    \n\n
    \n

    Configuration instance

    \n
    \n\n
    Raises
    \n\n
      \n
    • ValueError: If environment name is invalid
    • \n
    \n\n
    Example:
    \n\n
    \n
    \n
    >>> config = load_config("production", timeout=20)\n
    \n
    \n
    \n", "signature": "(\tenvironment: str = 'custom',\t**overrides: int | str | bool | float) -> pyoutlineapi.config.OutlineClientConfig:", "funcdef": "def"}, "pyoutlineapi.mask_sensitive_data": {"fullname": "pyoutlineapi.mask_sensitive_data", "modulename": "pyoutlineapi", "qualname": "mask_sensitive_data", "kind": "function", "doc": "

    Sensitive data masking with lazy copying and optimized recursion.

    \n\n

    Uses lazy copying - only creates new dict when needed.\nIncludes recursion depth protection.

    \n\n
    Parameters
    \n\n
      \n
    • data: Data dictionary to mask
    • \n
    • sensitive_keys: Set of sensitive key names (case-insensitive matching)
    • \n
    • _depth: Current recursion depth (internal)
    • \n
    \n\n
    Returns
    \n\n
    \n

    Masked data dictionary (may be same object if no sensitive data found)

    \n
    \n", "signature": "(\tdata: Mapping[str, typing.Any],\t*,\tsensitive_keys: frozenset[str] | None = None,\t_depth: int = 0) -> dict[str, typing.Any]:", "funcdef": "def"}, "pyoutlineapi.print_type_info": {"fullname": "pyoutlineapi.print_type_info", "modulename": "pyoutlineapi", "qualname": "print_type_info", "kind": "function", "doc": "

    Print information about available type aliases for advanced usage.

    \n", "signature": "() -> None:", "funcdef": "def"}, "pyoutlineapi.quick_setup": {"fullname": "pyoutlineapi.quick_setup", "modulename": "pyoutlineapi", "qualname": "quick_setup", "kind": "function", "doc": "

    Create configuration template file for quick setup.

    \n\n

    Creates .env.example file with all available configuration options.

    \n", "signature": "() -> None:", "funcdef": "def"}, "pyoutlineapi.set_audit_logger": {"fullname": "pyoutlineapi.set_audit_logger", "modulename": "pyoutlineapi", "qualname": "set_audit_logger", "kind": "function", "doc": "

    Set audit logger for current async context.

    \n\n

    Thread-safe and async-safe using contextvars.\nPreferred over global state for high-load applications.

    \n\n
    Parameters
    \n\n
      \n
    • logger_instance: Audit logger instance
    • \n
    \n", "signature": "(logger_instance: pyoutlineapi.audit.AuditLogger) -> None:", "funcdef": "def"}}, "docInfo": {"pyoutlineapi": {"qualname": 0, "fullname": 1, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 589}, "pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 47, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.APIError": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 174}, "pyoutlineapi.APIError.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 112, "bases": 0, "doc": 56}, "pyoutlineapi.APIError.status_code": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.APIError.endpoint": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.APIError.response_data": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.APIError.is_retryable": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 12}, "pyoutlineapi.APIError.is_client_error": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 28}, "pyoutlineapi.APIError.is_server_error": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 28}, "pyoutlineapi.APIError.is_rate_limit_error": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 28}, "pyoutlineapi.AccessKey": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 21}, "pyoutlineapi.AccessKey.id": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKey.name": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKey.password": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKey.port": {"qualname": 2, "fullname": 3, "annotation": 23, "default_value": 1, "signature": 0, "bases": 0, "doc": 7}, "pyoutlineapi.AccessKey.method": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKey.access_url": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKey.data_limit": {"qualname": 3, "fullname": 4, "annotation": 6, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKey.validate_name": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 36, "bases": 0, "doc": 35}, "pyoutlineapi.AccessKey.validate_id": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 48}, "pyoutlineapi.AccessKey.has_data_limit": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 26}, "pyoutlineapi.AccessKey.display_name": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 20}, "pyoutlineapi.AccessKeyCreateRequest": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 19}, "pyoutlineapi.AccessKeyCreateRequest.name": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyCreateRequest.method": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyCreateRequest.password": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyCreateRequest.port": {"qualname": 2, "fullname": 3, "annotation": 23, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"qualname": 2, "fullname": 3, "annotation": 6, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyList": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 20}, "pyoutlineapi.AccessKeyList.access_keys": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyList.count": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 30}, "pyoutlineapi.AccessKeyList.is_empty": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 24}, "pyoutlineapi.AccessKeyList.get_by_id": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 41, "bases": 0, "doc": 43}, "pyoutlineapi.AccessKeyList.get_by_name": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 40, "bases": 0, "doc": 42}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 30, "bases": 0, "doc": 24}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 30, "bases": 0, "doc": 24}, "pyoutlineapi.AccessKeyMetric": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 17}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyMetric.connection": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AccessKeyNameRequest": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 19}, "pyoutlineapi.AccessKeyNameRequest.name": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AsyncOutlineClient": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 20, "doc": 12}, "pyoutlineapi.AsyncOutlineClient.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 194, "bases": 0, "doc": 181}, "pyoutlineapi.AsyncOutlineClient.config": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 0, "signature": 0, "bases": 0, "doc": 21}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"qualname": 4, "fullname": 5, "annotation": 4, "default_value": 0, "signature": 0, "bases": 0, "doc": 32}, "pyoutlineapi.AsyncOutlineClient.json_format": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 23}, "pyoutlineapi.AsyncOutlineClient.create": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 227, "bases": 0, "doc": 182}, "pyoutlineapi.AsyncOutlineClient.from_env": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 173, "bases": 0, "doc": 222}, "pyoutlineapi.AsyncOutlineClient.health_check": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 31, "bases": 0, "doc": 82}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 31, "bases": 0, "doc": 93}, "pyoutlineapi.AsyncOutlineClient.get_status": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 31, "bases": 0, "doc": 80}, "pyoutlineapi.AuditContext": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 23}, "pyoutlineapi.AuditContext.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 105, "bases": 0, "doc": 3}, "pyoutlineapi.AuditContext.action": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AuditContext.resource": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AuditContext.success": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AuditContext.details": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AuditContext.correlation_id": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.AuditContext.from_call": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 164, "bases": 0, "doc": 81}, "pyoutlineapi.AuditLogger": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 2, "doc": 20}, "pyoutlineapi.AuditLogger.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 18, "bases": 0, "doc": 3}, "pyoutlineapi.AuditLogger.alog_action": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 133, "bases": 0, "doc": 9}, "pyoutlineapi.AuditLogger.log_action": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 133, "bases": 0, "doc": 9}, "pyoutlineapi.AuditLogger.shutdown": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 14, "bases": 0, "doc": 6}, "pyoutlineapi.BandwidthData": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 16}, "pyoutlineapi.BandwidthData.data": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.BandwidthData.timestamp": {"qualname": 2, "fullname": 3, "annotation": 20, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.BandwidthDataValue": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 16}, "pyoutlineapi.BandwidthDataValue.bytes": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.BandwidthInfo": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 17}, "pyoutlineapi.BandwidthInfo.current": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.BandwidthInfo.peak": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitConfig": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 26}, "pyoutlineapi.CircuitConfig.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 82, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitConfig.failure_threshold": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitConfig.recovery_timeout": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitConfig.success_threshold": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitConfig.call_timeout": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 29}, "pyoutlineapi.CircuitMetrics.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 122, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics.total_calls": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics.successful_calls": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics.failed_calls": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics.state_changes": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics.last_success_time": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitMetrics.success_rate": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 29}, "pyoutlineapi.CircuitMetrics.failure_rate": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 29}, "pyoutlineapi.CircuitMetrics.to_dict": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 32, "bases": 0, "doc": 30}, "pyoutlineapi.CircuitOpenError": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 146}, "pyoutlineapi.CircuitOpenError.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 38, "bases": 0, "doc": 49}, "pyoutlineapi.CircuitOpenError.retry_after": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 7}, "pyoutlineapi.CircuitState": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 2, "doc": 29}, "pyoutlineapi.CircuitState.CLOSED": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 7, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitState.OPEN": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 7, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CircuitState.HALF_OPEN": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 8, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 2, "doc": 22}, "pyoutlineapi.ConfigOverrides.timeout": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.max_connections": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.rate_limit": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.user_agent": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.enable_logging": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.json_format": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigurationError": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 122}, "pyoutlineapi.ConfigurationError.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 64, "bases": 0, "doc": 40}, "pyoutlineapi.ConfigurationError.field": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ConfigurationError.security_issue": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 9}, "pyoutlineapi.Constants.MIN_PORT": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_PORT": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 2, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 7, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 8, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 2, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 2, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CredentialSanitizer": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 9}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 142, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.CredentialSanitizer.sanitize": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 35}, "pyoutlineapi.DataLimit": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 7, "doc": 11}, "pyoutlineapi.DataLimit.bytes": {"qualname": 2, "fullname": 3, "annotation": 19, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.DataLimit.from_kilobytes": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 33}, "pyoutlineapi.DataLimit.from_megabytes": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 33}, "pyoutlineapi.DataLimit.from_gigabytes": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 33}, "pyoutlineapi.DataLimitRequest": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 35}, "pyoutlineapi.DataLimitRequest.limit": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.DataLimitRequest.to_payload": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 26, "bases": 0, "doc": 22}, "pyoutlineapi.DataTransferred": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 7, "doc": 18}, "pyoutlineapi.DataTransferred.bytes": {"qualname": 2, "fullname": 3, "annotation": 19, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.DefaultAuditLogger": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 11}, "pyoutlineapi.DefaultAuditLogger.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 68, "bases": 0, "doc": 54}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 133, "bases": 0, "doc": 64}, "pyoutlineapi.DefaultAuditLogger.log_action": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 133, "bases": 0, "doc": 63}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 37, "bases": 0, "doc": 32}, "pyoutlineapi.DevelopmentConfig": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 42}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.DevelopmentConfig.timeout": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ErrorResponse": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 18}, "pyoutlineapi.ErrorResponse.code": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ErrorResponse.message": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ExperimentalMetrics": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 16}, "pyoutlineapi.ExperimentalMetrics.server": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 41, "bases": 0, "doc": 43}, "pyoutlineapi.HealthCheckResult": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 9}, "pyoutlineapi.HealthCheckResult.healthy": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.HealthCheckResult.timestamp": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.HealthCheckResult.checks": {"qualname": 2, "fullname": 3, "annotation": 5, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.HealthCheckResult.failed_checks": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 24}, "pyoutlineapi.HealthCheckResult.success_rate": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 27}, "pyoutlineapi.HostnameRequest": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 20}, "pyoutlineapi.HostnameRequest.hostname": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.JsonDict": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 16, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.JsonPayload": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 34, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.LocationMetric": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 17}, "pyoutlineapi.LocationMetric.location": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.LocationMetric.asn": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.LocationMetric.as_org": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.LocationMetric.tunnel_time": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.LocationMetric.data_transferred": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.MetricsCollector": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 2, "doc": 17}, "pyoutlineapi.MetricsCollector.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 18, "bases": 0, "doc": 3}, "pyoutlineapi.MetricsCollector.increment": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 64, "bases": 0, "doc": 6}, "pyoutlineapi.MetricsCollector.timing": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 79, "bases": 0, "doc": 6}, "pyoutlineapi.MetricsCollector.gauge": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 79, "bases": 0, "doc": 6}, "pyoutlineapi.MetricsEnabledRequest": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 17}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.MetricsStatusResponse": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 21}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.MetricsTags": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 2, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.MultiServerManager": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 72}, "pyoutlineapi.MultiServerManager.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 127, "bases": 0, "doc": 76}, "pyoutlineapi.MultiServerManager.server_count": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 21}, "pyoutlineapi.MultiServerManager.active_servers": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 22}, "pyoutlineapi.MultiServerManager.get_server_names": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 20, "bases": 0, "doc": 34}, "pyoutlineapi.MultiServerManager.get_client": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 43, "bases": 0, "doc": 67}, "pyoutlineapi.MultiServerManager.get_all_clients": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 30, "bases": 0, "doc": 20}, "pyoutlineapi.MultiServerManager.health_check_all": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 64, "bases": 0, "doc": 44}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 55, "bases": 0, "doc": 40}, "pyoutlineapi.MultiServerManager.get_status_summary": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 31, "bases": 0, "doc": 31}, "pyoutlineapi.NoOpAuditLogger": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 29}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 133, "bases": 0, "doc": 7}, "pyoutlineapi.NoOpAuditLogger.log_action": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 133, "bases": 0, "doc": 7}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 14, "bases": 0, "doc": 6}, "pyoutlineapi.NoOpMetrics": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 19}, "pyoutlineapi.NoOpMetrics.increment": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 64, "bases": 0, "doc": 8}, "pyoutlineapi.NoOpMetrics.timing": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 79, "bases": 0, "doc": 8}, "pyoutlineapi.NoOpMetrics.gauge": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 79, "bases": 0, "doc": 8}, "pyoutlineapi.OutlineClientConfig": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 5}, "pyoutlineapi.OutlineClientConfig.api_url": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 9}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 6}, "pyoutlineapi.OutlineClientConfig.timeout": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 6}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.OutlineClientConfig.max_connections": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.OutlineClientConfig.user_agent": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 7, "signature": 0, "bases": 0, "doc": 6}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.OutlineClientConfig.json_format": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 11}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"qualname": 5, "fullname": 6, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 10}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 5}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 2, "signature": 0, "bases": 0, "doc": 7}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 6}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 2, "signature": 0, "bases": 0, "doc": 7}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 53}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 44, "bases": 0, "doc": 51}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 57}, "pyoutlineapi.OutlineClientConfig.validate_config": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 14, "bases": 0, "doc": 23}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"qualname": 4, "fullname": 5, "annotation": 9, "default_value": 0, "signature": 0, "bases": 0, "doc": 47}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 56, "bases": 0, "doc": 114}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"qualname": 3, "fullname": 4, "annotation": 7, "default_value": 0, "signature": 0, "bases": 0, "doc": 42}, "pyoutlineapi.OutlineClientConfig.from_env": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 98, "bases": 0, "doc": 162}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 96, "bases": 0, "doc": 179}, "pyoutlineapi.OutlineConnectionError": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 131}, "pyoutlineapi.OutlineConnectionError.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 65, "bases": 0, "doc": 36}, "pyoutlineapi.OutlineConnectionError.host": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.OutlineConnectionError.port": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.OutlineError": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 2, "doc": 186}, "pyoutlineapi.OutlineError.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 104, "bases": 0, "doc": 67}, "pyoutlineapi.OutlineError.details": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 0, "signature": 0, "bases": 0, "doc": 52}, "pyoutlineapi.OutlineError.safe_details": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 0, "signature": 0, "bases": 0, "doc": 24}, "pyoutlineapi.OutlineError.is_retryable": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 11}, "pyoutlineapi.OutlineError.default_retry_delay": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 10}, "pyoutlineapi.OutlineTimeoutError": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 134}, "pyoutlineapi.OutlineTimeoutError.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 69, "bases": 0, "doc": 38}, "pyoutlineapi.OutlineTimeoutError.timeout": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.OutlineTimeoutError.operation": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.PeakDeviceCount": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 18}, "pyoutlineapi.PeakDeviceCount.data": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.PeakDeviceCount.timestamp": {"qualname": 2, "fullname": 3, "annotation": 20, "default_value": 1, "signature": 0, "bases": 0, "doc": 6}, "pyoutlineapi.PortRequest": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 22}, "pyoutlineapi.PortRequest.port": {"qualname": 2, "fullname": 3, "annotation": 23, "default_value": 1, "signature": 0, "bases": 0, "doc": 7}, "pyoutlineapi.ProductionConfig": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 40}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ProductionConfig.enable_logging": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ProductionConfig.enforce_security": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 14, "bases": 0, "doc": 38}, "pyoutlineapi.QueryParams": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 8, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ResponseData": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 16, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ResponseParser": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 13}, "pyoutlineapi.ResponseParser.parse": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 695, "bases": 0, "doc": 290}, "pyoutlineapi.ResponseParser.parse_simple": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 348, "bases": 0, "doc": 195}, "pyoutlineapi.ResponseParser.validate_response_structure": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 379, "bases": 0, "doc": 230}, "pyoutlineapi.ResponseParser.extract_error_message": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 354, "bases": 0, "doc": 203}, "pyoutlineapi.ResponseParser.is_error_response": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 37, "bases": 0, "doc": 203}, "pyoutlineapi.SecureIDGenerator": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 7}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 10, "bases": 0, "doc": 30}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 10, "bases": 0, "doc": 29}, "pyoutlineapi.Server": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 17}, "pyoutlineapi.Server.name": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Server.server_id": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Server.metrics_enabled": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Server.created_timestamp_ms": {"qualname": 4, "fullname": 5, "annotation": 20, "default_value": 1, "signature": 0, "bases": 0, "doc": 6}, "pyoutlineapi.Server.port_for_new_access_keys": {"qualname": 6, "fullname": 7, "annotation": 23, "default_value": 1, "signature": 0, "bases": 0, "doc": 7}, "pyoutlineapi.Server.hostname_for_access_keys": {"qualname": 5, "fullname": 6, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Server.access_key_data_limit": {"qualname": 5, "fullname": 6, "annotation": 6, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Server.version": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Server.validate_name": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 24, "bases": 0, "doc": 47}, "pyoutlineapi.Server.has_global_limit": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 25}, "pyoutlineapi.Server.created_timestamp_seconds": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 29}, "pyoutlineapi.ServerExperimentalMetric": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 16}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"qualname": 3, "fullname": 4, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerExperimentalMetric.locations": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerMetrics": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 16}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"qualname": 6, "fullname": 7, "annotation": 3, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerMetrics.total_bytes": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 20}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 23}, "pyoutlineapi.ServerMetrics.user_count": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 20}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 25, "bases": 0, "doc": 43}, "pyoutlineapi.ServerMetrics.top_users": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 47, "bases": 0, "doc": 44}, "pyoutlineapi.ServerNameRequest": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 17}, "pyoutlineapi.ServerNameRequest.name": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerSummary": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 4, "doc": 8}, "pyoutlineapi.ServerSummary.server": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerSummary.access_keys_count": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerSummary.healthy": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerSummary.transfer_metrics": {"qualname": 3, "fullname": 4, "annotation": 5, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerSummary.experimental_metrics": {"qualname": 3, "fullname": 4, "annotation": 6, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerSummary.error": {"qualname": 2, "fullname": 3, "annotation": 4, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 26}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"qualname": 4, "fullname": 5, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 27}, "pyoutlineapi.ServerSummary.has_errors": {"qualname": 3, "fullname": 4, "annotation": 2, "default_value": 0, "signature": 0, "bases": 0, "doc": 24}, "pyoutlineapi.TimestampMs": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 20, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.TimestampSec": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 20, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.TunnelTime": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 7, "doc": 18}, "pyoutlineapi.TunnelTime.seconds": {"qualname": 2, "fullname": 3, "annotation": 2, "default_value": 1, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ValidationError": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 3, "doc": 128}, "pyoutlineapi.ValidationError.__init__": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 65, "bases": 0, "doc": 37}, "pyoutlineapi.ValidationError.field": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.ValidationError.model": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.Validators": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 9}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 39, "bases": 0, "doc": 54}, "pyoutlineapi.Validators.validate_port": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 19, "bases": 0, "doc": 49}, "pyoutlineapi.Validators.validate_name": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 19, "bases": 0, "doc": 48}, "pyoutlineapi.Validators.validate_url": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 65, "bases": 0, "doc": 72}, "pyoutlineapi.Validators.validate_string_not_empty": {"qualname": 5, "fullname": 6, "annotation": 0, "default_value": 0, "signature": 30, "bases": 0, "doc": 60}, "pyoutlineapi.Validators.validate_non_negative": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 45, "bases": 0, "doc": 59}, "pyoutlineapi.Validators.validate_since": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 19, "bases": 0, "doc": 76}, "pyoutlineapi.Validators.validate_key_id": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 25, "bases": 0, "doc": 53}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"qualname": 5, "fullname": 6, "annotation": 0, "default_value": 0, "signature": 19, "bases": 0, "doc": 37}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"qualname": 5, "fullname": 6, "annotation": 0, "default_value": 0, "signature": 19, "bases": 0, "doc": 34}, "pyoutlineapi.audited": {"qualname": 1, "fullname": 2, "annotation": 0, "default_value": 0, "signature": 93, "bases": 0, "doc": 184}, "pyoutlineapi.build_config_overrides": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 82, "bases": 0, "doc": 133}, "pyoutlineapi.correlation_id": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 13, "signature": 0, "bases": 0, "doc": 3}, "pyoutlineapi.create_client": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 149, "bases": 0, "doc": 158}, "pyoutlineapi.create_env_template": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 48, "bases": 0, "doc": 35}, "pyoutlineapi.create_multi_server_manager": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 142, "bases": 0, "doc": 280}, "pyoutlineapi.format_error_chain": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 40, "bases": 0, "doc": 173}, "pyoutlineapi.get_audit_logger": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 26, "bases": 0, "doc": 23}, "pyoutlineapi.get_or_create_audit_logger": {"qualname": 5, "fullname": 6, "annotation": 0, "default_value": 0, "signature": 43, "bases": 0, "doc": 43}, "pyoutlineapi.get_retry_delay": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 25, "bases": 0, "doc": 113}, "pyoutlineapi.get_safe_error_dict": {"qualname": 4, "fullname": 5, "annotation": 0, "default_value": 0, "signature": 36, "bases": 0, "doc": 153}, "pyoutlineapi.get_version": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 10, "bases": 0, "doc": 18}, "pyoutlineapi.is_json_serializable": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 311, "bases": 0, "doc": 39}, "pyoutlineapi.is_retryable": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 19, "bases": 0, "doc": 119}, "pyoutlineapi.is_valid_bytes": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 25, "bases": 0, "doc": 39}, "pyoutlineapi.is_valid_port": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 25, "bases": 0, "doc": 39}, "pyoutlineapi.load_config": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 72, "bases": 0, "doc": 122}, "pyoutlineapi.mask_sensitive_data": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 110, "bases": 0, "doc": 92}, "pyoutlineapi.print_type_info": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 10, "bases": 0, "doc": 12}, "pyoutlineapi.quick_setup": {"qualname": 2, "fullname": 3, "annotation": 0, "default_value": 0, "signature": 10, "bases": 0, "doc": 25}, "pyoutlineapi.set_audit_logger": {"qualname": 3, "fullname": 4, "annotation": 0, "default_value": 0, "signature": 30, "bases": 0, "doc": 44}}, "length": 359, "save": true}, "index": {"qualname": {"root": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 15, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}}, "df": 14, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}}, "df": 5}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}}, "df": 3}}}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 4}}}, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}}, "df": 1}}}, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}}, "df": 1}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.APIError.response_data": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 10, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}}, "df": 5, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}}, "df": 3}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKey.display_name": {"tf": 1}}, "df": 1}}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 2}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 3}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 2}}}}}}}, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.server_id": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 19, "s": {"docs": {"pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}}, "df": 2, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 10}}}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}}, "df": 5}}}}}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 7}}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.is_json_serializable": {"tf": 1}}, "df": 1}}}}}}}}}}, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ConfigurationError.security_issue": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 2}}}, "e": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.SecureIDGenerator": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 3}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}}, "df": 2}}}}}, "t": {"docs": {"pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 1, "u": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.quick_setup": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.APIError.status_code": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 4}}, "e": {"docs": {"pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 3, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 2}}}}}}}, "f": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 2}}}, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 2}}}}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.success": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}}, "df": 7, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}}, "df": 1}}}}}}}}}, "h": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}}, "df": 3}}}}}}, "a": {"2": {"5": {"6": {"docs": {"pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "s": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 2}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}}, "df": 1}}}}}}}, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}}, "df": 3}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}}}}, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 5, "s": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}}, "df": 6}}}, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.APIError.status_code": {"tf": 1}, "pyoutlineapi.APIError.endpoint": {"tf": 1}, "pyoutlineapi.APIError.response_data": {"tf": 1}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}}, "df": 9}}}}}}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}}, "df": 8, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}}, "df": 12, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}}, "df": 6}}}}}}}}}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 8}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}}, "df": 5}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.action": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 7}}, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {"pyoutlineapi.LocationMetric.as_org": {"tf": 1}}, "df": 1, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}}, "df": 10}}}}}}}}}}}}}}}}, "n": {"docs": {"pyoutlineapi.LocationMetric.asn": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.action": {"tf": 1}, "pyoutlineapi.AuditContext.resource": {"tf": 1}, "pyoutlineapi.AuditContext.success": {"tf": 1}, "pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 8}}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 1}}, "df": 5}}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}}, "df": 3}}, "l": {"docs": {"pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2, "o": {"docs": {}, "df": 0, "w": {"docs": {"pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}}, "df": 2}}}}, "f": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}}, "df": 3}}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}}, "df": 4}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 15}}, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}}, "df": 2}}, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}}, "df": 2}}}}}}}}, "s": {"docs": {"pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 11, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigurationError.security_issue": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {"pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.Server.server_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.correlation_id": {"tf": 1}}, "df": 12}, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 1}}}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.status_code": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}}, "df": 4}}}, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}}, "df": 5}}}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 7, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}}, "df": 15}}}}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.field": {"tf": 1}, "pyoutlineapi.ConfigurationError.security_issue": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Constants": {"tf": 1}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}}, "df": 31}}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.correlation_id": {"tf": 1}}, "df": 3}}}}}}}}}, "p": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState.CLOSED": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 6, "d": {"docs": {"pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}}, "df": 2}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CredentialSanitizer": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}}, "df": 3}}}}}}}}}}}}}}}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}}, "df": 2}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}}, "df": 4, "s": {"docs": {"pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}}, "df": 3}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.BandwidthInfo.current": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}}, "df": 13, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}}, "df": 6}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}}, "df": 11}}}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}}, "df": 4}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.CircuitState.CLOSED": {"tf": 1}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 4}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 4}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.endpoint": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 2}}}}}}, "v": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 3}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}}, "df": 8, "d": {"docs": {"pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}}, "df": 3}}}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 1}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 9, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}, "pyoutlineapi.ErrorResponse.message": {"tf": 1}}, "df": 3}}}}}}}}, "s": {"docs": {"pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 2}}}}, "x": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}}, "df": 1, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.response_data": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 5, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.ResponseData": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 6}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext.resource": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 2}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 9, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 3}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}}, "df": 3}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}}, "df": 1}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}}, "df": 9}, "i": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}}, "df": 12, "s": {"docs": {"pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 2}}}}}, "o": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 7, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 6}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3}}}}, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.LocationMetric.location": {"tf": 1}}, "df": 1, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.LocationMetric.location": {"tf": 1}, "pyoutlineapi.LocationMetric.asn": {"tf": 1}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}}, "df": 6}}}}}}, "s": {"docs": {"pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.load_config": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}}, "df": 2}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}}, "df": 4}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}}, "df": 4}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}}, "df": 11, "s": {"docs": {"pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}}, "df": 2}}}}}}, "w": {"docs": {"pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 1}, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}}, "df": 4}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 4}}}}}}}}}, "t": {"docs": {"pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 1}, "n": {"docs": {"pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 1}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}}, "df": 2}}}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1}}}}}}, "y": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 2}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.port": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 9, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}}, "df": 2}}}}}}}}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.BandwidthInfo.peak": {"tf": 1}}, "df": 1, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}}, "df": 3}}}}}}}}}}}}}, "r": {"docs": {"pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}}, "df": 1}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}}, "df": 2}}}}, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.print_type_info": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}}, "df": 2}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}}, "df": 5, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MetricsCollector": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}}, "df": 5}}}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.MetricsTags": {"tf": 1}}, "df": 1}}}}}}}}}, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ErrorResponse.message": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 2}}}}}}, "a": {"docs": {}, "df": 0, "x": {"docs": {"pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}}, "df": 14}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}}, "df": 3, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}}, "df": 1}}}}}}, "b": {"docs": {"pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}}, "df": 1}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 10}}}}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.ValidationError.model": {"tf": 1}}, "df": 2}}}}, "s": {"docs": {"pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}}, "df": 6}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}}, "df": 7, "s": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 1}}}}}, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 2, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 16}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.field": {"tf": 1}, "pyoutlineapi.ValidationError.model": {"tf": 1}}, "df": 4}}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 11}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.get_version": {"tf": 1}}, "df": 2}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 3}, "l": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}}, "df": 6}}}}}}}}}}}, "y": {"docs": {"pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}}, "df": 3}}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.host": {"tf": 1}}, "df": 2, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.HostnameRequest.hostname": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}}, "df": 2, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.HostnameRequest.hostname": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.get_version": {"tf": 1}}, "df": 18}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 2}}}}}}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 3}}}}}}}}, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 2}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Server.has_global_limit": {"tf": 1}}, "df": 1}}}}}}, "b": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}}, "df": 3, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}}, "df": 8}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}}, "df": 1, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}}, "df": 3, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}}, "df": 2}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}}, "df": 3}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}}, "df": 4}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 2}}}}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ConfigurationError.field": {"tf": 1}, "pyoutlineapi.ValidationError.field": {"tf": 1}}, "df": 2}}}, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 2}}}}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 6, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 4}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}}, "df": 6}}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}}, "df": 5}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}}, "df": 2}}}}}}, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}}, "df": 1, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}}, "df": 2}}}}}}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}}, "df": 3, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}}, "df": 2}}}}}}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}}, "df": 5, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}}, "df": 5, "m": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}}, "df": 1}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.timeout": {"tf": 1}}, "df": 15}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}}, "df": 2}}}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}}, "df": 1, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 6}}}}}}}}}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}}, "df": 6}}}}}}}}, "o": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}}, "df": 2, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 5}}}, "p": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 1}}, "t": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.create_env_template": {"tf": 1}}, "df": 1}}}}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.print_type_info": {"tf": 1}}, "df": 1}}}}, "j": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}}, "df": 4, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.JsonPayload": {"tf": 1}}, "df": 1}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 2}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineTimeoutError.operation": {"tf": 1}}, "df": 1}}}}}}}}, "r": {"docs": {"pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 1, "g": {"docs": {"pyoutlineapi.LocationMetric.as_org": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineClientConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}}, "df": 26}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.host": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.port": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}}, "df": 6}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.timeout": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.operation": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.QueryParams": {"tf": 1}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.quick_setup": {"tf": 1}}, "df": 1}}}}}}}, "fullname": {"root": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 15, "p": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.APIError.status_code": {"tf": 1}, "pyoutlineapi.APIError.endpoint": {"tf": 1}, "pyoutlineapi.APIError.response_data": {"tf": 1}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.action": {"tf": 1}, "pyoutlineapi.AuditContext.resource": {"tf": 1}, "pyoutlineapi.AuditContext.success": {"tf": 1}, "pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.CircuitState.CLOSED": {"tf": 1}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}, "pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.field": {"tf": 1}, "pyoutlineapi.ConfigurationError.security_issue": {"tf": 1}, "pyoutlineapi.Constants": {"tf": 1}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.CredentialSanitizer": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}, "pyoutlineapi.ErrorResponse.message": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.HostnameRequest.hostname": {"tf": 1}, "pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.LocationMetric.location": {"tf": 1}, "pyoutlineapi.LocationMetric.asn": {"tf": 1}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.MetricsCollector": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsTags": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.OutlineClientConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.host": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.port": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.timeout": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.operation": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.QueryParams": {"tf": 1}, "pyoutlineapi.ResponseData": {"tf": 1}, "pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.SecureIDGenerator": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.server_id": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}, "pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.field": {"tf": 1}, "pyoutlineapi.ValidationError.model": {"tf": 1}, "pyoutlineapi.Validators": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.correlation_id": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.get_version": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 359}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}}, "df": 2}}}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1}}}}}}, "y": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 2}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.port": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 9, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}}, "df": 2}}}}}}}}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.BandwidthInfo.peak": {"tf": 1}}, "df": 1, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}}, "df": 3}}}}}}}}}}}}}, "r": {"docs": {"pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}}, "df": 1}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}}, "df": 2}}}}, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.print_type_info": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}}, "df": 14, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}}, "df": 5}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}}, "df": 3}}}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 4}}}, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}}, "df": 1}}}, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}}, "df": 1}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.APIError.response_data": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 10, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}}, "df": 5, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}}, "df": 3}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKey.display_name": {"tf": 1}}, "df": 1}}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 2}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 3}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 2}}}}}}}, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.server_id": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 19, "s": {"docs": {"pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}}, "df": 2, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 10}}}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}}, "df": 5}}}}}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 7}}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.is_json_serializable": {"tf": 1}}, "df": 1}}}}}}}}}}, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ConfigurationError.security_issue": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 2}}}, "e": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.SecureIDGenerator": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 3}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}}, "df": 2}}}}}, "t": {"docs": {"pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 1, "u": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.quick_setup": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.APIError.status_code": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 4}}, "e": {"docs": {"pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 3, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 2}}}}}}}, "f": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 2}}}, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 2}}}}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.success": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}}, "df": 7, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}}, "df": 1}}}}}}}}}, "h": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}}, "df": 3}}}}}}, "a": {"2": {"5": {"6": {"docs": {"pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "s": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 2}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}}, "df": 1}}}}}}}, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}}, "df": 3}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}}}}, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 5, "s": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}}, "df": 6}}}, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.APIError.status_code": {"tf": 1}, "pyoutlineapi.APIError.endpoint": {"tf": 1}, "pyoutlineapi.APIError.response_data": {"tf": 1}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}}, "df": 9}}}}}}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}}, "df": 8, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}}, "df": 12, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}}, "df": 6}}}}}}}}}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 8}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}}, "df": 5}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.action": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 7}}, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {"pyoutlineapi.LocationMetric.as_org": {"tf": 1}}, "df": 1, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}}, "df": 10}}}}}}}}}}}}}}}}, "n": {"docs": {"pyoutlineapi.LocationMetric.asn": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.action": {"tf": 1}, "pyoutlineapi.AuditContext.resource": {"tf": 1}, "pyoutlineapi.AuditContext.success": {"tf": 1}, "pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 8}}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 1}}, "df": 5}}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}}, "df": 3}}, "l": {"docs": {"pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2, "o": {"docs": {}, "df": 0, "w": {"docs": {"pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}}, "df": 2}}}}, "f": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}}, "df": 3}}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}}, "df": 4}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 15}}, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}}, "df": 2}}, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}}, "df": 2}}}}}}}}, "s": {"docs": {"pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 11, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigurationError.security_issue": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {"pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.Server.server_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.correlation_id": {"tf": 1}}, "df": 12}, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 1}}}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.status_code": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}}, "df": 4}}}, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}}, "df": 5}}}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 7, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}}, "df": 15}}}}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.field": {"tf": 1}, "pyoutlineapi.ConfigurationError.security_issue": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Constants": {"tf": 1}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}}, "df": 31}}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.correlation_id": {"tf": 1}}, "df": 3}}}}}}}}}, "p": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState.CLOSED": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 6, "d": {"docs": {"pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}}, "df": 2}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CredentialSanitizer": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}}, "df": 3}}}}}}}}}}}}}}}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}}, "df": 2}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}}, "df": 4, "s": {"docs": {"pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}}, "df": 3}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.BandwidthInfo.current": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}}, "df": 13, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}}, "df": 6}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}}, "df": 11}}}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}}, "df": 4}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.CircuitState.CLOSED": {"tf": 1}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 4}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 4}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.endpoint": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 2}}}}}}, "v": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 3}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}}, "df": 8, "d": {"docs": {"pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}}, "df": 3}}}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 1}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 9, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}, "pyoutlineapi.ErrorResponse.message": {"tf": 1}}, "df": 3}}}}}}}}, "s": {"docs": {"pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 2}}}}, "x": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}}, "df": 1, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.response_data": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 5, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.ResponseData": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 6}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext.resource": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 2}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 9, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 3}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}}, "df": 3}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}}, "df": 1}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}}, "df": 9}, "i": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}}, "df": 12, "s": {"docs": {"pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 2}}}}}, "o": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 7, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 6}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3}}}}, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.LocationMetric.location": {"tf": 1}}, "df": 1, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.LocationMetric.location": {"tf": 1}, "pyoutlineapi.LocationMetric.asn": {"tf": 1}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}}, "df": 6}}}}}}, "s": {"docs": {"pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.load_config": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}}, "df": 2}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}}, "df": 4}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}}, "df": 4}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}}, "df": 11, "s": {"docs": {"pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}}, "df": 2}}}}}}, "w": {"docs": {"pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 1}, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}}, "df": 4}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 4}}}}}}}}}, "t": {"docs": {"pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 1}, "n": {"docs": {"pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 1}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}}, "df": 2}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}}, "df": 5, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MetricsCollector": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}}, "df": 5}}}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.MetricsTags": {"tf": 1}}, "df": 1}}}}}}}}}, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ErrorResponse.message": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 2}}}}}}, "a": {"docs": {}, "df": 0, "x": {"docs": {"pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}}, "df": 14}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}}, "df": 3, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}}, "df": 1}}}}}}, "b": {"docs": {"pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}}, "df": 1}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 10}}}}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.ValidationError.model": {"tf": 1}}, "df": 2}}}}, "s": {"docs": {"pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}}, "df": 6}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}}, "df": 7, "s": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 1}}}}}, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 2, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 16}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.field": {"tf": 1}, "pyoutlineapi.ValidationError.model": {"tf": 1}}, "df": 4}}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 11}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.get_version": {"tf": 1}}, "df": 2}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 3}, "l": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}}, "df": 6}}}}}}}}}}}, "y": {"docs": {"pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}}, "df": 3}}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.host": {"tf": 1}}, "df": 2, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.HostnameRequest.hostname": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}}, "df": 2, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.HostnameRequest.hostname": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.get_version": {"tf": 1}}, "df": 18}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 2}}}}}}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 3}}}}}}}}, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 2}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Server.has_global_limit": {"tf": 1}}, "df": 1}}}}}}, "b": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}}, "df": 3, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}}, "df": 8}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}}, "df": 1, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}}, "df": 3, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}}, "df": 2}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}}, "df": 3}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}}, "df": 4}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 2}}}}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ConfigurationError.field": {"tf": 1}, "pyoutlineapi.ValidationError.field": {"tf": 1}}, "df": 2}}}, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 2}}}}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 6, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 4}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}}, "df": 6}}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}}, "df": 5}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}}, "df": 2}}}}}}, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}}, "df": 1, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}}, "df": 2}}}}}}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}}, "df": 3, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}}, "df": 2}}}}}}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}}, "df": 5, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}}, "df": 5, "m": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}}, "df": 1}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.timeout": {"tf": 1}}, "df": 15}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}}, "df": 2}}}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}}, "df": 1, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 6}}}}}}}}}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}}, "df": 6}}}}}}}}, "o": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}}, "df": 2, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 5}}}, "p": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 1}}, "t": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.create_env_template": {"tf": 1}}, "df": 1}}}}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.print_type_info": {"tf": 1}}, "df": 1}}}}, "j": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}}, "df": 4, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.JsonPayload": {"tf": 1}}, "df": 1}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 2}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineTimeoutError.operation": {"tf": 1}}, "df": 1}}}}}}}}, "r": {"docs": {"pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 1, "g": {"docs": {"pyoutlineapi.LocationMetric.as_org": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineClientConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}}, "df": 26}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.host": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.port": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}}, "df": 6}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.timeout": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.operation": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.QueryParams": {"tf": 1}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.quick_setup": {"tf": 1}}, "df": 1}}}}}}}, "annotation": {"root": {"0": {"docs": {"pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 5}, "1": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.PortRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1.4142135623730951}}, "df": 4}, "6": {"5": {"5": {"3": {"5": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.PortRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1.4142135623730951}}, "df": 4}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKey.port": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AuditContext.action": {"tf": 1}, "pyoutlineapi.AuditContext.resource": {"tf": 1}, "pyoutlineapi.AuditContext.success": {"tf": 1}, "pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1.4142135623730951}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1.7320508075688772}, "pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}, "pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}, "pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1.7320508075688772}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1.7320508075688772}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}, "pyoutlineapi.ErrorResponse.message": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.HostnameRequest.hostname": {"tf": 1}, "pyoutlineapi.LocationMetric.location": {"tf": 1}, "pyoutlineapi.LocationMetric.asn": {"tf": 1.4142135623730951}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1.4142135623730951}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1.7320508075688772}, "pyoutlineapi.PortRequest.port": {"tf": 1.7320508075688772}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.server_id": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.version": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary.error": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}}, "df": 178, "b": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AuditContext.success": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 31}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}}, "df": 2, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.BandwidthData.data": {"tf": 1}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}}, "df": 2}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}, "pyoutlineapi.AuditContext.action": {"tf": 1}, "pyoutlineapi.AuditContext.resource": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}, "pyoutlineapi.ErrorResponse.message": {"tf": 1}, "pyoutlineapi.HostnameRequest.hostname": {"tf": 1}, "pyoutlineapi.LocationMetric.location": {"tf": 1}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.server_id": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}}, "df": 30}}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}}, "df": 2}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}}, "df": 2}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1}, "pyoutlineapi.LocationMetric.asn": {"tf": 1}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}}, "df": 17, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 9}}}}}}}, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 4}}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 7}}}}}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 9}}}}}}}}, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}}, "df": 7}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}}, "df": 1, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 9}}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}}, "df": 25}}}, "f": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}}, "df": 3}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 18}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 9}}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 9}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}}, "df": 3}}}}}}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}}, "df": 7}}}, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 3}}}}}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 9}}}}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}}, "df": 4}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}}, "df": 3}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AuditContext.details": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}}, "df": 10}}}}}}}}, "x": {"2": {"7": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimit.bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1.4142135623730951}, "pyoutlineapi.PortRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1.4142135623730951}}, "df": 9}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 4}}}, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}}, "df": 18}}}}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 9}}}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}}, "df": 19}}}}}, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimit.bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1.4142135623730951}, "pyoutlineapi.PortRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1.4142135623730951}}, "df": 9}}, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.PortRequest.port": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1.4142135623730951}}, "df": 4}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}}, "df": 3}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 5, "t": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}, "pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1}, "pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1}, "pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1}, "pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}, "pyoutlineapi.LocationMetric.asn": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}}, "df": 33}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}}, "df": 1}}}}}}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}}, "df": 1}}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}}, "df": 1, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {"pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 3}}}}}}, "default_value": {"root": {"0": {"docs": {"pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 9}, "1": {"0": {"0": {"docs": {"pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}}, "df": 4}, "4": {"8": {"5": {"7": {"6": {"0": {"docs": {"pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}}, "df": 7}, "docs": {"pyoutlineapi.CircuitState.CLOSED": {"tf": 1}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1}}, "df": 5}, "2": {"0": {"0": {"docs": {"pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1}}, "df": 1}, "4": {"8": {"docs": {"pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1}}, "df": 1}, "5": {"5": {"docs": {"pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1}}, "df": 2}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}}, "df": 4}, "3": {"0": {"0": {"docs": {"pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1}}, "df": 3}, "docs": {"pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1}}, "df": 2}, "docs": {"pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 1}, "4": {"0": {"8": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}, "docs": {"pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1}}, "df": 1}, "2": {"9": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}}, "df": 2}, "5": {"0": {"0": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}, "2": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}, "3": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}, "4": {"docs": {"pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 1}, "docs": {"pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1}}, "df": 1}, "docs": {"pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}}, "df": 1}, "6": {"0": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}}, "df": 1}, "4": {"docs": {"pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1}}, "df": 1}, "5": {"5": {"3": {"5": {"docs": {"pyoutlineapi.Constants.MAX_PORT": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "8": {"1": {"9": {"2": {"docs": {"pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1}}, "df": 1}, "9": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1, "]": {"docs": {}, "df": 0, "{": {"2": {"0": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.4142135623730951}}, "df": 1}, "docs": {}, "df": 0}, "6": {"4": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}}, "docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitState.CLOSED": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1.4142135623730951}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1.4142135623730951}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 5}, "pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1.4142135623730951}, "pyoutlineapi.QueryParams": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseData": {"tf": 1}, "pyoutlineapi.TimestampMs": {"tf": 1.4142135623730951}, "pyoutlineapi.TimestampSec": {"tf": 1.4142135623730951}, "pyoutlineapi.correlation_id": {"tf": 1.4142135623730951}}, "df": 15, "f": {"0": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1}}, "df": 2}}}}}}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1}}, "df": 5}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1.4142135623730951}, "pyoutlineapi.QueryParams": {"tf": 1}, "pyoutlineapi.ResponseData": {"tf": 1}}, "df": 4}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1.4142135623730951}, "pyoutlineapi.JsonPayload": {"tf": 2}, "pyoutlineapi.ResponseData": {"tf": 1.4142135623730951}}, "df": 3}}}}}}}}}, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}}}}}}, "x": {"2": {"7": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 5.0990195135927845}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1.4142135623730951}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 6}, "pyoutlineapi.JsonDict": {"tf": 2}, "pyoutlineapi.JsonPayload": {"tf": 2.8284271247461903}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseData": {"tf": 2}, "pyoutlineapi.TimestampMs": {"tf": 1.4142135623730951}, "pyoutlineapi.TimestampSec": {"tf": 1.4142135623730951}, "pyoutlineapi.correlation_id": {"tf": 2}}, "df": 10}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "s": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 3.4641016151377544}}, "df": 1, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 1}}}}}}, "h": {"docs": {}, "df": 0, "a": {"2": {"5": {"6": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 2}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MetricsTags": {"tf": 1}, "pyoutlineapi.QueryParams": {"tf": 1}}, "df": 2}}}, "a": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 2}}, "df": 1, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.4142135623730951}}, "df": 2, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}}, "df": 1}}}}}}}}, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1.4142135623730951}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.4142135623730951}}, "df": 2, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1.4142135623730951}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}}}}}}}, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.4142135623730951}}, "df": 2, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"2": {"5": {"6": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitState.CLOSED": {"tf": 1}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 3}}}}}}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState.CLOSED": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 2.449489742783178}}, "df": 1}}}}}, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.correlation_id": {"tf": 1}}, "df": 1}}}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.correlation_id": {"tf": 1}}, "df": 1}}}}}}}}}}}, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.4142135623730951}}, "df": 2}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1.4142135623730951}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.4142135623730951}}, "df": 2}}, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseData": {"tf": 1}}, "df": 3}}}}}}, "x": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.7320508075688772}}, "df": 2}}}}, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 6}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1}, "pyoutlineapi.ResponseData": {"tf": 1}, "pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 5}}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}}}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.4142135623730951}}, "df": 2}}}}}}}, "y": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey.id": {"tf": 1}, "pyoutlineapi.AccessKey.password": {"tf": 1}, "pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKey.method": {"tf": 1}, "pyoutlineapi.AccessKey.access_url": {"tf": 1}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1}, "pyoutlineapi.BandwidthData.data": {"tf": 1}, "pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.ErrorResponse.code": {"tf": 1}, "pyoutlineapi.ErrorResponse.message": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1}, "pyoutlineapi.HealthCheckResult.healthy": {"tf": 1}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1}, "pyoutlineapi.HostnameRequest.hostname": {"tf": 1}, "pyoutlineapi.LocationMetric.location": {"tf": 1}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.server_id": {"tf": 1}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1}, "pyoutlineapi.ServerSummary.server": {"tf": 1}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1}}, "df": 50}}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "/": {"0": {"docs": {"pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}}, "df": 2}, "docs": {}, "df": 0}}}}}}}}}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.name": {"tf": 1}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1}, "pyoutlineapi.LocationMetric.asn": {"tf": 1}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1}, "pyoutlineapi.Server.name": {"tf": 1}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1}, "pyoutlineapi.Server.version": {"tf": 1}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1}, "pyoutlineapi.ServerSummary.error": {"tf": 1}}, "df": 18, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseData": {"tf": 1}, "pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 5}}}}}}}, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.correlation_id": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitState.CLOSED": {"tf": 1}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}, "pyoutlineapi.correlation_id": {"tf": 1}}, "df": 4}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.JsonPayload": {"tf": 1}}, "df": 1}}}}}}}}}}}, "g": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitState.CLOSED": {"tf": 1}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}, "pyoutlineapi.correlation_id": {"tf": 1}}, "df": 4}, "e": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1.4142135623730951}, "pyoutlineapi.TimestampSec": {"tf": 1.4142135623730951}}, "df": 2}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitState.OPEN": {"tf": 1}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 2}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 3.4641016151377544}}, "df": 1, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 3.4641016151377544}}, "df": 1}}}}, "z": {"0": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.7320508075688772}}, "df": 1}, "docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.7320508075688772}}, "df": 1}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 2.449489742783178}}, "df": 1}}}}}}}}}, "n": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2, "t": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1.4142135623730951}, "pyoutlineapi.QueryParams": {"tf": 1}, "pyoutlineapi.ResponseData": {"tf": 1}}, "df": 4}}, "d": {"docs": {"pyoutlineapi.correlation_id": {"tf": 1}}, "df": 1}}, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1, "\\": {"docs": {}, "df": 0, "\\": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1.4142135623730951}, "pyoutlineapi.QueryParams": {"tf": 1}, "pyoutlineapi.ResponseData": {"tf": 1}}, "df": 4}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "[": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1}, "pyoutlineapi.MetricsTags": {"tf": 1}, "pyoutlineapi.QueryParams": {"tf": 1}, "pyoutlineapi.ResponseData": {"tf": 1}}, "df": 5}}}}}}}, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}}}}}}, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.correlation_id": {"tf": 1}}, "df": 1}}}}}}}, "j": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseData": {"tf": 1}}, "df": 3}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.JsonDict": {"tf": 1}, "pyoutlineapi.JsonPayload": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseData": {"tf": 1}}, "df": 3}}}}}}}}, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}}, "df": 1}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.TimestampMs": {"tf": 1}, "pyoutlineapi.TimestampSec": {"tf": 1}}, "df": 2}}}}}}}}}}, "signature": {"root": {"0": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 2.8284271247461903}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 8}, "1": {"0": {"0": {"0": {"0": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 1}, "docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 2}, "docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 1}, "2": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1}}, "df": 1}, "3": {"9": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 4.898979485566356}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 3.4641016151377544}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 3.4641016151377544}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 3.4641016151377544}, "pyoutlineapi.create_env_template": {"tf": 1.4142135623730951}, "pyoutlineapi.is_json_serializable": {"tf": 3.4641016151377544}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}}, "df": 7}, "docs": {}, "df": 0}, "5": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 4}, "6": {"0": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}}, "df": 2}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.APIError.__init__": {"tf": 9.643650760992955}, "pyoutlineapi.AccessKey.validate_name": {"tf": 5.477225575051661}, "pyoutlineapi.AccessKey.validate_id": {"tf": 4.47213595499958}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 5.744562646538029}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 5.744562646538029}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 5}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 5}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 12.569805089976535}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 13.601470508735444}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 11.874342087037917}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 5.0990195135927845}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 5.0990195135927845}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 5.0990195135927845}, "pyoutlineapi.AuditContext.__init__": {"tf": 9.273618495495704}, "pyoutlineapi.AuditContext.from_call": {"tf": 11.74734012447073}, "pyoutlineapi.AuditLogger.__init__": {"tf": 4}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 10.535653752852738}, "pyoutlineapi.AuditLogger.log_action": {"tf": 10.535653752852738}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 3.4641016151377544}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 8}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 9.695359714832659}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 5.196152422706632}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 5.5677643628300215}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 7.3484692283495345}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 4.47213595499958}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 4.47213595499958}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 4.47213595499958}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 4.47213595499958}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 4.69041575982343}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 7.416198487095663}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 10.535653752852738}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 10.535653752852738}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 5.5677643628300215}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 5.744562646538029}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 4}, "pyoutlineapi.MetricsCollector.increment": {"tf": 7.3484692283495345}, "pyoutlineapi.MetricsCollector.timing": {"tf": 8.18535277187245}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 8.18535277187245}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 10.14889156509222}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 4.123105625617661}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 5.916079783099616}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 5}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 7.280109889280518}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 6.782329983125268}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 5.0990195135927845}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 10.535653752852738}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 10.535653752852738}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 3.4641016151377544}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 7.3484692283495345}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 8.18535277187245}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 8.18535277187245}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 4.47213595499958}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 6}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 4.47213595499958}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 3.4641016151377544}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 6.855654600401044}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 9}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 8.831760866327848}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 7.416198487095663}, "pyoutlineapi.OutlineError.__init__": {"tf": 9.327379053088816}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 7.681145747868608}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 3.4641016151377544}, "pyoutlineapi.ResponseParser.parse": {"tf": 23.57965224510319}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 16.673332000533065}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 17.406895185529212}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 16.822603841260722}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 5.5677643628300215}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 3}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 3}, "pyoutlineapi.Server.validate_name": {"tf": 4.47213595499958}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 4.47213595499958}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 6.244997998398398}, "pyoutlineapi.ValidationError.__init__": {"tf": 7.416198487095663}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 5.656854249492381}, "pyoutlineapi.Validators.validate_port": {"tf": 4}, "pyoutlineapi.Validators.validate_name": {"tf": 4}, "pyoutlineapi.Validators.validate_url": {"tf": 7.280109889280518}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 4.898979485566356}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 6.082762530298219}, "pyoutlineapi.Validators.validate_since": {"tf": 4}, "pyoutlineapi.Validators.validate_key_id": {"tf": 4.47213595499958}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 4}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 4}, "pyoutlineapi.audited": {"tf": 8.831760866327848}, "pyoutlineapi.build_config_overrides": {"tf": 8.306623862918075}, "pyoutlineapi.create_client": {"tf": 10.954451150103322}, "pyoutlineapi.create_env_template": {"tf": 6.164414002968976}, "pyoutlineapi.create_multi_server_manager": {"tf": 10.723805294763608}, "pyoutlineapi.format_error_chain": {"tf": 5.744562646538029}, "pyoutlineapi.get_audit_logger": {"tf": 4.69041575982343}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 5.916079783099616}, "pyoutlineapi.get_retry_delay": {"tf": 4.58257569495584}, "pyoutlineapi.get_safe_error_dict": {"tf": 5.477225575051661}, "pyoutlineapi.get_version": {"tf": 3}, "pyoutlineapi.is_json_serializable": {"tf": 15.748015748023622}, "pyoutlineapi.is_retryable": {"tf": 4}, "pyoutlineapi.is_valid_bytes": {"tf": 4.58257569495584}, "pyoutlineapi.is_valid_port": {"tf": 4.58257569495584}, "pyoutlineapi.load_config": {"tf": 7.681145747868608}, "pyoutlineapi.mask_sensitive_data": {"tf": 9.591663046625438}, "pyoutlineapi.print_type_info": {"tf": 3}, "pyoutlineapi.quick_setup": {"tf": 3}, "pyoutlineapi.set_audit_logger": {"tf": 4.898979485566356}}, "df": 103, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 7}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 6, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 6, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 6}}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 6}}}}}, "b": {"docs": {"pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}}, "df": 1}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 5}}}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 2}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 2.23606797749979}, "pyoutlineapi.AuditLogger.log_action": {"tf": 2.23606797749979}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 2.23606797749979}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 2.23606797749979}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 2.23606797749979}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 2.23606797749979}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 4}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.8284271247461903}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 3}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 3}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.Validators.validate_name": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_url": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1.7320508075688772}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1.4142135623730951}, "pyoutlineapi.build_config_overrides": {"tf": 1.7320508075688772}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.get_version": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 2.6457513110645907}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}, "pyoutlineapi.mask_sensitive_data": {"tf": 1.7320508075688772}}, "df": 71}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}}, "df": 1}}, "e": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 39}}, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ConfigurationError.__init__": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1.4142135623730951}}, "df": 3}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 3}}}}}}, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager.get_client": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"2": {"5": {"6": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 4, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1.4142135623730951}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineError.__init__": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}}, "df": 1}}, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 8, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}}}}}}}}, "s": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 2}}}}}, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 7}}}}}}}}}}, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 16}, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 9}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1}}, "df": 1, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.7320508075688772}}, "df": 2}}}}, "s": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1.7320508075688772}}, "df": 1}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}}, "df": 1}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.load_config": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 2}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 3.1622776601683795}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.23606797749979}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_port": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1.4142135623730951}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 2.23606797749979}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 26}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3}}}}}}}, "d": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 12, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager.get_client": {"tf": 1}}, "df": 1}}}}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigurationError.__init__": {"tf": 1}}, "df": 1}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 2.449489742783178}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 3.1622776601683795}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 3.3166247903554}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 2.449489742783178}, "pyoutlineapi.AuditContext.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 2.6457513110645907}, "pyoutlineapi.AuditLogger.log_action": {"tf": 2.6457513110645907}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 2.6457513110645907}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 2.6457513110645907}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 2}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 2.6457513110645907}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 2.6457513110645907}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 2}, "pyoutlineapi.OutlineError.__init__": {"tf": 2}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 2}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 2}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 2}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 2}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1.4142135623730951}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1.4142135623730951}, "pyoutlineapi.print_type_info": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 46, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 3.1622776601683795}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.23606797749979}, "pyoutlineapi.is_json_serializable": {"tf": 2.23606797749979}}, "df": 5}}}}}}}, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 4}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 2}}}}}}, "v": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 3, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.load_config": {"tf": 1}}, "df": 1}}}}}}}}}}, "x": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 4}}}}}}}, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.create_env_template": {"tf": 1}}, "df": 1}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 4}}}}}, "r": {"docs": {"pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 1, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 7}}}}, "l": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}}, "df": 1}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 7, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 1}}}}}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.4142135623730951}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1.4142135623730951}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 32}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.4142135623730951}}, "df": 8}}}}}, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 2}}}}}, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}, "t": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}}, "df": 1, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.23606797749979}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1.4142135623730951}}, "df": 22}}}, "e": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 7}, "g": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 3}}}}}}}}, "u": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 2}}}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1.4142135623730951}}, "df": 1, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 8}}}}}}, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 6}}}, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 2}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1.4142135623730951}}, "df": 18}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 4, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 7}}}}}, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 10, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 9}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}}}}}}}}}}}, "s": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 1, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}}, "df": 1}}}}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 6}}}}}}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}}, "df": 3}}}, "f": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}}}, "v": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}}, "df": 6, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 10}}}}}, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 4}}}}}, "b": {"docs": {"pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}}, "df": 1}}, "p": {"docs": {"pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 1, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 2}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 2.23606797749979}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 2}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 2}, "pyoutlineapi.create_multi_server_manager": {"tf": 2}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 23}}}}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1.4142135623730951}}, "df": 3}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1.4142135623730951}}, "df": 3, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 3}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}}, "df": 2}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 2}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1.4142135623730951}}, "df": 13}}, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 1, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 7}}}}, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 3}}}}, "t": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}}, "df": 1}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1.4142135623730951}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 8}}}}}}}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 8}}}}}}}}, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.4142135623730951}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 9}}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}}, "df": 1}}}}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 6}}, "n": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 3.3166247903554}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.23606797749979}, "pyoutlineapi.is_json_serializable": {"tf": 2.23606797749979}}, "df": 5}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}}, "df": 7}}}}, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 6, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 1}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1.4142135623730951}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 3.3166247903554}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.is_json_serializable": {"tf": 2.23606797749979}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 16}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}}, "df": 2}}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 1}}}}}}}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}}, "df": 1}}}}}, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 3}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitMetrics.__init__": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 3}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitConfig.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 3.1622776601683795}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.23606797749979}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 2.23606797749979}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 29}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 3.4641016151377544}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.449489742783178}, "pyoutlineapi.is_json_serializable": {"tf": 2.449489742783178}}, "df": 5}}}}}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}}}}}}}, "g": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext.__init__": {"tf": 1}}, "df": 1}, "b": {"docs": {"pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}}, "df": 1}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 1}}}}}, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}}, "df": 1}}}}, "j": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 1, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.7320508075688772}, "pyoutlineapi.is_json_serializable": {"tf": 1.7320508075688772}}, "df": 5}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.7320508075688772}, "pyoutlineapi.is_json_serializable": {"tf": 1.7320508075688772}}, "df": 5}}}}}}}}}}, "bases": {"root": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 2.23606797749979}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1.4142135623730951}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1.4142135623730951}, "pyoutlineapi.ValidationError": {"tf": 1}}, "df": 35}}}}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.OutlineClientConfig": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.MetricsCollector": {"tf": 1}}, "df": 2}}}}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}}, "df": 6}}}}}}}}}, "n": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1.4142135623730951}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}}, "df": 6}}}}}, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 26}}}}, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 2}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 26}, "d": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.MetricsCollector": {"tf": 1}}, "df": 3}}}}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 1, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 26}}}}}}}}}}}}}}, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 1}}}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig": {"tf": 1}}, "df": 1}}}}}}}}}}}, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}}, "df": 1}}}}}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 2}}, "df": 1}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 2}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 1}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 3}}}}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineClientConfig": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 1}}}}}}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig": {"tf": 1}}, "df": 1}}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}, "doc": {"root": {"0": {"1": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1, "t": {"0": {"0": {"docs": {}, "df": 0, ":": {"0": {"0": {"docs": {}, "df": 0, ":": {"0": {"0": {"docs": {}, "df": 0, "z": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "docs": {"pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1.4142135623730951}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 10}, "1": {"0": {"0": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}}, "df": 1}, "1": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "9": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}, "1": {"1": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1}, "docs": {}, "df": 0}, "2": {"1": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "3": {"4": {"5": {"6": {"7": {"8": {"9": {"0": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 2}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 3}, "8": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "5": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}, "docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}}, "df": 9}, "2": {"0": {"2": {"4": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}, "5": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 6, "x": {"docs": {"pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 1}}, "4": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}, "5": {"6": {"docs": {"pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.4142135623730951}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 4}, "3": {"0": {"docs": {"pyoutlineapi.OutlineTimeoutError": {"tf": 1}}, "df": 1, "m": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}, "9": {"docs": {"pyoutlineapi.OutlineError": {"tf": 2}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2}, "pyoutlineapi.build_config_overrides": {"tf": 2}, "pyoutlineapi.get_safe_error_dict": {"tf": 3.1622776601683795}}, "df": 4}, "docs": {}, "df": 0}, "4": {"0": {"0": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}}, "df": 1}, "4": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}}, "df": 2}, "docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}}, "df": 1}, "2": {"9": {"docs": {"pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1.4142135623730951}}, "df": 1}, "docs": {}, "df": 0}, "4": {"3": {"docs": {"pyoutlineapi.OutlineConnectionError": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "5": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}, "9": {"9": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "x": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}}, "df": 1}}}, "5": {"0": {"0": {"docs": {"pyoutlineapi.APIError.is_server_error": {"tf": 1}}, "df": 1}, "3": {"docs": {"pyoutlineapi.is_retryable": {"tf": 1}}, "df": 1}, "docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1}, "9": {"9": {"docs": {"pyoutlineapi.APIError.is_server_error": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "x": {"docs": {"pyoutlineapi.APIError.is_server_error": {"tf": 1}}, "df": 1}}}, "6": {"0": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}}, "df": 1}, "4": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 3}, "5": {"5": {"3": {"5": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}}, "df": 3}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "7": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}, "8": {"0": {"8": {"0": {"docs": {"pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 1}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}}, "df": 1}, "6": {"0": {"1": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "9": {"8": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}}, "df": 2}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi": {"tf": 20.346989949375804}, "pyoutlineapi.DEFAULT_SENSITIVE_KEYS": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError": {"tf": 10.344080432788601}, "pyoutlineapi.APIError.__init__": {"tf": 5.196152422706632}, "pyoutlineapi.APIError.status_code": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError.endpoint": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError.response_data": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError.is_retryable": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError.is_client_error": {"tf": 3.3166247903554}, "pyoutlineapi.APIError.is_server_error": {"tf": 3.3166247903554}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 3.3166247903554}, "pyoutlineapi.AccessKey": {"tf": 2.23606797749979}, "pyoutlineapi.AccessKey.id": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.name": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.password": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.port": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.method": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.access_url": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.data_limit": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.validate_name": {"tf": 4.58257569495584}, "pyoutlineapi.AccessKey.validate_id": {"tf": 5.5677643628300215}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 3.3166247903554}, "pyoutlineapi.AccessKey.display_name": {"tf": 3.3166247903554}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 2.23606797749979}, "pyoutlineapi.AccessKeyCreateRequest.name": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyCreateRequest.method": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyCreateRequest.password": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyCreateRequest.port": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyCreateRequest.limit": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyList": {"tf": 2.23606797749979}, "pyoutlineapi.AccessKeyList.access_keys": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyList.count": {"tf": 3.605551275463989}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 3.3166247903554}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 4.58257569495584}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 4.69041575982343}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 3.3166247903554}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 3.3166247903554}, "pyoutlineapi.AccessKeyMetric": {"tf": 2.23606797749979}, "pyoutlineapi.AccessKeyMetric.access_key_id": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyMetric.tunnel_time": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyMetric.data_transferred": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyMetric.connection": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 2.23606797749979}, "pyoutlineapi.AccessKeyNameRequest.name": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 10.44030650891055}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 3.3166247903554}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 3.7416573867739413}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 3.3166247903554}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 10.295630140987}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 11.958260743101398}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 5.5677643628300215}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 6.244997998398398}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 6}, "pyoutlineapi.AuditContext": {"tf": 2.449489742783178}, "pyoutlineapi.AuditContext.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditContext.action": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditContext.resource": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditContext.success": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditContext.details": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditContext.correlation_id": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditContext.from_call": {"tf": 6.557438524302}, "pyoutlineapi.AuditLogger": {"tf": 2.449489742783178}, "pyoutlineapi.AuditLogger.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 1.7320508075688772}, "pyoutlineapi.BandwidthData": {"tf": 2.23606797749979}, "pyoutlineapi.BandwidthData.data": {"tf": 1.7320508075688772}, "pyoutlineapi.BandwidthData.timestamp": {"tf": 1.7320508075688772}, "pyoutlineapi.BandwidthDataValue": {"tf": 2.23606797749979}, "pyoutlineapi.BandwidthDataValue.bytes": {"tf": 1.7320508075688772}, "pyoutlineapi.BandwidthInfo": {"tf": 2.23606797749979}, "pyoutlineapi.BandwidthInfo.current": {"tf": 1.7320508075688772}, "pyoutlineapi.BandwidthInfo.peak": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitConfig": {"tf": 2.449489742783178}, "pyoutlineapi.CircuitConfig.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitConfig.failure_threshold": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitConfig.recovery_timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitConfig.success_threshold": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitConfig.call_timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics": {"tf": 2.449489742783178}, "pyoutlineapi.CircuitMetrics.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.total_calls": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.successful_calls": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.failed_calls": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.state_changes": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.last_failure_time": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.last_success_time": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 3.4641016151377544}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 3.4641016151377544}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 3.7416573867739413}, "pyoutlineapi.CircuitOpenError": {"tf": 9.38083151964686}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 5.196152422706632}, "pyoutlineapi.CircuitOpenError.retry_after": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitState": {"tf": 2.23606797749979}, "pyoutlineapi.CircuitState.CLOSED": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitState.OPEN": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitState.HALF_OPEN": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides": {"tf": 2.449489742783178}, "pyoutlineapi.ConfigOverrides.timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.retry_attempts": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.max_connections": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.rate_limit": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.user_agent": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.enable_circuit_breaker": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.circuit_failure_threshold": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.circuit_recovery_timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.circuit_success_threshold": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.circuit_call_timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.enable_logging": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.json_format": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.allow_private_networks": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides.resolve_dns_for_ssrf": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigurationError": {"tf": 9.1104335791443}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 4.58257569495584}, "pyoutlineapi.ConfigurationError.field": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigurationError.security_issue": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MIN_PORT": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_PORT": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_NAME_LENGTH": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.CERT_FINGERPRINT_LENGTH": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_KEY_ID_LENGTH": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_URL_LENGTH": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_TIMEOUT": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_RETRY_ATTEMPTS": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_MIN_CONNECTIONS": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_MAX_CONNECTIONS": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_RETRY_DELAY": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_MIN_TIMEOUT": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_MAX_TIMEOUT": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_USER_AGENT": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_RECURSION_DEPTH": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_SNAPSHOT_SIZE_MB": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.RETRY_STATUS_CODES": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.LOG_LEVEL_DEBUG": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.LOG_LEVEL_INFO": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.LOG_LEVEL_WARNING": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.LOG_LEVEL_ERROR": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_RESPONSE_SIZE": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_RESPONSE_CHUNK_SIZE": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_RPS": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT_BURST": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DEFAULT_RATE_LIMIT": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_CONNECTIONS_PER_HOST": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.DNS_CACHE_TTL": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.TIMEOUT_WARNING_RATIO": {"tf": 1.7320508075688772}, "pyoutlineapi.Constants.MAX_TIMEOUT": {"tf": 1.7320508075688772}, "pyoutlineapi.CredentialSanitizer": {"tf": 1.7320508075688772}, "pyoutlineapi.CredentialSanitizer.PATTERNS": {"tf": 1.7320508075688772}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 4.58257569495584}, "pyoutlineapi.DataLimit": {"tf": 1.7320508075688772}, "pyoutlineapi.DataLimit.bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 4.58257569495584}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 4.58257569495584}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 4.58257569495584}, "pyoutlineapi.DataLimitRequest": {"tf": 3.4641016151377544}, "pyoutlineapi.DataLimitRequest.limit": {"tf": 1.7320508075688772}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 3.3166247903554}, "pyoutlineapi.DataTransferred": {"tf": 2.23606797749979}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger": {"tf": 1.7320508075688772}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 4.795831523312719}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 5.656854249492381}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 5.656854249492381}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 3.7416573867739413}, "pyoutlineapi.DevelopmentConfig": {"tf": 3.872983346207417}, "pyoutlineapi.DevelopmentConfig.enable_logging": {"tf": 1.7320508075688772}, "pyoutlineapi.DevelopmentConfig.enable_circuit_breaker": {"tf": 1.7320508075688772}, "pyoutlineapi.DevelopmentConfig.timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.ErrorResponse": {"tf": 2.23606797749979}, "pyoutlineapi.ErrorResponse.code": {"tf": 1.7320508075688772}, "pyoutlineapi.ErrorResponse.message": {"tf": 1.7320508075688772}, "pyoutlineapi.ExperimentalMetrics": {"tf": 2.23606797749979}, "pyoutlineapi.ExperimentalMetrics.server": {"tf": 1.7320508075688772}, "pyoutlineapi.ExperimentalMetrics.access_keys": {"tf": 1.7320508075688772}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 4.58257569495584}, "pyoutlineapi.HealthCheckResult": {"tf": 1.7320508075688772}, "pyoutlineapi.HealthCheckResult.healthy": {"tf": 1.7320508075688772}, "pyoutlineapi.HealthCheckResult.timestamp": {"tf": 1.7320508075688772}, "pyoutlineapi.HealthCheckResult.checks": {"tf": 1.7320508075688772}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 3.3166247903554}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 3.4641016151377544}, "pyoutlineapi.HostnameRequest": {"tf": 2.23606797749979}, "pyoutlineapi.HostnameRequest.hostname": {"tf": 1.7320508075688772}, "pyoutlineapi.JsonDict": {"tf": 1.7320508075688772}, "pyoutlineapi.JsonPayload": {"tf": 1.7320508075688772}, "pyoutlineapi.LocationMetric": {"tf": 2.23606797749979}, "pyoutlineapi.LocationMetric.location": {"tf": 1.7320508075688772}, "pyoutlineapi.LocationMetric.asn": {"tf": 1.7320508075688772}, "pyoutlineapi.LocationMetric.as_org": {"tf": 1.7320508075688772}, "pyoutlineapi.LocationMetric.tunnel_time": {"tf": 1.7320508075688772}, "pyoutlineapi.LocationMetric.data_transferred": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector": {"tf": 2.449489742783178}, "pyoutlineapi.MetricsCollector.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 2.23606797749979}, "pyoutlineapi.MetricsEnabledRequest.metrics_enabled": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsStatusResponse": {"tf": 2.23606797749979}, "pyoutlineapi.MetricsStatusResponse.metrics_enabled": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsTags": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager": {"tf": 5.477225575051661}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 6}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 3.3166247903554}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 3.3166247903554}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 3.7416573867739413}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 5.916079783099616}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 3.3166247903554}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 4.58257569495584}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 4.58257569495584}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 3.7416573867739413}, "pyoutlineapi.NoOpAuditLogger": {"tf": 2.449489742783178}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpMetrics": {"tf": 2.449489742783178}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1.7320508075688772}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 5.5677643628300215}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 5.5677643628300215}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 5.5677643628300215}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 3.3166247903554}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 4}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 8.602325267042627}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 3.7416573867739413}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 10.723805294763608}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 11.224972160321824}, "pyoutlineapi.OutlineConnectionError": {"tf": 9.746794344808963}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 4.58257569495584}, "pyoutlineapi.OutlineConnectionError.host": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineConnectionError.port": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineError": {"tf": 10.770329614269007}, "pyoutlineapi.OutlineError.__init__": {"tf": 5.744562646538029}, "pyoutlineapi.OutlineError.details": {"tf": 4.69041575982343}, "pyoutlineapi.OutlineError.safe_details": {"tf": 3.3166247903554}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineTimeoutError": {"tf": 9.746794344808963}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 4.58257569495584}, "pyoutlineapi.OutlineTimeoutError.timeout": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineTimeoutError.operation": {"tf": 1.7320508075688772}, "pyoutlineapi.PeakDeviceCount": {"tf": 2.23606797749979}, "pyoutlineapi.PeakDeviceCount.data": {"tf": 1.7320508075688772}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1.4142135623730951}, "pyoutlineapi.PortRequest": {"tf": 2.23606797749979}, "pyoutlineapi.PortRequest.port": {"tf": 1.7320508075688772}, "pyoutlineapi.ProductionConfig": {"tf": 4.242640687119285}, "pyoutlineapi.ProductionConfig.enable_circuit_breaker": {"tf": 1.7320508075688772}, "pyoutlineapi.ProductionConfig.enable_logging": {"tf": 1.7320508075688772}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 4.58257569495584}, "pyoutlineapi.QueryParams": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseData": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.parse": {"tf": 13.30413469565007}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 11.357816691600547}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 12.12435565298214}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 11}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 11.40175425099138}, "pyoutlineapi.SecureIDGenerator": {"tf": 1.7320508075688772}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 3.605551275463989}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 3.7416573867739413}, "pyoutlineapi.Server": {"tf": 2.23606797749979}, "pyoutlineapi.Server.name": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.server_id": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.metrics_enabled": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.hostname_for_access_keys": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.access_key_data_limit": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.version": {"tf": 1.7320508075688772}, "pyoutlineapi.Server.validate_name": {"tf": 5.5677643628300215}, "pyoutlineapi.Server.has_global_limit": {"tf": 3.3166247903554}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 3.605551275463989}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 2.23606797749979}, "pyoutlineapi.ServerExperimentalMetric.tunnel_time": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerExperimentalMetric.data_transferred": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerExperimentalMetric.bandwidth": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerExperimentalMetric.locations": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerMetrics": {"tf": 2.23606797749979}, "pyoutlineapi.ServerMetrics.bytes_transferred_by_user_id": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 3.3166247903554}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 3.3166247903554}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 3.3166247903554}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 4.58257569495584}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 4.58257569495584}, "pyoutlineapi.ServerNameRequest": {"tf": 2.23606797749979}, "pyoutlineapi.ServerNameRequest.name": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.server": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.access_keys_count": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.healthy": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.transfer_metrics": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.experimental_metrics": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.error": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 3.3166247903554}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 3.3166247903554}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 3.3166247903554}, "pyoutlineapi.TimestampMs": {"tf": 1.7320508075688772}, "pyoutlineapi.TimestampSec": {"tf": 1.7320508075688772}, "pyoutlineapi.TunnelTime": {"tf": 2.23606797749979}, "pyoutlineapi.TunnelTime.seconds": {"tf": 1.7320508075688772}, "pyoutlineapi.ValidationError": {"tf": 9.38083151964686}, "pyoutlineapi.ValidationError.__init__": {"tf": 4.58257569495584}, "pyoutlineapi.ValidationError.field": {"tf": 1.7320508075688772}, "pyoutlineapi.ValidationError.model": {"tf": 1.7320508075688772}, "pyoutlineapi.Validators": {"tf": 1.7320508075688772}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 5.656854249492381}, "pyoutlineapi.Validators.validate_port": {"tf": 5.5677643628300215}, "pyoutlineapi.Validators.validate_name": {"tf": 5.5677643628300215}, "pyoutlineapi.Validators.validate_url": {"tf": 6.244997998398398}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 5.916079783099616}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 5.916079783099616}, "pyoutlineapi.Validators.validate_since": {"tf": 6.48074069840786}, "pyoutlineapi.Validators.validate_key_id": {"tf": 5.5677643628300215}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 4.58257569495584}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 4.58257569495584}, "pyoutlineapi.audited": {"tf": 8.06225774829855}, "pyoutlineapi.build_config_overrides": {"tf": 9}, "pyoutlineapi.correlation_id": {"tf": 1.7320508075688772}, "pyoutlineapi.create_client": {"tf": 8.48528137423857}, "pyoutlineapi.create_env_template": {"tf": 3.872983346207417}, "pyoutlineapi.create_multi_server_manager": {"tf": 13.674794331177344}, "pyoutlineapi.format_error_chain": {"tf": 11.180339887498949}, "pyoutlineapi.get_audit_logger": {"tf": 3.3166247903554}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 4.69041575982343}, "pyoutlineapi.get_retry_delay": {"tf": 8.54400374531753}, "pyoutlineapi.get_safe_error_dict": {"tf": 9.433981132056603}, "pyoutlineapi.get_version": {"tf": 3.3166247903554}, "pyoutlineapi.is_json_serializable": {"tf": 4.58257569495584}, "pyoutlineapi.is_retryable": {"tf": 9.055385138137417}, "pyoutlineapi.is_valid_bytes": {"tf": 4.58257569495584}, "pyoutlineapi.is_valid_port": {"tf": 4.58257569495584}, "pyoutlineapi.load_config": {"tf": 9.055385138137417}, "pyoutlineapi.mask_sensitive_data": {"tf": 6}, "pyoutlineapi.print_type_info": {"tf": 1.7320508075688772}, "pyoutlineapi.quick_setup": {"tf": 3}, "pyoutlineapi.set_audit_logger": {"tf": 4}}, "df": 359, "p": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineError": {"tf": 1}}, "df": 2}}}}}}}}}}, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 2}}}}}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}}, "df": 3}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AuditLogger.alog_action": {"tf": 1}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 2, "/": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "e": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 2, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 2}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 2}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}}, "df": 1, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}}, "df": 6}}}}, "e": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}}, "df": 2}}}, "y": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}}, "df": 1}}}}}, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.MetricsCollector": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 3}}}}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 2}}}}}}}, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 1}}}}}}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 1}, "s": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_port": {"tf": 2.23606797749979}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.is_valid_port": {"tf": 1.4142135623730951}}, "df": 10}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1}}, "df": 4, "s": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1.7320508075688772}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 57}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 2}}}, "e": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2}}, "df": 2}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}}, "df": 3}}}}, "h": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1.7320508075688772}}, "df": 8}}, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}, "y": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1.4142135623730951}}, "df": 2}}}}}, "c": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.get_version": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 4, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 9}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 2}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 2}}}, "s": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}}, "df": 2, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.PeakDeviceCount": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}}, "df": 5}}}, "a": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 9, "s": {"docs": {"pyoutlineapi": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 2}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 12, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi": {"tf": 2}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}, "pyoutlineapi.set_audit_logger": {"tf": 1.4142135623730951}}, "df": 12, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 2.23606797749979}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 5}}}}}}}}}}}}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}}, "df": 2}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}}}, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.APIError": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}}, "df": 23, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 3}}}}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.Constants": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 2}}}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 14, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}}, "df": 3}}}}}}}, "w": {"docs": {"pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1.4142135623730951}}, "df": 2, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}}, "df": 1}}}, "s": {"docs": {"pyoutlineapi.MetricsCollector": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.print_type_info": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}}, "df": 7}}}}}}}, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3}}}}}}}, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 6}}}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 14, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1}}, "df": 1}}}}}}, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 2}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 2}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 6}}, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}}, "df": 4}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}}, "df": 3}}}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}}, "df": 3}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 2}}}}}}}}, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}, "pyoutlineapi.get_audit_logger": {"tf": 1.4142135623730951}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1.4142135623730951}, "pyoutlineapi.set_audit_logger": {"tf": 1.4142135623730951}}, "df": 18, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 2}}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 2}}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 4}}}}}}}, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 5, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 4}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 6}}}}}}}, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 12}}}}}}, "s": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}}, "df": 2}}}, "e": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 5}}, "f": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 2}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}}, "df": 6}}}}, "n": {"docs": {"pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.4142135623730951}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 2, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.CredentialSanitizer": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 20}, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}}, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 3}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}}, "df": 2}}}}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 2}}, "df": 2}}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ValidationError": {"tf": 1}}, "df": 1}}}}}}, "b": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.print_type_info": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 1, "r": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}, "l": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 2}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1.7320508075688772}, "pyoutlineapi.ValidationError.__init__": {"tf": 1.4142135623730951}}, "df": 15}}, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}}, "df": 1}}}}}}}}}}, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 2}}}}}, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.NoOpMetrics": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 4}}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 1}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 6, "s": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.4142135623730951}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1.4142135623730951}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsCollector": {"tf": 1.4142135623730951}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 31, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}, "/": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}}, "df": 2}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ServerMetrics": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 2}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.449489742783178}, "pyoutlineapi.ValidationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 10, "s": {"docs": {"pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 2}}}}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.BandwidthData": {"tf": 1}}, "df": 1}}}}}}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 4}}}}, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_megabytes": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}}, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1.4142135623730951}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 7}, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 6}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 2.449489742783178}}, "df": 5}}}}, "y": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}}, "df": 1}}, "x": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}}, "df": 2, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}}, "df": 5}}}}}, "k": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineClientConfig": {"tf": 1}}, "df": 1, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}}, "df": 1}}}}}}}}}, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 1}}, "s": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 2}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}}}, "l": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 4}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 2, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}, "b": {"docs": {"pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}}, "df": 1}, "y": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1.7320508075688772}}, "df": 1}}}}}}}, "f": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 3}}}, "n": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 2}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 7}}}}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.7320508075688772}, "pyoutlineapi.create_env_template": {"tf": 1.4142135623730951}, "pyoutlineapi.quick_setup": {"tf": 1.4142135623730951}}, "df": 4}}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1.7320508075688772}, "pyoutlineapi.ValidationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}}, "df": 10, "s": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 2}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.AuditLogger": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1.7320508075688772}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.MetricsCollector": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1.4142135623730951}}, "df": 66, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1.4142135623730951}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1.7320508075688772}}, "df": 5, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ErrorResponse": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 8}}}, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.NoOpMetrics": {"tf": 1}}, "df": 1}}}}}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 2}}, "n": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.7320508075688772}, "pyoutlineapi.audited": {"tf": 2.23606797749979}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 5}}}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi": {"tf": 2.6457513110645907}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.7320508075688772}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.CredentialSanitizer": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.audited": {"tf": 2.449489742783178}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.format_error_chain": {"tf": 1.4142135623730951}, "pyoutlineapi.get_audit_logger": {"tf": 1}}, "df": 22}}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 5, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 5}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 14}}, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}}, "df": 2}}}, "l": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.7320508075688772}, "pyoutlineapi.audited": {"tf": 1}}, "df": 6}}, "l": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 3}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 2}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 2}}}}}}}}, "c": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 2.6457513110645907}, "pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 2}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 2}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 2}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 2.23606797749979}}, "df": 12, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}}, "df": 4}}}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 3}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1.4142135623730951}}, "df": 2}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}}, "df": 1, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1}}, "df": 3}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}}, "df": 4, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1.4142135623730951}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.APIError": {"tf": 2}, "pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 10}}, "m": {"docs": {"pyoutlineapi.OutlineConnectionError": {"tf": 1}}, "df": 1, "/": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 2}}}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}}, "df": 3}}, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 3}}}}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 1}}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}}, "df": 1}, "d": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}}, "df": 1}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 1}}}}}}}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1.4142135623730951}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1.4142135623730951}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 6}}}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 2}}}}}, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1.4142135623730951}}, "df": 5, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 8, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}}, "df": 1}, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 1}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}}}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AccessKey.display_name": {"tf": 1}}, "df": 1}}}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}}, "df": 10, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 2.23606797749979}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 2}, "pyoutlineapi.CircuitConfig": {"tf": 1.4142135623730951}, "pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1.4142135623730951}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.7320508075688772}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.load_config": {"tf": 1.7320508075688772}, "pyoutlineapi.quick_setup": {"tf": 1.4142135623730951}}, "df": 23, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 9}}}}}, "s": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.7320508075688772}}, "df": 2}}}}}, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3}}}}, "s": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.7320508075688772}}, "df": 2}}}}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}}, "df": 5}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}}, "df": 3}}}}}}, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}}, "df": 2, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 2}}}}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 3}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 3}}}}}}, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 2}}}}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.Constants": {"tf": 1}}, "df": 1}}}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}}, "df": 4, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MetricsCollector.increment": {"tf": 1}}, "df": 1}}, "s": {"docs": {"pyoutlineapi.is_valid_bytes": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 7}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.MetricsCollector": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 10}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1.4142135623730951}}, "df": 3}}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 4}}, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}}, "df": 2}}}}, "e": {"docs": {"pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}}, "df": 4}}}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineError.details": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.MetricsCollector": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 7, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 6, "/": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.BandwidthData": {"tf": 1}}, "df": 1}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 2}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 14, "s": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 2}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 2}}, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.CredentialSanitizer": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.details": {"tf": 1}}, "df": 3}}}}}}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.SecureIDGenerator": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.7320508075688772}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1.4142135623730951}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 21, "s": {"docs": {"pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 6}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 2}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.7320508075688772}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 7}}}}}}}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1.7320508075688772}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 12, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}}, "df": 1}}}}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1.7320508075688772}, "pyoutlineapi.DataLimitRequest": {"tf": 1.7320508075688772}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 4}, "i": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}}, "df": 5}}, "a": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 10}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 2}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1.7320508075688772}}, "df": 2}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1.4142135623730951}}, "df": 2}}}}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}}, "df": 1}}}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.create_env_template": {"tf": 1.7320508075688772}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 2}}}}}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.ConfigOverrides": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 2}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}}, "df": 10, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}}, "df": 1}}}}}}}}, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 2}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1.4142135623730951}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 28, "/": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}}, "df": 4, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}}, "df": 5}}}}}}}}}, "y": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 2}}, "o": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1.4142135623730951}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 41, "o": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}}, "df": 1}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1.7320508075688772}}, "df": 5}}}, "p": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1.4142135623730951}}, "df": 1}}, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1.4142135623730951}}, "df": 6, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 2.23606797749979}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 20, "s": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1.7320508075688772}}, "df": 7, "s": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {"pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}}, "df": 2}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.MetricsCollector.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}}, "df": 2}}}}}, "u": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 1, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 1}}}}}}}}}}, "o": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}}, "df": 4, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}}, "df": 4, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}}, "df": 5, "c": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 4}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineConnectionError": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}}, "df": 1}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1.4142135623730951}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 21, "g": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}}}}, "n": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 28, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 4}}}, "p": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 8, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 24}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}}, "df": 2}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1.7320508075688772}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 6}}, "s": {"docs": {"pyoutlineapi.quick_setup": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1.4142135623730951}}, "df": 3, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 2}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 6, "s": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 5}}}}}}}}}, "f": {"docs": {"pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.MetricsCollector": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 29}, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 13}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 1, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.7320508075688772}, "pyoutlineapi.create_client": {"tf": 1.7320508075688772}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 10}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}}, "df": 1}}}}}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "w": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 6}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 1}}}}}}}}, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 2}}}}}}}}}, "v": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}}, "df": 6, "p": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}}, "df": 2}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}}, "df": 2}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 2}, "pyoutlineapi.Validators.validate_since": {"tf": 1.7320508075688772}, "pyoutlineapi.is_json_serializable": {"tf": 1.7320508075688772}, "pyoutlineapi.is_valid_bytes": {"tf": 1.7320508075688772}, "pyoutlineapi.is_valid_port": {"tf": 1.7320508075688772}}, "df": 10, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 18}}}}}, "s": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}}, "df": 2}}}, "i": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.is_valid_port": {"tf": 1.4142135623730951}}, "df": 3, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_url": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 15, "d": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 14}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.ValidationError": {"tf": 1.7320508075688772}, "pyoutlineapi.ValidationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 11, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}}, "df": 2}}}}}}}, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ResponseParser": {"tf": 1}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.get_version": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 2.23606797749979}, "pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 2}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.validate_name": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 25, "/": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.HostnameRequest": {"tf": 1}}, "df": 1}}}}}}}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.PortRequest": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 2}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 8}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ValidationError": {"tf": 1}}, "df": 1}}}}}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}}, "df": 2}}}}, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.is_json_serializable": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}}}}, "l": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 3}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}}, "df": 1}}}}}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1.4142135623730951}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 2}}, "df": 8}}}}}}}, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}}, "df": 1}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_call_timeout": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1.4142135623730951}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 15}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.Constants": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.Validators": {"tf": 1}}, "df": 7}}}, "e": {"docs": {"pyoutlineapi.SecureIDGenerator": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 3}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {"pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}}, "df": 3}}}}, "u": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.quick_setup": {"tf": 1}}, "df": 1}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 3}}}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 1}}}}}}}}}, "o": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 2}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.APIError": {"tf": 2}, "pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_server_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.7320508075688772}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1.4142135623730951}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 13}}, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {"pyoutlineapi": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 3, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}}, "df": 2}}, "e": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2}}, "df": 1, "d": {"docs": {"pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 3}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 2}, "pyoutlineapi.get_version": {"tf": 1}}, "df": 8, "s": {"docs": {"pyoutlineapi.CredentialSanitizer": {"tf": 1}}, "df": 1}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1.4142135623730951}}, "df": 2}}, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1.4142135623730951}}, "df": 4, "d": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1.7320508075688772}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}}, "df": 11}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.4142135623730951}}, "df": 2}}}}}}}}}}, "f": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.7320508075688772}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 2}, "pyoutlineapi.set_audit_logger": {"tf": 1.4142135623730951}}, "df": 13, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 2}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.AccessKey": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}}, "df": 23}}}}}, "h": {"docs": {}, "df": 0, "a": {"2": {"5": {"6": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.7320508075688772}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {"pyoutlineapi.OutlineClientConfig.cert_sha256": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 3, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.MetricsStatusResponse": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 2}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}}, "df": 4}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 4}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 3}}}, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1.7320508075688772}}, "df": 1}}}, "g": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 2}}}}}}}, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.max_connections": {"tf": 1}}, "df": 7}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 2}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 4}}}}}, "c": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 6, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 3}}}, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}}, "df": 1}}, "/": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 1}}}}}, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 2}}}}}, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 4}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 1}}}}}}}, "y": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}}, "df": 2, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}}, "df": 2, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 2}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 2}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}}, "df": 3}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}}, "df": 2}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}, "f": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 2, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.7320508075688772}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 7, "s": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.__init__": {"tf": 2}, "pyoutlineapi.OutlineError.details": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 8}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 2}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.APIError": {"tf": 1}}, "df": 1}}}}}}}}, "e": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 2}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1.7320508075688772}}, "df": 4}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}, "c": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.MetricsCollector": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1.7320508075688772}}, "df": 1}}}, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}}, "df": 1, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 3}}}}}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}}, "df": 1}}}}}}, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}}, "df": 2}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1.4142135623730951}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 2}}}}}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.PeakDeviceCount": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"pyoutlineapi.APIError": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2.23606797749979}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1.4142135623730951}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 2.23606797749979}}, "df": 23, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest": {"tf": 1}}, "df": 4}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DataTransferred": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKey.display_name": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 2}}}}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1.4142135623730951}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 3}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 1}}}}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 6, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1.4142135623730951}}, "df": 12}, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.HealthCheckResult": {"tf": 1}}, "df": 1}}}}}}}}}}, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}}, "df": 1}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}}}}}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}}, "df": 1, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}}, "df": 1}}}}}}, "y": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}, "n": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 2}}}, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}}}}, "o": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.7320508075688772}}, "df": 4}}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}}, "df": 1}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.resolve_dns_for_ssrf": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1.4142135623730951}}, "df": 2}}}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.4142135623730951}, "pyoutlineapi.ErrorResponse": {"tf": 1.4142135623730951}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 2}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 2.8284271247461903}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}}, "df": 14, "s": {"docs": {"pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 3}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 2}}, "df": 5}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}}, "df": 8, "s": {"docs": {"pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 1}}}}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}}, "df": 2}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}}, "df": 4}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.timeout": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerNameRequest": {"tf": 1.4142135623730951}}, "df": 14, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.rate_limit": {"tf": 1}}, "df": 4}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 2.23606797749979}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1.7320508075688772}}, "df": 10, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1.4142135623730951}}, "df": 8}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}}, "df": 1}, "d": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 3}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}}, "df": 9, "s": {"docs": {"pyoutlineapi.APIError.is_client_error": {"tf": 1}, "pyoutlineapi.APIError.is_server_error": {"tf": 1}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.4142135623730951}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}, "pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.7320508075688772}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.get_version": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 97}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}}, "df": 2}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}}, "df": 1}}}}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}}, "df": 2}}}}, "r": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.MetricsCollector.timing": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1.7320508075688772}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}}}, "x": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}}, "df": 1}}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CredentialSanitizer.sanitize": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}}, "df": 3}}}}, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1}}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineConnectionError": {"tf": 1}}, "df": 1}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}}, "df": 1}}}, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}}}}}, "a": {"docs": {}, "df": 0, "w": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}}, "df": 4}, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1.4142135623730951}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1.4142135623730951}}, "df": 6, "s": {"docs": {"pyoutlineapi.CircuitMetrics.to_dict": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 28}, "d": {"docs": {"pyoutlineapi.ValidationError": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}}, "df": 2}}, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.CircuitConfig": {"tf": 1}}, "df": 2}}}}}}}, "x": {"6": {"1": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "2": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "5": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "9": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1}, "e": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "d": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}, "7": {"0": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "2": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}, "4": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "u": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1, "l": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}}, "df": 1}}}}}}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.DataLimit": {"tf": 1}}, "df": 1}, "x": {"docs": {"pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}}, "df": 2}}}, "s": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}}, "df": 1, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.print_type_info": {"tf": 1}}, "df": 4}}}, "e": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 4, "r": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 2}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 7, "s": {"docs": {"pyoutlineapi.ServerMetrics.user_count": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1.4142135623730951}}, "df": 2}, "/": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {"pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.HealthCheckResult.success_rate": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 13}, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 3}}}, "d": {"docs": {"pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 2}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.ResponseParser": {"tf": 1}}, "df": 2}, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators": {"tf": 1}}, "df": 1}}}}}}}}, "r": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.ConfigurationError": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.7320508075688772}, "pyoutlineapi.Validators.validate_url": {"tf": 2.23606797749979}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 2}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}}, "df": 11, "s": {"docs": {"pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1.4142135623730951}}, "df": 1}}}, "p": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}}, "df": 1}}}}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.APIError": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_server_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 2}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1.4142135623730951}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 44, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}}, "df": 2}}}, "o": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.AuditLogger": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 1}}}}, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}}, "df": 6}}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}}, "df": 1}}}}}}}}, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.PeakDeviceCount.timestamp": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_ms": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 20, "f": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}}, "df": 8, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.print_type_info": {"tf": 1}}, "df": 8}}}}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1}}, "df": 11, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}}, "df": 1}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 20}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_megabytes": {"tf": 1}, "pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.7320508075688772}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1.7320508075688772}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1.4142135623730951}}, "df": 22, "s": {"docs": {"pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 2}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}}, "df": 1}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}}}}}}}, "t": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}}}}}}, "o": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1.4142135623730951}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 3}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.Validators.validate_non_negative": {"tf": 1.4142135623730951}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.4142135623730951}}, "df": 2}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 1}}}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {"pyoutlineapi.MultiServerManager.get_client": {"tf": 1.7320508075688772}}, "df": 1, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager.get_client": {"tf": 1}}, "df": 1}}}}}}}}, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.MetricsCollector": {"tf": 1}}, "df": 1}}}}}}}, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.MetricsCollector.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}}, "df": 2}}}}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 1}}}}}}, "p": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.Validators": {"tf": 1}}, "df": 1}}}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 2}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1.7320508075688772}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1.4142135623730951}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.7320508075688772}, "pyoutlineapi.SecureIDGenerator": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1.4142135623730951}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 2.23606797749979}, "pyoutlineapi.audited": {"tf": 2.23606797749979}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1.4142135623730951}}, "df": 16, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1.4142135623730951}}, "df": 3, "s": {"docs": {"pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}}, "df": 1}}}}}}}}}, "s": {"docs": {"pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 1}}, "f": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_server_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1}, "pyoutlineapi.Validators.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.Validators.validate_key_id": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1.4142135623730951}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 56}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "m": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}}, "df": 2}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}}, "df": 1}}}}}}}}, "p": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}, "/": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.create_env_template": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}}, "df": 1, "d": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1.4142135623730951}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 8, "s": {"docs": {"pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1.4142135623730951}, "pyoutlineapi.Constants": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 4}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 12}}, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}}}}}}}, "o": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.AuditLogger.log_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.audited": {"tf": 2.23606797749979}}, "df": 8, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.MultiServerManager.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}, "pyoutlineapi.get_audit_logger": {"tf": 1.4142135623730951}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1.4142135623730951}, "pyoutlineapi.set_audit_logger": {"tf": 1.7320508075688772}}, "df": 15}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.Validators.sanitize_url_for_logging": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}}, "df": 15, "/": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.OutlineError.__init__": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "s": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}}, "df": 2}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.LocationMetric": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.LocationMetric": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}}, "df": 3}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 3}}, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 1}}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1, "g": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.OutlineError.__init__": {"tf": 1}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.ServerExperimentalMetric": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.mask_sensitive_data": {"tf": 1.4142135623730951}}, "df": 1}}}}, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}}, "df": 1}}}}}, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "p": {"docs": {"pyoutlineapi.APIError": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.ProductionConfig": {"tf": 1.4142135623730951}}, "df": 1, ":": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}}}}}, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "b": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}}}}, "s": {"1": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1}, "2": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi": {"tf": 1}}, "df": 1}}}, "g": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AsyncOutlineClient": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.ResponseParser": {"tf": 1}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 4}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}}, "df": 2}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 3}}}}}}, "s": {"docs": {"pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}}, "df": 3}, "l": {"docs": {}, "df": 0, "f": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}, "r": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.Validators": {"tf": 1}}, "df": 1}}}}}}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1.7320508075688772}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1.7320508075688772}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.4142135623730951}}, "df": 6, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1.4142135623730951}}, "df": 3}}}}}, "x": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}}, "df": 1}}, "o": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}, "s": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineConnectionError": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}}, "df": 3, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 2}}}}}}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 3}}}, "o": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 2.449489742783178}, "pyoutlineapi.APIError": {"tf": 2}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.ConfigurationError": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 2}, "pyoutlineapi.OutlineConnectionError": {"tf": 2}, "pyoutlineapi.OutlineError": {"tf": 2.449489742783178}, "pyoutlineapi.OutlineTimeoutError": {"tf": 2}, "pyoutlineapi.ResponseParser.parse": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 3.7416573867739413}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 3.1622776601683795}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 2.8284271247461903}, "pyoutlineapi.ValidationError": {"tf": 2.449489742783178}, "pyoutlineapi.create_multi_server_manager": {"tf": 2.8284271247461903}, "pyoutlineapi.format_error_chain": {"tf": 2}, "pyoutlineapi.get_retry_delay": {"tf": 1.4142135623730951}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}, "pyoutlineapi.is_retryable": {"tf": 1.4142135623730951}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}}, "df": 22}}, "e": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1.4142135623730951}}, "df": 2}}}}}, "e": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_since": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1.4142135623730951}}, "df": 3, "n": {"docs": {}, "df": 0, "v": {"docs": {"pyoutlineapi": {"tf": 2}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 2.449489742783178}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 2.23606797749979}, "pyoutlineapi.create_client": {"tf": 1.4142135623730951}, "pyoutlineapi.create_env_template": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 8, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 2}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.4142135623730951}, "pyoutlineapi.load_config": {"tf": 2}}, "df": 4}}}}}}}}}, "d": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.APIError": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.Validators.sanitize_endpoint_for_logging": {"tf": 2}}, "df": 4}}}}}}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_logging": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}}, "df": 5, "d": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 4}, "s": {"docs": {"pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.MetricsEnabledRequest": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}}, "df": 1, "s": {"docs": {"pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 1}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 1}}}}}}}}}, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.parse": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}}, "df": 1}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.Validators.validate_key_id": {"tf": 1}}, "df": 1}}}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.APIError": {"tf": 1}}, "df": 1}}}}}}}}}}, "x": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.create_multi_server_manager": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.load_config": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 30}}}}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.Server.has_global_limit": {"tf": 1}}, "df": 2}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.AccessKeyMetric": {"tf": 1.4142135623730951}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1.4142135623730951}, "pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 11, "/": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.ExperimentalMetrics": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimitRequest": {"tf": 1}}, "df": 1}, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.ValidationError": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.create_client": {"tf": 1}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 3}}}, "s": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}}, "df": 1}}}}}}, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 2, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1.7320508075688772}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}}, "df": 8, "s": {"docs": {"pyoutlineapi.CredentialSanitizer": {"tf": 1}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}, "s": {"docs": {"pyoutlineapi.OutlineError.__init__": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1}}}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.APIError": {"tf": 2}, "pyoutlineapi.APIError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.APIError.is_client_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_server_error": {"tf": 1.4142135623730951}, "pyoutlineapi.APIError.is_rate_limit_error": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ErrorResponse": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineConnectionError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineTimeoutError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 2.6457513110645907}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 2.8284271247461903}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.ValidationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1.7320508075688772}, "pyoutlineapi.get_retry_delay": {"tf": 2}, "pyoutlineapi.get_safe_error_dict": {"tf": 2.449489742783178}, "pyoutlineapi.is_retryable": {"tf": 2.449489742783178}}, "df": 32, "s": {"docs": {"pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1.4142135623730951}}, "df": 3}}}}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.Server.validate_name": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1.4142135623730951}}, "df": 5}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}}, "df": 4}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.DevelopmentConfig": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.MultiServerManager.health_check_all": {"tf": 1}}, "df": 1}}}, "t": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}}, "df": 4}}, "f": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}}, "df": 2}}, "t": {"docs": {"pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.create_env_template": {"tf": 1}}, "df": 3, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 3}}}}}}}}}}}, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi": {"tf": 1.7320508075688772}, "pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.AuditLogger": {"tf": 1}, "pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1.4142135623730951}, "pyoutlineapi.Constants": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1.4142135623730951}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.HealthCheckResult": {"tf": 1}, "pyoutlineapi.MultiServerManager": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_cert": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_user_agent": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.validate_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.ProductionConfig": {"tf": 1}, "pyoutlineapi.ProductionConfig.enforce_security": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1}, "pyoutlineapi.ServerSummary": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}, "pyoutlineapi.Validators": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.create_client": {"tf": 2}, "pyoutlineapi.create_multi_server_manager": {"tf": 1.7320508075688772}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}, "pyoutlineapi.quick_setup": {"tf": 1}}, "df": 62, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}}, "df": 7}}}}}, "l": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}}, "df": 1}}, "d": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.Constants": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_recovery_timeout": {"tf": 1}}, "df": 5}}, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.OutlineError.details": {"tf": 1}}, "df": 1}}}}}}, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.ConfigOverrides": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}}, "df": 4}}}}, "n": {"docs": {"pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 2}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}}, "df": 2}}}}, "g": {"docs": {"pyoutlineapi.Validators.validate_since": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "t": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.display_name": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_sanitized_config": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_status": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_server_names": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_all_clients": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_healthy_servers": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.get_sanitized_config": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1}, "pyoutlineapi.OutlineError.details": {"tf": 1}, "pyoutlineapi.OutlineError.safe_details": {"tf": 1}, "pyoutlineapi.OutlineTimeoutError": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.create_client": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_or_create_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1.4142135623730951}, "pyoutlineapi.get_safe_error_dict": {"tf": 1}, "pyoutlineapi.get_version": {"tf": 1}}, "df": 47}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.SecureIDGenerator": {"tf": 1}}, "df": 1}}}, "e": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}, "pyoutlineapi.SecureIDGenerator.generate_request_id": {"tf": 1}}, "df": 2}}}}}}}, "t": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.APIError": {"tf": 3}, "pyoutlineapi.AsyncOutlineClient.__init__": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.7320508075688772}, "pyoutlineapi.AsyncOutlineClient.from_env": {"tf": 1.7320508075688772}, "pyoutlineapi.CircuitOpenError": {"tf": 3}, "pyoutlineapi.ConfigurationError": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.from_env": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineConnectionError": {"tf": 2.449489742783178}, "pyoutlineapi.OutlineError": {"tf": 1.7320508075688772}, "pyoutlineapi.OutlineTimeoutError": {"tf": 2.449489742783178}, "pyoutlineapi.ResponseParser.parse": {"tf": 3.872983346207417}, "pyoutlineapi.ResponseParser.parse_simple": {"tf": 3}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 3}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 3}, "pyoutlineapi.ResponseParser.is_error_response": {"tf": 3.4641016151377544}, "pyoutlineapi.ValidationError": {"tf": 1.7320508075688772}, "pyoutlineapi.build_config_overrides": {"tf": 2.449489742783178}, "pyoutlineapi.create_multi_server_manager": {"tf": 2.449489742783178}, "pyoutlineapi.format_error_chain": {"tf": 1.7320508075688772}, "pyoutlineapi.get_retry_delay": {"tf": 2.449489742783178}, "pyoutlineapi.get_safe_error_dict": {"tf": 2.449489742783178}, "pyoutlineapi.is_retryable": {"tf": 2.449489742783178}, "pyoutlineapi.load_config": {"tf": 1.7320508075688772}}, "df": 26}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.MultiServerManager": {"tf": 1}}, "df": 1, "l": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.shutdown": {"tf": 1}}, "df": 2}}}}}}}}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}}, "df": 2}}}}}}}}, "b": {"docs": {"pyoutlineapi.DataLimit.from_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1.4142135623730951}}, "df": 3}, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.MetricsCollector.gauge": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}}, "df": 2}}}}, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.is_json_serializable": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}, "pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 3}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.Server.has_global_limit": {"tf": 1.4142135623730951}, "pyoutlineapi.set_audit_logger": {"tf": 1}}, "df": 2}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey.display_name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1.7320508075688772}, "pyoutlineapi.ConfigurationError": {"tf": 1}, "pyoutlineapi.ConfigurationError.__init__": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1.4142135623730951}, "pyoutlineapi.Server.validate_name": {"tf": 2}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1.4142135623730951}, "pyoutlineapi.ValidationError.__init__": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_name": {"tf": 2.23606797749979}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1.4142135623730951}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1.4142135623730951}, "pyoutlineapi.audited": {"tf": 2}, "pyoutlineapi.load_config": {"tf": 1.4142135623730951}}, "df": 17, "s": {"docs": {"pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.HealthCheckResult.failed_checks": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 5}}}}, "o": {"docs": {"pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitMetrics.success_rate": {"tf": 1}, "pyoutlineapi.CircuitMetrics.failure_rate": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_status_summary": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger": {"tf": 1.4142135623730951}, "pyoutlineapi.NoOpAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.log_action": {"tf": 1}, "pyoutlineapi.NoOpAuditLogger.shutdown": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 17, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 4, "e": {"docs": {"pyoutlineapi": {"tf": 2.6457513110645907}, "pyoutlineapi.AccessKey.validate_name": {"tf": 1}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1.4142135623730951}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary.has_errors": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1}, "pyoutlineapi.get_audit_logger": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}}, "df": 12}}, "t": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.create_minimal": {"tf": 1}, "pyoutlineapi.ResponseParser.extract_error_message": {"tf": 1.7320508075688772}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1}, "pyoutlineapi.Validators.validate_string_not_empty": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}, "pyoutlineapi.get_retry_delay": {"tf": 1}, "pyoutlineapi.get_safe_error_dict": {"tf": 1.4142135623730951}}, "df": 11, "e": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}}, "df": 3}}, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1, "i": {"docs": {}, "df": 0, "z": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineClientConfig.validate_api_url": {"tf": 1}, "pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 2, "d": {"docs": {"pyoutlineapi.Validators.validate_cert_fingerprint": {"tf": 1}}, "df": 1}}}}}}}}}, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.AccessKey.port": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.MultiServerManager.server_count": {"tf": 1.4142135623730951}, "pyoutlineapi.MultiServerManager.active_servers": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.retry_attempts": {"tf": 1}, "pyoutlineapi.PortRequest.port": {"tf": 1}, "pyoutlineapi.Server.port_for_new_access_keys": {"tf": 1}, "pyoutlineapi.ServerMetrics.user_count": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}, "pyoutlineapi.ValidationError": {"tf": 1}, "pyoutlineapi.Validators.validate_port": {"tf": 1.4142135623730951}}, "df": 11, "s": {"docs": {"pyoutlineapi.is_valid_port": {"tf": 1}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.Validators.validate_non_negative": {"tf": 1.4142135623730951}}, "df": 2}}}}}}, "t": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.OutlineClientConfig.allow_private_networks": {"tf": 1}, "pyoutlineapi.OutlineConnectionError": {"tf": 1}, "pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 3, "s": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.OutlineClientConfig.circuit_success_threshold": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 2}}}}, "w": {"docs": {"pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 3}}}, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi": {"tf": 1}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKey.validate_id": {"tf": 1.7320508075688772}, "pyoutlineapi.AccessKey.has_data_limit": {"tf": 1}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_id": {"tf": 2}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics.get_key_metric": {"tf": 2}, "pyoutlineapi.Validators.validate_key_id": {"tf": 2.23606797749979}, "pyoutlineapi.audited": {"tf": 1.4142135623730951}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 13, "s": {"docs": {"pyoutlineapi": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.AccessKeyList.is_empty": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.filter_with_limits": {"tf": 1.4142135623730951}, "pyoutlineapi.AccessKeyList.filter_without_limits": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.create": {"tf": 1.4142135623730951}, "pyoutlineapi.AsyncOutlineClient.get_server_summary": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.model_copy_immutable": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 15, "/": {"docs": {}, "df": 0, "{": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "}": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyNameRequest": {"tf": 1}}, "df": 1}}}}}}}}}}}, "w": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.format_error_chain": {"tf": 1}}, "df": 2}}}}}}}, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}}, "df": 2}}}}}, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}}, "b": {"docs": {"pyoutlineapi.DataLimit.from_kilobytes": {"tf": 1}}, "df": 1}}, "b": {"docs": {"pyoutlineapi.create_multi_server_manager": {"tf": 1}}, "df": 1, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.OutlineError": {"tf": 1}}, "df": 1, "d": {"docs": {"pyoutlineapi.APIError": {"tf": 1}, "pyoutlineapi.APIError.is_retryable": {"tf": 1}, "pyoutlineapi.AccessKey": {"tf": 1}, "pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyList": {"tf": 1}, "pyoutlineapi.AccessKeyMetric": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.BandwidthData": {"tf": 1}, "pyoutlineapi.BandwidthDataValue": {"tf": 1}, "pyoutlineapi.BandwidthInfo": {"tf": 1}, "pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.ErrorResponse": {"tf": 1}, "pyoutlineapi.ExperimentalMetrics": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.LocationMetric": {"tf": 1.4142135623730951}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.MetricsStatusResponse": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.PeakDeviceCount": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 1}, "pyoutlineapi.Server": {"tf": 1}, "pyoutlineapi.ServerExperimentalMetric": {"tf": 1}, "pyoutlineapi.ServerMetrics": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}, "pyoutlineapi.TunnelTime": {"tf": 1}}, "df": 26}}, "i": {"docs": {}, "df": 0, "c": {"docs": {"pyoutlineapi.AsyncOutlineClient.health_check": {"tf": 1}}, "df": 1}}}, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.BandwidthData": {"tf": 1.4142135623730951}, "pyoutlineapi.BandwidthDataValue": {"tf": 1.4142135623730951}, "pyoutlineapi.BandwidthInfo": {"tf": 1.4142135623730951}}, "df": 3}}}}}}}, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 2}}, "df": 1, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}}, "df": 3}}}}}}, "c": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DefaultAuditLogger": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.__init__": {"tf": 1}}, "df": 2}}}}}}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.MetricsCollector": {"tf": 1}}, "df": 1}}}}}}}, "e": {"docs": {"pyoutlineapi.APIError.__init__": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.is_retryable": {"tf": 1}, "pyoutlineapi.is_retryable": {"tf": 1}, "pyoutlineapi.mask_sensitive_data": {"tf": 1}}, "df": 6, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.AccessKeyList.count": {"tf": 1}, "pyoutlineapi.Server.created_timestamp_seconds": {"tf": 1}}, "df": 2}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.alog_action": {"tf": 1}, "pyoutlineapi.DefaultAuditLogger.log_action": {"tf": 1}}, "df": 3}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.CircuitOpenError": {"tf": 1.4142135623730951}, "pyoutlineapi.CircuitOpenError.__init__": {"tf": 1}, "pyoutlineapi.CircuitOpenError.default_retry_delay": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_failure_threshold": {"tf": 1}, "pyoutlineapi.OutlineError": {"tf": 1}, "pyoutlineapi.OutlineError.default_retry_delay": {"tf": 1}, "pyoutlineapi.ResponseParser.validate_response_structure": {"tf": 1}}, "df": 7}}}}}, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "y": {"docs": {"pyoutlineapi.AccessKeyCreateRequest": {"tf": 1}, "pyoutlineapi.AccessKeyNameRequest": {"tf": 1}, "pyoutlineapi.DataLimitRequest": {"tf": 1}, "pyoutlineapi.HostnameRequest": {"tf": 1}, "pyoutlineapi.MetricsEnabledRequest": {"tf": 1}, "pyoutlineapi.PortRequest": {"tf": 1}, "pyoutlineapi.ServerNameRequest": {"tf": 1}}, "df": 7}}, "o": {"docs": {}, "df": 0, "l": {"docs": {"pyoutlineapi.audited": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.ResponseParser.is_error_response": {"tf": 1}}, "df": 1}}}}}}, "y": {"docs": {"pyoutlineapi.AccessKeyList.get_by_id": {"tf": 1}, "pyoutlineapi.AccessKeyList.get_by_name": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.MultiServerManager.get_client": {"tf": 1}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1}}, "df": 5, "t": {"docs": {}, "df": 0, "e": {"docs": {"pyoutlineapi.DataTransferred": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}}, "df": 2, "s": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.DataLimit": {"tf": 1}, "pyoutlineapi.DataLimit.bytes": {"tf": 1}, "pyoutlineapi.DataLimitRequest.to_payload": {"tf": 1}, "pyoutlineapi.DataTransferred.bytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.total_bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"tf": 1}, "pyoutlineapi.ServerMetrics.get_user_bytes": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerMetrics.top_users": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary.total_bytes_transferred": {"tf": 1.4142135623730951}, "pyoutlineapi.ServerSummary.total_gigabytes_transferred": {"tf": 1}, "pyoutlineapi.is_valid_bytes": {"tf": 1}}, "df": 13}}}}, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.AuditContext.from_call": {"tf": 1}, "pyoutlineapi.build_config_overrides": {"tf": 1.4142135623730951}}, "df": 2, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"pyoutlineapi.build_config_overrides": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}}, "df": 1}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"pyoutlineapi.CircuitConfig": {"tf": 1}, "pyoutlineapi.CircuitMetrics": {"tf": 1}, "pyoutlineapi.CircuitOpenError": {"tf": 1}, "pyoutlineapi.CircuitState": {"tf": 1}, "pyoutlineapi.DevelopmentConfig": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.enable_circuit_breaker": {"tf": 1}, "pyoutlineapi.OutlineClientConfig.circuit_config": {"tf": 1.4142135623730951}, "pyoutlineapi.ProductionConfig": {"tf": 1}}, "df": 8}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"pyoutlineapi.Validators.validate_url": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "d": {"docs": {"pyoutlineapi.CircuitState": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "s": {"docs": {"pyoutlineapi.SecureIDGenerator.generate_correlation_id": {"tf": 1}}, "df": 1}}}}, "j": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"pyoutlineapi.AsyncOutlineClient.json_format": {"tf": 1.4142135623730951}, "pyoutlineapi.OutlineClientConfig.json_format": {"tf": 1}, "pyoutlineapi.ResponseParser.parse": {"tf": 2.6457513110645907}, "pyoutlineapi.is_json_serializable": {"tf": 1.4142135623730951}}, "df": 4}}}}, "z": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {"pyoutlineapi.NoOpAuditLogger": {"tf": 1}, "pyoutlineapi.NoOpMetrics": {"tf": 1}, "pyoutlineapi.NoOpMetrics.increment": {"tf": 1}, "pyoutlineapi.NoOpMetrics.timing": {"tf": 1}, "pyoutlineapi.NoOpMetrics.gauge": {"tf": 1}, "pyoutlineapi.audited": {"tf": 1}}, "df": 6}}}}}}}, "pipeline": ["trimmer"], "_isPrebuiltIndex": true}; // mirrored in build-search-index.js (part 1) // Also split on html tags. this is a cheap heuristic, but good enough. diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index bd64272..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1404 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.12.11" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiohttp-3.12.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff576cb82b995ff213e58255bc776a06ebd5ebb94a587aab2fb5df8ee4e3f967"}, - {file = "aiohttp-3.12.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fe3a9ae8a7c93bec5b7cfacfbc781ed5ae501cf6a6113cf3339b193af991eaf9"}, - {file = "aiohttp-3.12.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:efafc6f8c7c49ff567e0f02133b4d50eef5183cf96d4b0f1c7858d478e9751f6"}, - {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6866da6869cc60d84921b55330d23cbac4f243aebfabd9da47bbc40550e6548"}, - {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:14aa6f41923324618687bec21adf1d5e8683264ccaa6266c38eb01aeaa404dea"}, - {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4aec7c3ccf2ed6b55db39e36eb00ad4e23f784fca2d38ea02e6514c485866dc"}, - {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efd174af34bd80aa07813a69fee000ce8745962e2d3807c560bdf4972b5748e4"}, - {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb02a172c073b0aaf792f0b78d02911f124879961d262d3163119a3e91eec31d"}, - {file = "aiohttp-3.12.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcf5791dcd63e1fc39f5b0d4d16fe5e6f2b62f0f3b0f1899270fa4f949763317"}, - {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47f7735b7e44965bd9c4bde62ca602b1614292278315e12fa5afbcc9f9180c28"}, - {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d211453930ab5995e99e3ffa7c5c33534852ad123a11761f1bf7810cd853d3d8"}, - {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:104f1f9135be00c8a71c5fc53ac7d49c293a8eb310379d2171f0e41172277a09"}, - {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e6cbaf3c02ef605b6f251d8bb71b06632ba24e365c262323a377b639bcfcbdae"}, - {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9d9922bc6cca3bc7a8f8b60a3435f6bca6e33c8f9490f6079a023cfb4ee65af0"}, - {file = "aiohttp-3.12.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:554f4338611155e7d2f0dc01e71e71e5f6741464508cbc31a74eb35c9fb42982"}, - {file = "aiohttp-3.12.11-cp310-cp310-win32.whl", hash = "sha256:421ca03e2117d8756479e04890659f6b356d6399bbdf07af5a32d5c8b4ace5ac"}, - {file = "aiohttp-3.12.11-cp310-cp310-win_amd64.whl", hash = "sha256:cd58a0fae0d13a44456953d43706f9457b231879c4b3c9d0a1e0c6e2a4913d46"}, - {file = "aiohttp-3.12.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a7603f3998cd2893801d254072aaf1b5117183fcf5e726b6c27fc4239dc8c30a"}, - {file = "aiohttp-3.12.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:afe8c1860fb0df6e94725339376628e915b2b85e734eca4d14281ed5c11275b0"}, - {file = "aiohttp-3.12.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f014d909931e34f81b0080b289642d4fc4f4a700a161bd694a5cebdd77882ab5"}, - {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:734e64ceb8918b3d7099b2d000e174d8d944fb7d494de522cecb0fa45ffcb0cd"}, - {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4b603513b4596a8b80bfbedcb33e9f8ed93f44d3dfaac97db0bb9185a6d2c5c0"}, - {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:196fbd7951b89d9a4be3a09e1f49b3534eb0b764989df66b429e8685138f8d27"}, - {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1585fefa6a62a1140bf3e439f9648cb5bf360be2bbe76d057dddd175c030e30c"}, - {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e2874e665c771e6c87e81f8d4ac64d999da5e1a110b3ae0088b035529a08d5"}, - {file = "aiohttp-3.12.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6563fa3bfb79f892a24d3f39ca246c7409cf3b01a3a84c686e548a69e4fc1bf"}, - {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f31bfeb53cfc5e028a0ade48ef76a3580016b92007ceb8311f5bd1b4472b7007"}, - {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fa806cdb0b7e99fb85daea0de0dda3895eea6a624f962f3800dfbbfc07f34fb6"}, - {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:210470f8078ecd1f596247a70f17d88c4e785ffa567ab909939746161f304444"}, - {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cb9af1ce647cda1707d7b7e23b36eead3104ed959161f14f4ebc51d9b887d4a2"}, - {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ccef35cc9e96bb3fcd79f3ef9d6ae4f72c06585c2e818deafc4a499a220904a1"}, - {file = "aiohttp-3.12.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e8ccb376eaf184bcecd77711697861095bc3352c912282e33d065222682460da"}, - {file = "aiohttp-3.12.11-cp311-cp311-win32.whl", hash = "sha256:7c345f7e7f10ac21a48ffd387c04a17da06f96bd087d55af30d1af238e9e164d"}, - {file = "aiohttp-3.12.11-cp311-cp311-win_amd64.whl", hash = "sha256:b461f7918c8042e927f629eccf7c120197135bd2eb14cc12fffa106b937d051b"}, - {file = "aiohttp-3.12.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3d222c693342ccca64320410ada8f06a47c4762ff82de390f3357a0e51ca102c"}, - {file = "aiohttp-3.12.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f50c10bd5799d82a9effe90d5d5840e055a2c94e208b76f9ed9e6373ca2426fe"}, - {file = "aiohttp-3.12.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a01a21975b0fd5160886d9f2cd6ed13cdfc8d59f2a51051708ed729afcc2a2fb"}, - {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39d29b6888ddd5a120dba1d52c78c0b45f5f34e227a23696cbece684872e62bd"}, - {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1df121c3ffcc5f7381cd4c84e8554ff121f558e92c318f48e049843b47ee9f1b"}, - {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:644f74197757e26266a5f57af23424f8cd506c1ef70d9b288e21244af69d6fdc"}, - {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726d9a15a1fd1058b2d27d094b1fec627e9fd92882ca990d90ded9b7c550bd21"}, - {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405a60b979da942cec2c26381683bc230f3bcca346bf23a59c1dfc397e44b17b"}, - {file = "aiohttp-3.12.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27e75e96a4a747756c2f59334e81cbb9a398e015bc9e08b28f91090e5f3a85ef"}, - {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15e1da30ac8bf92fb3f8c245ff53ace3f0ea1325750cc2f597fb707140dfd950"}, - {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0329934d4df1500f13449c1db205d662123d9d0ee1c9d0c8c0cb997cdac75710"}, - {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a06b2a031d6c828828317ee951f07d8a0455edc9cd4fc0e0432fd6a4dfd612d"}, - {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87ece62697b8792e595627c4179f0eca4b038f39b0b354e67a149fa6f83d9493"}, - {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5c981b7659379b5cb3b149e480295adfcdf557b5892a792519a56badbe9f33ef"}, - {file = "aiohttp-3.12.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e6fb2170cb0b9abbe0bee2767b08bb4a3dbf01583880ecea97bca9f3f918ea78"}, - {file = "aiohttp-3.12.11-cp312-cp312-win32.whl", hash = "sha256:f20e4ec84a26f91adc8c54345a383095248d11851f257c816e8f1d853a6cef4c"}, - {file = "aiohttp-3.12.11-cp312-cp312-win_amd64.whl", hash = "sha256:b54d4c3cd77cf394e71a7ad5c3b8143a5bfe105a40fc693bcdfe472a286f1d95"}, - {file = "aiohttp-3.12.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5fadc4b67f972a701805aa501cd9d22cdbeda21f9c9ae85e60678f84b1727a16"}, - {file = "aiohttp-3.12.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:144d67c29ae36f052584fc45a363e92798441a5af5762d83037aade3e2aa9dc5"}, - {file = "aiohttp-3.12.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b73299e4bf37d14c6e4ca5ce7087b44914a8d9e1f40faedc271f28d64ec277e"}, - {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1226325e98e6d3cdfdaca639efdc3af8e82cd17287ae393626d1bd60626b0e93"}, - {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0ecae011f2f779271407f2959877230670de3c48f67e5db9fbafa9fddbfa3a"}, - {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8a711883eedcd55f2e1ba218d8224b9f20f1dfac90ffca28e78daf891667e3a"}, - {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2601c1fcd9b67e632548cfd3c760741b31490502f6f3e5e21287678c1c6fa1b2"}, - {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d5b11ea794ee54b33d0d817a1aec0ef0dd2026f070b493bc5a67b7e413b95d4"}, - {file = "aiohttp-3.12.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:109b3544138ce8a5aca598d5e7ff958699e3e19ee3675d27d5ee9c2e30765a4a"}, - {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b795085d063d24c6d09300c85ddd6b9c49816d5c498b40b6899ca24584e936e4"}, - {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ebcbc113f40e4c9c0f8d2b6b31a2dd2a9768f3fa5f623b7e1285684e24f5159f"}, - {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:590e5d792150d75fa34029d0555b126e65ad50d66818a996303de4af52b65b32"}, - {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9c2a4dec596437b02f0c34f92ea799d6e300184a0304c1e54e462af52abeb0a8"}, - {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aace119abc495cc4ced8745e3faceb0c22e8202c60b55217405c5f389b569576"}, - {file = "aiohttp-3.12.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd749731390520a2dc1ce215bcf0ee1018c3e2e3cd834f966a02c0e71ad7d637"}, - {file = "aiohttp-3.12.11-cp313-cp313-win32.whl", hash = "sha256:65952736356d1fbc9efdd17492dce36e2501f609a14ccb298156e392d3ad8b83"}, - {file = "aiohttp-3.12.11-cp313-cp313-win_amd64.whl", hash = "sha256:854132093e12dd77f5c07975581c42ae51a6a8868dcbbb509c77d1963c3713b7"}, - {file = "aiohttp-3.12.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4f1f92cde9d9a470121a0912566585cf989f0198718477d73f3ae447a6911644"}, - {file = "aiohttp-3.12.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f36958b508e03d6c5b2ed3562f517feb415d7cc3a9b2255f319dcedb1517561a"}, - {file = "aiohttp-3.12.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06e18aaa360d59dd25383f18454f79999915d063b7675cf0ac6e7146d1f19fd1"}, - {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:019d6075bc18fdc1e47e9dabaf339c9cc32a432aca4894b55e23536919640d87"}, - {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:063b0de9936ed9b9222aa9bdf34b1cc731d34138adfc4dbb1e4bbde1ab686778"}, - {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8437e3d8041d4a0d73a48c563188d5821067228d521805906e92f25576076f95"}, - {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:340ee38cecd533b48f1fe580aa4eddfb9c77af2a80c58d9ff853b9675adde416"}, - {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f672d8dbca49e9cf9e43de934ee9fd6716740263a7e37c1a3155d6195cdef285"}, - {file = "aiohttp-3.12.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4a36ae8bebb71276f1aaadb0c08230276fdadad88fef35efab11d17f46b9885"}, - {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b63b3b5381791f96b07debbf9e2c4e909c87ecbebe4fea9dcdc82789c7366234"}, - {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:8d353c5396964a79b505450e8efbfd468b0a042b676536505e8445d9ab1ef9ae"}, - {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8ddd775457180d149ca0dbc4ebff5616948c09fa914b66785e5f23227fec5a05"}, - {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:29f642b386daf2fadccbcd2bc8a3d6541a945c0b436f975c3ce0ec318b55ad6e"}, - {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cb907dcd8899084a56bb13a74e9fdb49070aed06229ae73395f49a9ecddbd9b1"}, - {file = "aiohttp-3.12.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:760846271518d649be968cee1b245b84d348afe896792279312ca758511d798f"}, - {file = "aiohttp-3.12.11-cp39-cp39-win32.whl", hash = "sha256:d28f7d2b68f4ef4006ca92baea02aa2dce2b8160cf471e4c3566811125f5c8b9"}, - {file = "aiohttp-3.12.11-cp39-cp39-win_amd64.whl", hash = "sha256:2af98debfdfcc52cae5713bbfbfe3328fc8591c6f18c93cf3b61749de75f6ef2"}, - {file = "aiohttp-3.12.11.tar.gz", hash = "sha256:a5149ae1b11ce4cf8b122846bfa3d7c5f29fe3bfe6745ab21b3eea9615bc5564"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] - -[[package]] -name = "aioresponses" -version = "0.7.8" -description = "Mock out requests made by ClientSession from aiohttp package" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94"}, - {file = "aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11"}, -] - -[package.dependencies] -aiohttp = ">=3.3.0,<4.0.0" -packaging = ">=22.0" - -[[package]] -name = "aiosignal" -version = "1.3.2" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, - {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - -[[package]] -name = "black" -version = "24.10.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, - {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, - {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, - {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, - {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, - {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, - {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, - {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, - {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, - {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, - {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, - {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, - {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, - {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, - {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, - {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, - {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, - {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, - {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, - {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, - {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, - {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "click" -version = "8.2.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.8.2" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, - {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, - {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, - {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, - {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, - {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, - {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, - {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, - {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, - {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, - {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, - {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, - {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, - {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, - {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, - {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, - {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, - {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, - {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, - {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, - {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, - {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, - {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, - {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, - {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, - {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, - {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, - {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, - {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, - {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, - {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "frozenlist" -version = "1.6.2" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "frozenlist-1.6.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:92836b9903e52f787f4f4bfc6cf3b03cf19de4cbc09f5969e58806f876d8647f"}, - {file = "frozenlist-1.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3af419982432a13a997451e611ff7681a4fbf81dca04f70b08fc51106335ff0"}, - {file = "frozenlist-1.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1570ba58f0852a6e6158d4ad92de13b9aba3474677c3dee827ba18dcf439b1d8"}, - {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de575df0135949c4049ae42db714c43d1693c590732abc78c47a04228fc1efb"}, - {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b6eaba27ec2b3c0af7845619a425eeae8d510d5cc83fb3ef80569129238153b"}, - {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af1ee5188d2f63b4f09b67cf0c60b8cdacbd1e8d24669eac238e247d8b157581"}, - {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9179c5186eb996c0dd7e4c828858ade4d7a8d1d12dd67320675a6ae7401f2647"}, - {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38814ebc3c6bb01dc3bb4d6cffd0e64c19f4f2d03e649978aeae8e12b81bdf43"}, - {file = "frozenlist-1.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dbcab0531318fc9ca58517865fae63a2fe786d5e2d8f3a56058c29831e49f13"}, - {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7472e477dc5d6a000945f45b6e38cbb1093fdec189dc1e98e57f8ab53f8aa246"}, - {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:17c230586d47332774332af86cc1e69ee095731ec70c27e5698dfebb9db167a0"}, - {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:946a41e095592cf1c88a1fcdd154c13d0ef6317b371b817dc2b19b3d93ca0811"}, - {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d90c9b36c669eb481de605d3c2da02ea98cba6a3f5e93b3fe5881303026b2f14"}, - {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8651dd2d762d6eefebe8450ec0696cf3706b0eb5e46463138931f70c667ba612"}, - {file = "frozenlist-1.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:48400e6a09e217346949c034105b0df516a1b3c5aa546913b70b71b646caa9f5"}, - {file = "frozenlist-1.6.2-cp310-cp310-win32.whl", hash = "sha256:56354f09082262217f837d91106f1cc204dd29ac895f9bbab33244e2fa948bd7"}, - {file = "frozenlist-1.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3016ff03a332cdd2800f0eed81ca40a2699b2f62f23626e8cf81a2993867978a"}, - {file = "frozenlist-1.6.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb66c5d48b89701b93d58c31a48eb64e15d6968315a9ccc7dfbb2d6dc2c62ab7"}, - {file = "frozenlist-1.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8fb9aee4f7b495044b868d7e74fb110d8996e8fddc0bfe86409c7fc7bd5692f0"}, - {file = "frozenlist-1.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48dde536fc4d8198fad4e211f977b1a5f070e6292801decf2d6bc77b805b0430"}, - {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91dd2fb760f4a2c04b3330e0191787c3437283f9241f0b379017d4b13cea8f5e"}, - {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f01f34f8a5c7b4d74a1c65227678822e69801dcf68edd4c11417a7c83828ff6f"}, - {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f43f872cc4cfc46d9805d0e71302e9c39c755d5ad7572198cd2ceb3a291176cc"}, - {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f96cc8ab3a73d42bcdb6d9d41c3dceffa8da8273ac54b71304b891e32de8b13"}, - {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c0b257123320832cce9bea9935c860e4fa625b0e58b10db49fdfef70087df81"}, - {file = "frozenlist-1.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc4def97ccc0232f491836050ae664d3d2352bb43ad4cd34cd3399ad8d1fc8"}, - {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3663463c040315f025bd6a5f88b3748082cfe111e90fd422f71668c65de52"}, - {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:16b9e7b59ea6eef876a8a5fac084c95fd4bac687c790c4d48c0d53c6bcde54d1"}, - {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:308b40d32a98a8d0d09bc28e4cbc13a0b803a0351041d4548564f28f6b148b05"}, - {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:baf585d8968eaad6c1aae99456c40978a9fa822ccbdb36fd4746b581ef338192"}, - {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4dfdbdb671a6af6ea1a363b210373c8233df3925d9a7fb99beaa3824f6b99656"}, - {file = "frozenlist-1.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:94916e3acaeb8374d5aea9c37db777c9f0a2b9be46561f5de30064cbbbfae54a"}, - {file = "frozenlist-1.6.2-cp311-cp311-win32.whl", hash = "sha256:0453e3d2d12616949cb2581068942a0808c7255f2abab0676d2da7db30f9ea11"}, - {file = "frozenlist-1.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:fb512753c4bbf0af03f6b9c7cc5ecc9bbac2e198a94f61aaabd26c3cf3229c8c"}, - {file = "frozenlist-1.6.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48544d07404d7fcfccb6cc091922ae10de4d9e512c537c710c063ae8f5662b85"}, - {file = "frozenlist-1.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ee0cf89e7638de515c0bb2e8be30e8e2e48f3be9b6c2f7127bca4a1f35dff45"}, - {file = "frozenlist-1.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e084d838693d73c0fe87d212b91af80c18068c95c3d877e294f165056cedfa58"}, - {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d918b01781c6ebb5b776c18a87dd3016ff979eb78626aaca928bae69a640c3"}, - {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2892d9ab060a847f20fab83fdb886404d0f213f648bdeaebbe76a6134f0973d"}, - {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbd2225d7218e7d386f4953d11484b0e38e5d134e85c91f0a6b0f30fb6ae25c4"}, - {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b679187cba0a99f1162c7ec1b525e34bdc5ca246857544d16c1ed234562df80"}, - {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bceb7bd48849d4b76eac070a6d508aa3a529963f5d9b0a6840fd41fb381d5a09"}, - {file = "frozenlist-1.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b1b79ae86fdacc4bf842a4e0456540947abba64a84e61b5ae24c87adb089db"}, - {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c5c3c575148aa7308a38709906842039d7056bf225da6284b7a11cf9275ac5d"}, - {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:16263bd677a31fe1a5dc2b803b564e349c96f804a81706a62b8698dd14dbba50"}, - {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2e51b2054886ff7db71caf68285c2cd936eb7a145a509965165a2aae715c92a7"}, - {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ae1785b76f641cce4efd7e6f49ca4ae456aa230383af5ab0d4d3922a7e37e763"}, - {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:30155cc481f73f92f47ab1e858a7998f7b1207f9b5cf3b3cba90ec65a7f224f5"}, - {file = "frozenlist-1.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e1a1d82f2eb3d2875a8d139ae3f5026f7797f9de5dce44f53811ab0a883e85e7"}, - {file = "frozenlist-1.6.2-cp312-cp312-win32.whl", hash = "sha256:84105cb0f3479dfa20b85f459fb2db3b0ee52e2f84e86d447ea8b0de1fb7acdd"}, - {file = "frozenlist-1.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:eecc861bd30bc5ee3b04a1e6ebf74ed0451f596d91606843f3edbd2f273e2fe3"}, - {file = "frozenlist-1.6.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2ad8851ae1f6695d735f8646bf1e68675871789756f7f7e8dc8224a74eabb9d0"}, - {file = "frozenlist-1.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd2d5abc0ccd99a2a5b437987f3b1e9c265c1044d2855a09ac68f09bbb8082ca"}, - {file = "frozenlist-1.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15c33f665faa9b8f8e525b987eeaae6641816e0f6873e8a9c4d224338cebbb55"}, - {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e6c0681783723bb472b6b8304e61ecfcb4c2b11cf7f243d923813c21ae5d2a"}, - {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:61bae4d345a26550d0ed9f2c9910ea060f89dbfc642b7b96e9510a95c3a33b3c"}, - {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90e5a84016d0d2fb828f770ede085b5d89155fcb9629b8a3237c960c41c120c3"}, - {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55dc289a064c04819d669e6e8a85a1c0416e6c601782093bdc749ae14a2f39da"}, - {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b79bcf97ca03c95b044532a4fef6e5ae106a2dd863875b75fde64c553e3f4820"}, - {file = "frozenlist-1.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e5e7564d232a782baa3089b25a0d979e2e4d6572d3c7231fcceacc5c22bf0f7"}, - {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6fcd8d56880dccdd376afb18f483ab55a0e24036adc9a83c914d4b7bb5729d4e"}, - {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4fbce985c7fe7bafb4d9bf647c835dbe415b465a897b0c79d1bdf0f3fae5fe50"}, - {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3bd12d727cd616387d50fe283abebb2db93300c98f8ff1084b68460acd551926"}, - {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:38544cae535ed697960891131731b33bb865b7d197ad62dc380d2dbb1bceff48"}, - {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:47396898f98fae5c9b9bb409c3d2cf6106e409730f35a0926aad09dd7acf1ef5"}, - {file = "frozenlist-1.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10d835f8ce8571fd555db42d3aef325af903535dad7e6faa7b9c8abe191bffc"}, - {file = "frozenlist-1.6.2-cp313-cp313-win32.whl", hash = "sha256:a400fe775a41b6d7a3fef00d88f10cbae4f0074c9804e282013d7797671ba58d"}, - {file = "frozenlist-1.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:cc8b25b321863ed46992558a29bb09b766c41e25f31461666d501be0f893bada"}, - {file = "frozenlist-1.6.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:56de277a0e0ad26a1dcdc99802b4f5becd7fd890807b68e3ecff8ced01d58132"}, - {file = "frozenlist-1.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9cb386dd69ae91be586aa15cb6f39a19b5f79ffc1511371eca8ff162721c4867"}, - {file = "frozenlist-1.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53835d8a6929c2f16e02616f8b727bd140ce8bf0aeddeafdb290a67c136ca8ad"}, - {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc49f2277e8173abf028d744f8b7d69fe8cc26bffc2de97d47a3b529599fbf50"}, - {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:65eb9e8a973161bdac5fa06ea6bd261057947adc4f47a7a6ef3d6db30c78c5b4"}, - {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:301eb2f898d863031f8c5a56c88a6c5d976ba11a4a08a1438b96ee3acb5aea80"}, - {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207f717fd5e65fddb77d33361ab8fa939f6d89195f11307e073066886b33f2b8"}, - {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f83992722642ee0db0333b1dbf205b1a38f97d51a7382eb304ba414d8c3d1e05"}, - {file = "frozenlist-1.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12af99e6023851b36578e5bcc60618b5b30f4650340e29e565cd1936326dbea7"}, - {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6f01620444a674eaad900a3263574418e99c49e2a5d6e5330753857363b5d59f"}, - {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:82b94c8948341512306ca8ccc702771600b442c6abe5f8ee017e00e452a209e8"}, - {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:324a4cf4c220ddb3db1f46ade01e48432c63fa8c26812c710006e7f6cfba4a08"}, - {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:695284e51458dabb89af7f7dc95c470aa51fd259207aba5378b187909297feef"}, - {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:9ccbeb1c8dda4f42d0678076aa5cbde941a232be71c67b9d8ca89fbaf395807c"}, - {file = "frozenlist-1.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cbbdf62fcc1864912c592a1ec748fee94f294c6b23215d5e8e9569becb7723ee"}, - {file = "frozenlist-1.6.2-cp313-cp313t-win32.whl", hash = "sha256:76857098ee17258df1a61f934f2bae052b8542c9ea6b187684a737b2e3383a65"}, - {file = "frozenlist-1.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c06a88daba7e891add42f9278cdf7506a49bc04df9b1648be54da1bf1c79b4c6"}, - {file = "frozenlist-1.6.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99119fa5ae292ac1d3e73336ecbe3301dbb2a7f5b4e6a4594d3a6b2e240c31c1"}, - {file = "frozenlist-1.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af923dbcfd382554e960328133c2a8151706673d1280f55552b1bb914d276267"}, - {file = "frozenlist-1.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69e85175df4cc35f2cef8cb60a8bad6c5fc50e91524cd7018d73dd2fcbc70f5d"}, - {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dcdffe18c0e35ce57b3d7c1352893a3608e7578b814abb3b2a3cc15907e682"}, - {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cc228faf4533327e5f1d153217ab598648a2cd5f6b1036d82e63034f079a5861"}, - {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ee53aba5d0768e2c5c6185ec56a94bab782ef002429f293497ec5c5a3b94bdf"}, - {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3214738024afd53434614ee52aa74353a562414cd48b1771fa82fd982cb1edb"}, - {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5628e6a6f74ef1693adbe25c0bce312eb9aee82e58abe370d287794aff632d0f"}, - {file = "frozenlist-1.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7678d3e32cb3884879f10c679804c08f768df55078436fb56668f3e13e2a5e"}, - {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b776ab5217e2bf99c84b2cbccf4d30407789c0653f72d1653b5f8af60403d28f"}, - {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:b1e162a99405cb62d338f747b8625d6bd7b6794383e193335668295fb89b75fb"}, - {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2de1ddeb9dd8a07383f6939996217f0f1b2ce07f6a01d74c9adb1db89999d006"}, - {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2dcabe4e7aac889d41316c1698df0eb2565ed233b66fab6bc4a5c5b7769cad4c"}, - {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:06e28cd2ac31797e12ec8c65aa462a89116323f045e8b1930127aba9486aab24"}, - {file = "frozenlist-1.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:86f908b70043c3517f862247bdc621bd91420d40c3e90ede1701a75f025fcd5f"}, - {file = "frozenlist-1.6.2-cp39-cp39-win32.whl", hash = "sha256:2647a3d11f10014a5f9f2ca38c7fadd0dd28f5b1b5e9ce9c9d194aa5d0351c7e"}, - {file = "frozenlist-1.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:e2cbef30ba27a1d9f3e3c6aa84a60f53d907d955969cd0103b004056e28bca08"}, - {file = "frozenlist-1.6.2-py3-none-any.whl", hash = "sha256:947abfcc8c42a329bbda6df97a4b9c9cdb4e12c85153b3b57b9d2f02aa5877dc"}, - {file = "frozenlist-1.6.2.tar.gz", hash = "sha256:effc641518696471cf4962e8e32050133bc1f7b2851ae8fd0cb8797dd70dc202"}, -] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main", "dev"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "multidict" -version = "6.4.4" -description = "multidict implementation" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "multidict-6.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8adee3ac041145ffe4488ea73fa0a622b464cc25340d98be76924d0cda8545ff"}, - {file = "multidict-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b61e98c3e2a861035aaccd207da585bdcacef65fe01d7a0d07478efac005e028"}, - {file = "multidict-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75493f28dbadecdbb59130e74fe935288813301a8554dc32f0c631b6bdcdf8b0"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc3c6a37e048b5395ee235e4a2a0d639c2349dffa32d9367a42fc20d399772"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87cb72263946b301570b0f63855569a24ee8758aaae2cd182aae7d95fbc92ca7"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bbf7bd39822fd07e3609b6b4467af4c404dd2b88ee314837ad1830a7f4a8299"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1f7cbd4f1f44ddf5fd86a8675b7679176eae770f2fc88115d6dddb6cefb59bc"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5ac9e5bfce0e6282e7f59ff7b7b9a74aa8e5c60d38186a4637f5aa764046ad"}, - {file = "multidict-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4efc31dfef8c4eeb95b6b17d799eedad88c4902daba39ce637e23a17ea078915"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fcad2945b1b91c29ef2b4050f590bfcb68d8ac8e0995a74e659aa57e8d78e01"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d877447e7368c7320832acb7159557e49b21ea10ffeb135c1077dbbc0816b598"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:33a12ebac9f380714c298cbfd3e5b9c0c4e89c75fe612ae496512ee51028915f"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0f14ea68d29b43a9bf37953881b1e3eb75b2739e896ba4a6aa4ad4c5b9ffa145"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0327ad2c747a6600e4797d115d3c38a220fdb28e54983abe8964fd17e95ae83c"}, - {file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d1a20707492db9719a05fc62ee215fd2c29b22b47c1b1ba347f9abc831e26683"}, - {file = "multidict-6.4.4-cp310-cp310-win32.whl", hash = "sha256:d83f18315b9fca5db2452d1881ef20f79593c4aa824095b62cb280019ef7aa3d"}, - {file = "multidict-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:9c17341ee04545fd962ae07330cb5a39977294c883485c8d74634669b1f7fe04"}, - {file = "multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95"}, - {file = "multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a"}, - {file = "multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2"}, - {file = "multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c"}, - {file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08"}, - {file = "multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49"}, - {file = "multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529"}, - {file = "multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2"}, - {file = "multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d"}, - {file = "multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1"}, - {file = "multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740"}, - {file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e"}, - {file = "multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b"}, - {file = "multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781"}, - {file = "multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9"}, - {file = "multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf"}, - {file = "multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c"}, - {file = "multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4"}, - {file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1"}, - {file = "multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd"}, - {file = "multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373"}, - {file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156"}, - {file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c"}, - {file = "multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab"}, - {file = "multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e"}, - {file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd"}, - {file = "multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e"}, - {file = "multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb"}, - {file = "multidict-6.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:603f39bd1cf85705c6c1ba59644b480dfe495e6ee2b877908de93322705ad7cf"}, - {file = "multidict-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc60f91c02e11dfbe3ff4e1219c085695c339af72d1641800fe6075b91850c8f"}, - {file = "multidict-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:496bcf01c76a70a31c3d746fd39383aad8d685ce6331e4c709e9af4ced5fa221"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4219390fb5bf8e548e77b428bb36a21d9382960db5321b74d9d9987148074d6b"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef4e9096ff86dfdcbd4a78253090ba13b1d183daa11b973e842465d94ae1772"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49a29d7133b1fc214e818bbe025a77cc6025ed9a4f407d2850373ddde07fd04a"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e32053d6d3a8b0dfe49fde05b496731a0e6099a4df92154641c00aa76786aef5"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc403092a49509e8ef2d2fd636a8ecefc4698cc57bbe894606b14579bc2a955"}, - {file = "multidict-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5363f9b2a7f3910e5c87d8b1855c478c05a2dc559ac57308117424dfaad6805c"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e543a40e4946cf70a88a3be87837a3ae0aebd9058ba49e91cacb0b2cd631e2b"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:60d849912350da557fe7de20aa8cf394aada6980d0052cc829eeda4a0db1c1db"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:19d08b4f22eae45bb018b9f06e2838c1e4b853c67628ef8ae126d99de0da6395"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d693307856d1ef08041e8b6ff01d5b4618715007d288490ce2c7e29013c12b9a"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fad6daaed41021934917f4fb03ca2db8d8a4d79bf89b17ebe77228eb6710c003"}, - {file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c10d17371bff801af0daf8b073c30b6cf14215784dc08cd5c43ab5b7b8029bbc"}, - {file = "multidict-6.4.4-cp39-cp39-win32.whl", hash = "sha256:7e23f2f841fcb3ebd4724a40032d32e0892fbba4143e43d2a9e7695c5e50e6bd"}, - {file = "multidict-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d7b50b673ffb4ff4366e7ab43cf1f0aef4bd3608735c5fbdf0bdb6f690da411"}, - {file = "multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac"}, - {file = "multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "mypy" -version = "1.16.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, - {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, - {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"}, - {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"}, - {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"}, - {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"}, - {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"}, - {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"}, - {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"}, - {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"}, - {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"}, - {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"}, - {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"}, - {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"}, - {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"}, - {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"}, - {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"}, - {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"}, - {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"}, - {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"}, - {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"}, - {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"}, - {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"}, - {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"}, - {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"}, - {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"}, - {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"}, - {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"}, - {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"}, - {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"}, - {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"}, - {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pdoc" -version = "15.0.4" -description = "API Documentation for Python Projects" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pdoc-15.0.4-py3-none-any.whl", hash = "sha256:f9028e85e7bb8475b054e69bde1f6d26fc4693d25d9fa1b1ce9009bec7f7a5c4"}, - {file = "pdoc-15.0.4.tar.gz", hash = "sha256:cf9680f10f5b4863381f44ef084b1903f8f356acb0d4cc6b64576ba9fb712c82"}, -] - -[package.dependencies] -Jinja2 = ">=2.11.0" -MarkupSafe = ">=1.1.1" -pygments = ">=2.12.0" - -[[package]] -name = "platformdirs" -version = "4.3.8" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "propcache" -version = "0.3.1" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, - {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, - {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, - {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, - {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, - {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, - {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, - {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, - {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, - {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, - {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, - {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, - {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, - {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, - {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, - {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, - {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, - {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, -] - -[[package]] -name = "pydantic" -version = "2.11.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, - {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.33.2" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "8.4.0" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, - {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.25.3" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, - {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, -] - -[package.dependencies] -pytest = ">=8.2,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "ruff" -version = "0.8.6" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"}, - {file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"}, - {file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"}, - {file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"}, - {file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"}, - {file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"}, - {file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"}, - {file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"}, - {file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"}, -] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_full_version <= \"3.11.0a6\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "typing-extensions" -version = "4.14.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, - {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.1" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "yarl" -version = "1.20.0" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, - {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, - {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, - {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, - {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, - {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, - {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, - {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, - {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, - {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, - {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"}, - {file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"}, - {file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"}, - {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"}, - {file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"}, - {file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"}, - {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, - {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[metadata] -lock-version = "2.1" -python-versions = ">=3.10,<4.0" -content-hash = "607fefb9beb17f31d3b060a3c97b30762291c48b195789680fcd29325ae6ce20" diff --git a/pyoutlineapi/__init__.py b/pyoutlineapi/__init__.py index 19a6f08..cec4637 100644 --- a/pyoutlineapi/__init__.py +++ b/pyoutlineapi/__init__.py @@ -1,5 +1,4 @@ -""" -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. Copyright (c) 2025 Denis Rozhnovskiy All rights reserved. @@ -10,94 +9,379 @@ Source code repository: https://github.com/orenlab/pyoutlineapi -""" -from __future__ import annotations +Quick Start: -import sys -from importlib import metadata -from typing import Final, TYPE_CHECKING +```python +from pyoutlineapi import AsyncOutlineClient +# From environment variables +async with AsyncOutlineClient.from_env() as client: + server = await client.get_server_info() + print(f"Server: {server.name}") -def check_python_version(): - if sys.version_info < (3, 10): - raise RuntimeError("PyOutlineAPI requires Python 3.10 or higher") +# Prefer from_env for production usage +async with AsyncOutlineClient.from_env() as client: + keys = await client.get_access_keys() +``` +Advanced Usage - Type Hints: -check_python_version() +```python +from pyoutlineapi import ( + AsyncOutlineClient, + AuditLogger, + AuditDetails, + MetricsCollector, + MetricsTags, +) -# Core client imports -from .client import AsyncOutlineClient -from .exceptions import APIError, OutlineError +class CustomAuditLogger: + def log_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: AuditDetails | None = None, + correlation_id: str | None = None, + ) -> None: + print(f"[AUDIT] {action} on {resource}") -# Package metadata -try: - __version__: str = metadata.version("pyoutlineapi") -except metadata.PackageNotFoundError: # Fallback for development - __version__ = "0.3.0-dev" +async with AsyncOutlineClient.from_env( + audit_logger=CustomAuditLogger(), +) as client: + await client.create_access_key(name="test") +``` +""" -__author__: Final[str] = "Denis Rozhnovskiy" -__email__: Final[str] = "pytelemonbot@mail.ru" -__license__: Final[str] = "MIT" +from __future__ import annotations -# Type checking imports -if TYPE_CHECKING: - from .models import ( - AccessKey, - AccessKeyCreateRequest, - AccessKeyList, - AccessKeyNameRequest, - DataLimit, - DataLimitRequest, - ErrorResponse, - ExperimentalMetrics, - HostnameRequest, - MetricsEnabledRequest, - MetricsStatusResponse, - PortRequest, - Server, - ServerMetrics, - ServerNameRequest - ) +from importlib import metadata +from typing import TYPE_CHECKING, Final, NoReturn -# Runtime imports +# Core imports +from .audit import ( + AuditContext, + AuditLogger, + DefaultAuditLogger, + NoOpAuditLogger, + audited, + get_audit_logger, + get_or_create_audit_logger, + set_audit_logger, +) +from .base_client import MetricsCollector, NoOpMetrics, correlation_id +from .circuit_breaker import CircuitConfig, CircuitMetrics, CircuitState +from .client import ( + AsyncOutlineClient, + MultiServerManager, + create_client, + create_multi_server_manager, +) +from .common_types import ( + DEFAULT_SENSITIVE_KEYS, + AuditDetails, + ConfigOverrides, + Constants, + CredentialSanitizer, + JsonPayload, + MetricsTags, + QueryParams, + ResponseData, + SecureIDGenerator, + TimestampMs, + TimestampSec, + Validators, + build_config_overrides, + is_json_serializable, + is_valid_bytes, + is_valid_port, + mask_sensitive_data, +) +from .config import ( + DevelopmentConfig, + OutlineClientConfig, + ProductionConfig, + create_env_template, + load_config, +) +from .exceptions import ( + APIError, + CircuitOpenError, + ConfigurationError, + OutlineConnectionError, + OutlineError, + OutlineTimeoutError, + ValidationError, + format_error_chain, + get_retry_delay, + get_safe_error_dict, + is_retryable, +) from .models import ( AccessKey, AccessKeyCreateRequest, AccessKeyList, + AccessKeyMetric, AccessKeyNameRequest, + BandwidthData, + BandwidthDataValue, + BandwidthInfo, + ConnectionInfo, DataLimit, DataLimitRequest, + DataTransferred, ErrorResponse, ExperimentalMetrics, + HealthCheckResult, HostnameRequest, + LocationMetric, MetricsEnabledRequest, MetricsStatusResponse, + PeakDeviceCount, PortRequest, Server, + ServerExperimentalMetric, ServerMetrics, ServerNameRequest, + ServerSummary, + TunnelTime, ) +from .response_parser import JsonDict, ResponseParser +# Package metadata +try: + __version__: str = metadata.version("pyoutlineapi") +except metadata.PackageNotFoundError: + __version__ = "0.4.0-dev" + +__author__: Final[str] = "Denis Rozhnovskiy" +__email__: Final[str] = "pytelemonbot@mail.ru" +__license__: Final[str] = "MIT" + +# Public API __all__: Final[list[str]] = [ - # Client - "AsyncOutlineClient", - "OutlineError", + "DEFAULT_SENSITIVE_KEYS", "APIError", - # Models "AccessKey", "AccessKeyCreateRequest", "AccessKeyList", + "AccessKeyMetric", "AccessKeyNameRequest", + "AsyncOutlineClient", + "AuditContext", + "AuditLogger", + "BandwidthData", + "BandwidthDataValue", + "BandwidthInfo", + "CircuitConfig", + "CircuitMetrics", + "CircuitOpenError", + "CircuitState", + "ConfigOverrides", + "ConfigurationError", + "Constants", + "CredentialSanitizer", "DataLimit", "DataLimitRequest", + "DataTransferred", + "DefaultAuditLogger", + "DevelopmentConfig", "ErrorResponse", "ExperimentalMetrics", + "HealthCheckResult", "HostnameRequest", + "JsonDict", + "JsonPayload", + "LocationMetric", + "MetricsCollector", "MetricsEnabledRequest", "MetricsStatusResponse", + "MetricsTags", + "MultiServerManager", + "NoOpAuditLogger", + "NoOpMetrics", + "OutlineClientConfig", + "OutlineConnectionError", + "OutlineError", + "OutlineTimeoutError", + "PeakDeviceCount", "PortRequest", + "ProductionConfig", + "QueryParams", + "ResponseData", + "ResponseParser", + "SecureIDGenerator", "Server", + "ServerExperimentalMetric", "ServerMetrics", "ServerNameRequest", + "ServerSummary", + "TimestampMs", + "TimestampSec", + "TunnelTime", + "ValidationError", + "Validators", + "__author__", + "__email__", + "__license__", + "__version__", + "audited", + "build_config_overrides", + "correlation_id", + "create_client", + "create_env_template", + "create_multi_server_manager", + "format_error_chain", + "get_audit_logger", + "get_or_create_audit_logger", + "get_retry_delay", + "get_safe_error_dict", + "get_version", + "is_json_serializable", + "is_retryable", + "is_valid_bytes", + "is_valid_port", + "load_config", + "mask_sensitive_data", + "print_type_info", + "quick_setup", + "set_audit_logger", ] + + +# ===== Convenience Functions ===== + + +def get_version() -> str: + """Get package version string. + + :return: Package version + """ + return __version__ + + +def quick_setup() -> None: + """Create configuration template file for quick setup. + + Creates `.env.example` file with all available configuration options. + """ + create_env_template() + print("✅ Created .env.example") + print("📝 Edit the file with your server details") + print("🚀 Then use: AsyncOutlineClient.from_env()") + + +def print_type_info() -> None: + """Print information about available type aliases for advanced usage.""" + info = """ +🎯 PyOutlineAPI Type Aliases for Advanced Usage +=============================================== + +For creating custom AuditLogger: + from pyoutlineapi import AuditLogger, AuditDetails + + class MyAuditLogger: + def log_action( + self, + action: str, + resource: str, + *, + details: AuditDetails | None = None, + ... + ) -> None: ... + + async def alog_action( + self, + action: str, + resource: str, + *, + details: AuditDetails | None = None, + ... + ) -> None: ... + +For creating custom MetricsCollector: + from pyoutlineapi import MetricsCollector, MetricsTags + + class MyMetrics: + def increment( + self, + metric: str, + *, + tags: MetricsTags | None = None + ) -> None: ... + +Available Type Aliases: + - TimestampMs, TimestampSec # Unix timestamps + - JsonPayload, ResponseData # JSON data types + - QueryParams # URL query parameters + - AuditDetails # Audit log details + - MetricsTags # Metrics tags + +Constants and Validators: + from pyoutlineapi import Constants, Validators + + # Access constants + Constants.RETRY_STATUS_CODES + Constants.MIN_PORT, Constants.MAX_PORT + + # Use validators + Validators.validate_port(8080) + Validators.validate_key_id("my-key") + +Utility Classes: + from pyoutlineapi import ( + CredentialSanitizer, + SecureIDGenerator, + ResponseParser, + ) + + # Sanitize sensitive data + safe_url = CredentialSanitizer.sanitize(url) + + # Generate secure IDs + secure_id = SecureIDGenerator.generate() + + # Parse API responses + parsed = ResponseParser.parse(data, Model) + +📖 Documentation: https://github.com/orenlab/pyoutlineapi + """ + print(info) + + +# ===== Better Error Messages ===== + + +def __getattr__(name: str) -> NoReturn: + """Provide helpful error messages for common mistakes. + + :param name: Attribute name + :raises AttributeError: If attribute not found + """ + mistakes = { + "OutlineClient": "Use 'AsyncOutlineClient' instead", + "OutlineSettings": "Use 'OutlineClientConfig' instead", + "create_resilient_client": ( + "Use 'AsyncOutlineClient.from_env()' with 'enable_circuit_breaker=True'" + ), + } + + if name in mistakes: + raise AttributeError(f"{name} not available. {mistakes[name]}") + + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +# ===== Interactive Help ===== + +if TYPE_CHECKING: + import sys + + if hasattr(sys, "ps1"): + # Show help in interactive mode + print(f"🚀 PyOutlineAPI v{__version__}") + print("💡 Quick start: pyoutlineapi.quick_setup()") + print("🎯 Type hints: pyoutlineapi.print_type_info()") + print("📚 Help: help(pyoutlineapi.AsyncOutlineClient)") diff --git a/pyoutlineapi/api_mixins.py b/pyoutlineapi/api_mixins.py new file mode 100644 index 0000000..3755d7f --- /dev/null +++ b/pyoutlineapi/api_mixins.py @@ -0,0 +1,587 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +from typing import Protocol, cast, runtime_checkable + +from .audit import AuditLogger, audited, get_or_create_audit_logger +from .common_types import JsonPayload, QueryParams, ResponseData, Validators +from .models import ( + AccessKey, + AccessKeyCreateRequest, + AccessKeyList, + AccessKeyNameRequest, + DataLimit, + ExperimentalMetrics, + HostnameRequest, + MetricsEnabledRequest, + MetricsStatusResponse, + PortRequest, + Server, + ServerMetrics, + ServerNameRequest, +) +from .response_parser import JsonDict, ResponseParser + +# ===== Mixins for Audit Support ===== + + +class AuditableMixin: + """Mixin providing audit logger access with singleton fallback.""" + + @property + def _audit_logger(self) -> AuditLogger: + """Get audit logger with singleton fallback. + + :return: Instance logger if set, otherwise shared default logger + """ + instance_dict = self.__dict__ + if "_audit_logger_instance" in instance_dict: + return cast(AuditLogger, instance_dict["_audit_logger_instance"]) + return get_or_create_audit_logger() + + +class JsonFormattingMixin: + """Mixin for handling JSON formatting preferences.""" + + def _resolve_json_format(self, as_json: bool | None) -> bool: + """Resolve JSON format preference using priority: parameter > config > default. + + :param as_json: Explicit format preference + :return: Resolved format preference + """ + if as_json is not None: + return as_json + # Use getattr with default to avoid AttributeError overhead + return getattr(self, "_default_json_format", False) + + +# ===== HTTP Client Protocol ===== + + +@runtime_checkable +class HTTPClientProtocol(Protocol): + """Runtime-checkable protocol for HTTP client. + + Defines minimal interface needed by mixins. + """ + + async def _request( + self, + method: str, + endpoint: str, + *, + json: JsonPayload = None, + params: QueryParams | None = None, + ) -> ResponseData: + """Internal request method. + + :param method: HTTP method + :param endpoint: API endpoint + :param json: Request JSON payload + :param params: Query parameters + :return: Response data + """ + ... # pragma: no cover + + def _resolve_json_format(self, as_json: bool | None) -> bool: + """Resolve JSON format preference. + + :param as_json: Explicit format preference + :return: Resolved format preference + """ + ... # pragma: no cover + + +# ===== Server Management Mixin ===== + + +class ServerMixin(AuditableMixin, JsonFormattingMixin): + """Server management operations. + + API Endpoints (based on OpenAPI schema): + - GET /server + - PUT /name + - PUT /server/hostname-for-access-keys + - PUT /server/port-for-new-access-keys + """ + + __slots__ = () + + async def get_server_info( + self: HTTPClientProtocol, + *, + as_json: bool | None = None, + ) -> Server | JsonDict: + """Get server information and configuration. + + Based on OpenAPI: GET /server + + :param as_json: Return raw JSON instead of model + :return: Server information + """ + data = await self._request("GET", "server") + return ResponseParser.parse( + data, Server, as_json=self._resolve_json_format(as_json) + ) + + @audited() + async def rename_server(self: HTTPClientProtocol, name: str) -> bool: + """Rename the server. + + Based on OpenAPI: PUT /name + + :param name: New server name + :return: True if successful + :raises ValueError: If name is empty + """ + validated_name = Validators.validate_name(name) + + request = ServerNameRequest(name=validated_name) + data = await self._request( + "PUT", "name", json=request.model_dump(by_alias=True) + ) + return ResponseParser.parse_simple(data) + + @audited() + async def set_hostname(self: HTTPClientProtocol, hostname: str) -> bool: + """Set hostname for access keys. + + Based on OpenAPI: PUT /server/hostname-for-access-keys + + :param hostname: Hostname to set + :return: True if successful + :raises ValueError: If hostname is empty + """ + if not hostname or not hostname.strip(): + msg = "Hostname cannot be empty" + raise ValueError(msg) + + sanitized_hostname = hostname.strip() + request = HostnameRequest(hostname=sanitized_hostname) + + data = await self._request( + "PUT", + "server/hostname-for-access-keys", + json=request.model_dump(by_alias=True), + ) + return ResponseParser.parse_simple(data) + + @audited() + async def set_default_port(self: HTTPClientProtocol, port: int) -> bool: + """Set default port for new access keys. + + Based on OpenAPI: PUT /server/port-for-new-access-keys + + :param port: Port number (1-65535) + :return: True if successful + :raises ValueError: If port is invalid + """ + validated_port = Validators.validate_port(port) + request = PortRequest(port=validated_port) + + data = await self._request( + "PUT", + "server/port-for-new-access-keys", + json=request.model_dump(by_alias=True), + ) + return ResponseParser.parse_simple(data) + + +# ===== Access Key Management Mixin ===== + + +class AccessKeyMixin(AuditableMixin, JsonFormattingMixin): + """Access key management operations. + + API Endpoints (based on OpenAPI schema): + - POST /access-keys + - PUT /access-keys/{id} + - GET /access-keys + - GET /access-keys/{id} + - DELETE /access-keys/{id} + - PUT /access-keys/{id}/name + - PUT /access-keys/{id}/data-limit + - DELETE /access-keys/{id}/data-limit + """ + + __slots__ = () + + @audited() + async def create_access_key( + self: HTTPClientProtocol, + *, + name: str | None = None, + password: str | None = None, + port: int | None = None, + method: str | None = None, + limit: DataLimit | None = None, + as_json: bool | None = None, + ) -> AccessKey | JsonDict: + """Create new access key with auto-generated ID. + + Based on OpenAPI: POST /access-keys + + :param name: Key name + :param password: Key password + :param port: Custom port + :param method: Encryption method + :param limit: Data transfer limit + :param as_json: Return raw JSON instead of model + :return: Created access key + """ + validated_name = Validators.validate_name(name) if name is not None else None + validated_port = Validators.validate_port(port) if port is not None else None + + request = AccessKeyCreateRequest( + name=validated_name, + password=password, + port=validated_port, + method=method, + limit=limit, + ) + + payload = request.model_dump(by_alias=True, exclude_none=True) + data = await self._request("POST", "access-keys", json=payload) + + return ResponseParser.parse( + data, AccessKey, as_json=self._resolve_json_format(as_json) + ) + + @audited() + async def create_access_key_with_id( + self: HTTPClientProtocol, + key_id: str, + *, + name: str | None = None, + password: str | None = None, + port: int | None = None, + method: str | None = None, + limit: DataLimit | None = None, + as_json: bool | None = None, + ) -> AccessKey | JsonDict: + """Create new access key with specific ID. + + Based on OpenAPI: PUT /access-keys/{id} + + :param key_id: Desired access key ID + :param name: Key name + :param password: Key password + :param port: Custom port + :param method: Encryption method + :param limit: Data transfer limit + :param as_json: Return raw JSON instead of model + :return: Created access key + :raises ValueError: If key_id is invalid + """ + validated_key_id = Validators.validate_key_id(key_id) + + validated_name = Validators.validate_name(name) if name is not None else None + validated_port = Validators.validate_port(port) if port is not None else None + + request = AccessKeyCreateRequest( + name=validated_name, + password=password, + port=validated_port, + method=method, + limit=limit, + ) + + payload = request.model_dump(by_alias=True, exclude_none=True) + data = await self._request( + "PUT", f"access-keys/{validated_key_id}", json=payload + ) + + return ResponseParser.parse( + data, AccessKey, as_json=self._resolve_json_format(as_json) + ) + + async def get_access_keys( + self: HTTPClientProtocol, + *, + as_json: bool | None = None, + ) -> AccessKeyList | JsonDict: + """Get list of all access keys. + + Based on OpenAPI: GET /access-keys + + :param as_json: Return raw JSON instead of model + :return: List of access keys + """ + data = await self._request("GET", "access-keys") + return ResponseParser.parse( + data, AccessKeyList, as_json=self._resolve_json_format(as_json) + ) + + async def get_access_key( + self: HTTPClientProtocol, + key_id: str, + *, + as_json: bool | None = None, + ) -> AccessKey | JsonDict: + """Get specific access key by ID. + + Based on OpenAPI: GET /access-keys/{id} + + :param key_id: Access key ID + :param as_json: Return raw JSON instead of model + :return: Access key + :raises ValueError: If key_id is invalid + """ + validated_key_id = Validators.validate_key_id(key_id) + + data = await self._request("GET", f"access-keys/{validated_key_id}") + return ResponseParser.parse( + data, AccessKey, as_json=self._resolve_json_format(as_json) + ) + + @audited() + async def delete_access_key(self: HTTPClientProtocol, key_id: str) -> bool: + """Delete access key. + + Based on OpenAPI: DELETE /access-keys/{id} + + :param key_id: Access key ID + :return: True if successful + :raises ValueError: If key_id is invalid + """ + validated_key_id = Validators.validate_key_id(key_id) + + data = await self._request("DELETE", f"access-keys/{validated_key_id}") + return ResponseParser.parse_simple(data) + + @audited() + async def rename_access_key( + self: HTTPClientProtocol, + key_id: str, + name: str, + ) -> bool: + """Rename access key. + + Based on OpenAPI: PUT /access-keys/{id}/name + + :param key_id: Access key ID + :param name: New name + :return: True if successful + :raises ValueError: If key_id or name is invalid + """ + validated_key_id = Validators.validate_key_id(key_id) + validated_name = Validators.validate_name(name) + + request = AccessKeyNameRequest(name=validated_name) + data = await self._request( + "PUT", + f"access-keys/{validated_key_id}/name", + json=request.model_dump(by_alias=True), + ) + return ResponseParser.parse_simple(data) + + @audited() + async def set_access_key_data_limit( + self: HTTPClientProtocol, + key_id: str, + limit: DataLimit, + ) -> bool: + """Set data limit for specific access key. + + Based on OpenAPI: PUT /access-keys/{id}/data-limit + + :param key_id: Access key ID + :param limit: Data transfer limit + :return: True if successful + :raises ValueError: If key_id is invalid + """ + validated_key_id = Validators.validate_key_id(key_id) + + # Fix: Wrap payload in "limit" key as required by API + payload = cast(JsonDict, {"limit": limit.model_dump(by_alias=True)}) + + data = await self._request( + "PUT", + f"access-keys/{validated_key_id}/data-limit", + json=payload, + ) + return ResponseParser.parse_simple(data) + + @audited() + async def remove_access_key_data_limit( + self: HTTPClientProtocol, + key_id: str, + ) -> bool: + """Remove data limit from access key. + + Based on OpenAPI: DELETE /access-keys/{id}/data-limit + + :param key_id: Access key ID + :return: True if successful + :raises ValueError: If key_id is invalid + """ + validated_key_id = Validators.validate_key_id(key_id) + + data = await self._request( + "DELETE", f"access-keys/{validated_key_id}/data-limit" + ) + return ResponseParser.parse_simple(data) + + +# ===== Data Limit Mixin ===== + + +class DataLimitMixin(AuditableMixin): + """Global data limit operations. + + API Endpoints (based on OpenAPI schema): + - PUT /server/access-key-data-limit + - DELETE /server/access-key-data-limit + """ + + __slots__ = () + + @audited() + async def set_global_data_limit( + self: HTTPClientProtocol, + limit: DataLimit, + ) -> bool: + """Set global data limit for all access keys. + + Based on OpenAPI: PUT /server/access-key-data-limit + + :param limit: Data transfer limit + :return: True if successful + """ + # Fix: Wrap payload in "limit" key as required by API + payload = cast(JsonDict, {"limit": limit.model_dump(by_alias=True)}) + + data = await self._request( + "PUT", + "server/access-key-data-limit", + json=payload, + ) + return ResponseParser.parse_simple(data) + + @audited() + async def remove_global_data_limit(self: HTTPClientProtocol) -> bool: + """Remove global data limit. + + Based on OpenAPI: DELETE /server/access-key-data-limit + + :return: True if successful + """ + data = await self._request("DELETE", "server/access-key-data-limit") + return ResponseParser.parse_simple(data) + + +# ===== Metrics Mixin ===== + + +class MetricsMixin(AuditableMixin, JsonFormattingMixin): + """Metrics operations. + + API Endpoints (based on OpenAPI schema): + - GET /metrics/enabled + - PUT /metrics/enabled + - GET /metrics/transfer + - GET /experimental/server/metrics + """ + + __slots__ = () + + async def get_metrics_status( + self: HTTPClientProtocol, + *, + as_json: bool | None = None, + ) -> MetricsStatusResponse | JsonDict: + """Get metrics collection status. + + Based on OpenAPI: GET /metrics/enabled + + :param as_json: Return raw JSON instead of model + :return: Metrics status + """ + data = await self._request("GET", "metrics/enabled") + return ResponseParser.parse( + data, MetricsStatusResponse, as_json=self._resolve_json_format(as_json) + ) + + @audited() + async def set_metrics_status(self: HTTPClientProtocol, enabled: bool) -> bool: + """Enable or disable metrics collection. + + Based on OpenAPI: PUT /metrics/enabled + + :param enabled: True to enable, False to disable + :return: True if successful + :raises ValueError: If enabled is invalid + """ + request = MetricsEnabledRequest(metricsEnabled=enabled) + data = await self._request( + "PUT", + "metrics/enabled", + json=request.model_dump(by_alias=True), + ) + return ResponseParser.parse_simple(data) + + async def get_transfer_metrics( + self: HTTPClientProtocol, + *, + as_json: bool | None = None, + ) -> ServerMetrics | JsonDict: + """Get transfer metrics for all access keys. + + Based on OpenAPI: GET /metrics/transfer + + :param as_json: Return raw JSON instead of model + :return: Transfer metrics + """ + data = await self._request("GET", "metrics/transfer") + return ResponseParser.parse( + data, ServerMetrics, as_json=self._resolve_json_format(as_json) + ) + + async def get_experimental_metrics( + self: HTTPClientProtocol, + since: str, + *, + as_json: bool | None = None, + ) -> ExperimentalMetrics | JsonDict: + """Get experimental server metrics. + + Based on OpenAPI: GET /experimental/server/metrics + + :param since: Time period (e.g., '24h', '7d') or ISO-8601 timestamp + :param as_json: Return raw JSON instead of model + :return: Experimental metrics + :raises ValueError: If since parameter is invalid + """ + sanitized_since = Validators.validate_since(since) + + data = await self._request( + "GET", + "experimental/server/metrics", + params={"since": sanitized_since}, + ) + return ResponseParser.parse( + data, ExperimentalMetrics, as_json=self._resolve_json_format(as_json) + ) + + +__all__ = [ + "AccessKeyMixin", + "AuditableMixin", + "DataLimitMixin", + "HTTPClientProtocol", + "JsonFormattingMixin", + "MetricsMixin", + "ServerMixin", +] diff --git a/pyoutlineapi/audit.py b/pyoutlineapi/audit.py new file mode 100644 index 0000000..25494c4 --- /dev/null +++ b/pyoutlineapi/audit.py @@ -0,0 +1,821 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import asyncio +import contextvars +import inspect +import logging +import time +from contextlib import suppress +from dataclasses import dataclass, field +from functools import wraps +from typing import ( + TYPE_CHECKING, + Any, + ParamSpec, + Protocol, + TypeVar, + cast, + runtime_checkable, +) +from weakref import WeakValueDictionary + +from .common_types import DEFAULT_SENSITIVE_KEYS + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + +logger = logging.getLogger(__name__) + +# Type variables +P = ParamSpec("P") +R = TypeVar("R") + +_audit_logger_context: contextvars.ContextVar[AuditLogger | None] = ( + contextvars.ContextVar("audit_logger", default=None) +) + +_logger_cache: WeakValueDictionary[int, AuditLogger] = WeakValueDictionary() + + +# ===== Audit Context ===== + + +@dataclass(slots=True, frozen=True) +class AuditContext: + """Immutable audit context extracted from function call. + + Uses structural pattern matching and signature inspection for smart extraction. + """ + + action: str + resource: str + success: bool + details: dict[str, Any] = field(default_factory=dict) + correlation_id: str | None = None + + @classmethod + def from_call( + cls, + func: Callable[..., Any], + instance: object, + args: tuple[Any, ...], + kwargs: dict[str, Any], + result: object = None, + exception: Exception | None = None, + ) -> AuditContext: + """Build audit context from function call with intelligent extraction. + + :param func: Function being audited + :param instance: Instance (self) for methods + :param args: Positional arguments + :param kwargs: Keyword arguments + :param result: Function result (if successful) + :param exception: Exception (if failed) + :return: Complete audit context + """ + success = exception is None + + # Extract action from function name (snake_case -> action) + action = func.__name__ + + # Smart resource extraction + resource = cls._extract_resource(func, args, kwargs, result, success) + + # Smart details extraction with automatic sanitization + details = cls._extract_details(func, args, kwargs, result, exception, success) + + # Correlation ID from instance if available + correlation_id = getattr(instance, "_correlation_id", None) + + return cls( + action=action, + resource=resource, + success=success, + details=details, + correlation_id=correlation_id, + ) + + @staticmethod + def _extract_resource( + func: Callable[..., Any], + args: tuple[Any, ...], + kwargs: dict[str, Any], + result: object, + success: bool, + ) -> str: + """Smart resource extraction using structural pattern matching. + + Priority: + 1. result.id (for create operations) + 2. Known resource parameter names (key_id, id, resource_id) + 3. First meaningful argument + 4. Function name analysis + 5. 'unknown' fallback + + :param func: Function being audited + :param args: Positional arguments + :param kwargs: Keyword arguments + :param result: Function result + :param success: Whether operation succeeded + :return: Resource identifier + """ + # Pattern 1: Extract from successful result + if success and result is not None: + match result: + case _ if hasattr(result, "id"): + return str(result.id) + case dict() if "id" in result: + return str(result["id"]) + + # Pattern 2: Extract from known parameter names + sig = inspect.signature(func) + params = list(sig.parameters.keys()) + + # Skip 'self' and 'cls' + params = [p for p in params if p not in ("self", "cls")] + + # Try common resource identifiers in priority order + for resource_param in ("key_id", "id", "resource_id", "user_id", "name"): + if resource_param in kwargs: + return str(kwargs[resource_param]) + + # Pattern 3: First meaningful parameter + if params and params[0] in kwargs: + return str(kwargs[params[0]]) + + # Pattern 4: First positional argument (after self) + if args: + return str(args[0]) + + # Pattern 5: Analyze function name for hints + func_name = func.__name__.lower() + if any(keyword in func_name for keyword in ("server", "global", "system")): + return "server" + + return "unknown" + + @staticmethod + def _extract_details( + func: Callable[..., Any], + args: tuple[Any, ...], + kwargs: dict[str, Any], + result: object, + exception: Exception | None, + success: bool, + ) -> dict[str, Any]: + """Smart details extraction using signature introspection. + + Only includes meaningful parameters (excludes technical ones and None values). + Automatically sanitizes sensitive data. + + :param func: Function being audited + :param args: Positional arguments + :param kwargs: Keyword arguments + :param result: Function result + :param exception: Exception if failed + :param success: Whether operation succeeded + :return: Sanitized details dictionary + """ + details: dict[str, Any] = {"success": success} + + # Signature-based extraction + sig = inspect.signature(func) + + # Parameters to exclude from details + excluded = {"self", "cls", "as_json", "return_raw"} + + for param_name, param in sig.parameters.items(): + if param_name in excluded: + continue + + # Get actual value + value = kwargs.get(param_name) + + # Only include meaningful values (not None, not default) + if value is not None and value != param.default: + # Convert complex objects to simple representations + match value: + case _ if hasattr(value, "model_dump"): + # Pydantic models + details[param_name] = value.model_dump(exclude_none=True) + case dict(): + details[param_name] = value + case list() | tuple(): + details[param_name] = len(value) # Count, not content + case _: + details[param_name] = value + + # Add error information if present + if exception: + details["error"] = str(exception) + details["error_type"] = type(exception).__name__ + + # Sanitize sensitive data + return _sanitize_details(details) + + +# ===== Audit Logger Protocol ===== + + +@runtime_checkable +class AuditLogger(Protocol): + """Protocol for audit logging implementations. + + Designed for async-first applications with sync fallback support. + """ + + async def alog_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + """Log auditable action asynchronously (primary method).""" + ... # pragma: no cover + + def log_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + """Log auditable action synchronously (fallback method).""" + ... # pragma: no cover + + async def shutdown(self) -> None: + """Gracefully shutdown logger.""" + ... # pragma: no cover + + +# ===== Default Implementation ===== + + +class DefaultAuditLogger: + """Async audit logger with batching and backpressure handling.""" + + __slots__ = ( + "_batch_size", + "_batch_timeout", + "_lock", + "_queue", + "_queue_size", + "_shutdown_event", + "_task", + ) + + def __init__( + self, + *, + queue_size: int = 10000, + batch_size: int = 100, + batch_timeout: float = 1.0, + ) -> None: + """Initialize audit logger with batching support. + + :param queue_size: Maximum queue size (backpressure protection) + :param batch_size: Maximum batch size for processing + :param batch_timeout: Maximum time to wait for batch completion (seconds) + """ + self._queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=queue_size) + self._queue_size = queue_size + self._batch_size = batch_size + self._batch_timeout = batch_timeout + self._task: asyncio.Task[None] | None = None + self._shutdown_event = asyncio.Event() + self._lock = asyncio.Lock() + + async def alog_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + """Log auditable action asynchronously with automatic batching. + + :param action: Action being performed + :param resource: Resource identifier + :param user: User performing the action (optional) + :param details: Additional structured details (optional) + :param correlation_id: Request correlation ID (optional) + """ + if self._shutdown_event.is_set(): + # Fallback to sync logging during shutdown + return self.log_action( + action, + resource, + user=user, + details=details, + correlation_id=correlation_id, + ) + + # Ensure background task is running + await self._ensure_task_running() + + # Build log entry + entry = self._build_entry(action, resource, user, details, correlation_id) + + # Try to enqueue, handle backpressure + try: + self._queue.put_nowait(entry) + except asyncio.QueueFull: + # Backpressure: log warning and use sync fallback + if logger.isEnabledFor(logging.WARNING): + logger.warning( + "[AUDIT] Queue full (%d items), using sync fallback", + self._queue_size, + ) + self.log_action( + action, + resource, + user=user, + details=details, + correlation_id=correlation_id, + ) + + def log_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + """Log auditable action synchronously (fallback method). + + :param action: Action being performed + :param resource: Resource identifier + :param user: User performing the action (optional) + :param details: Additional structured details (optional) + :param correlation_id: Request correlation ID (optional) + """ + entry = self._build_entry(action, resource, user, details, correlation_id) + self._write_log(entry) + + async def _ensure_task_running(self) -> None: + """Ensure background processing task is running (lazy start with lock).""" + if self._task is not None and not self._task.done(): + return + + async with self._lock: + # Double-check after acquiring lock + if self._task is None or self._task.done(): + self._task = asyncio.create_task( + self._process_queue(), name="audit-logger" + ) + + async def _process_queue(self) -> None: + """Background task for processing audit logs in batches. + + Uses batching for improved throughput and reduced I/O overhead. + """ + batch: list[dict[str, Any]] = [] + + try: + while not self._shutdown_event.is_set(): + try: + # Wait for item with timeout for batch processing + entry = await asyncio.wait_for( + self._queue.get(), timeout=self._batch_timeout + ) + batch.append(entry) + + # Process batch when size reached or queue empty + if len(batch) >= self._batch_size or self._queue.empty(): + self._write_batch(batch) + batch.clear() + + self._queue.task_done() + + except asyncio.TimeoutError: + # Timeout: flush partial batch if any + if batch: + self._write_batch(batch) + batch.clear() + + except asyncio.CancelledError: + # Flush remaining batch on cancellation + if batch: + self._write_batch(batch) + raise + finally: + if logger.isEnabledFor(logging.DEBUG): + logger.debug("[AUDIT] Queue processor stopped") + + def _write_batch(self, batch: list[dict[str, Any]]) -> None: + """Write batch of log entries efficiently. + + :param batch: Batch of log entries to write + """ + for entry in batch: + self._write_log(entry) + + def _write_log(self, entry: dict[str, Any]) -> None: + """Write single log entry to logger. + + :param entry: Log entry to write + """ + message = self._format_message(entry) + logger.info(message, extra=entry) + + @staticmethod + def _build_entry( + action: str, + resource: str, + user: str | None, + details: dict[str, Any] | None, + correlation_id: str | None, + ) -> dict[str, Any]: + """Build structured log entry with sanitization. + + :param action: Action being performed + :param resource: Resource identifier + :param user: User performing action + :param details: Additional details + :param correlation_id: Correlation ID + :return: Structured log entry + """ + entry: dict[str, Any] = { + "action": action, + "resource": resource, + "timestamp": time.time(), + "is_audit": True, + } + + if user is not None: + entry["user"] = user + if correlation_id is not None: + entry["correlation_id"] = correlation_id + if details is not None: + entry["details"] = _sanitize_details(details) + + return entry + + @staticmethod + def _format_message(entry: dict[str, Any]) -> str: + """Format audit log message for human readability. + + :param entry: Log entry + :return: Formatted message + """ + action = entry["action"] + resource = entry["resource"] + user = entry.get("user") + correlation_id = entry.get("correlation_id") + + parts = ["[AUDIT]", action, "on", resource] + + if user: + parts.extend(["by", user]) + if correlation_id: + parts.append(f"[{correlation_id}]") + + return " ".join(parts) + + async def shutdown(self, *, timeout: float = 5.0) -> None: + """Gracefully shutdown audit logger with queue draining. + + :param timeout: Maximum time to wait for queue to drain (seconds) + """ + async with self._lock: + if self._shutdown_event.is_set(): + return + + self._shutdown_event.set() + + if logger.isEnabledFor(logging.DEBUG): + logger.debug("[AUDIT] Shutting down, draining queue") + + # Wait for queue to drain + try: + await asyncio.wait_for(self._queue.join(), timeout=timeout) + except asyncio.TimeoutError: + remaining = self._queue.qsize() + if logger.isEnabledFor(logging.WARNING): + logger.warning( + "[AUDIT] Queue did not drain within %ss, %d items remaining", + timeout, + remaining, + ) + + # Cancel processing task + if self._task and not self._task.done(): + self._task.cancel() + with suppress(asyncio.CancelledError): + await self._task + + if logger.isEnabledFor(logging.DEBUG): + logger.debug("[AUDIT] Shutdown complete") + + +# ===== No-Op Implementation ===== + + +class NoOpAuditLogger: + """Zero-overhead no-op audit logger. + + Implements AuditLogger protocol but performs no operations. + Useful for disabling audit without code changes or performance impact. + """ + + __slots__ = () + + async def alog_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + """No-op async log.""" + + def log_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + """No-op sync log.""" + + async def shutdown(self) -> None: + """No-op shutdown.""" + + +# ===== Professional Audit Decorator ===== + + +def audited( + *, + log_success: bool = True, + log_failure: bool = True, +) -> Callable[[Callable[P, R]], Callable[P, R]]: + """Audit logging decorator with zero-config smart extraction. + + Automatically extracts ALL information from function signature and execution: + - Action name: from function name + - Resource: from result.id, first parameter, or function analysis + - Details: from function signature (excluding None and defaults) + - Correlation ID: from instance._correlation_id if available + - Success/failure: from exception handling + + Usage: + @audited() + async def create_access_key(self, name: str, port: int = 8080) -> AccessKey: + # action: "create_access_key" + # resource: result.id + # details: {"name": "...", "port": 8080} (if not default) + ... + + @audited(log_success=False) + async def critical_operation(self, resource_id: str) -> bool: + # Only logs failures for alerting + ... + + :param log_success: Log successful operations (default: True) + :param log_failure: Log failed operations (default: True) + :return: Decorated function with automatic audit logging + """ + + def decorator(func: Callable[P, R]) -> Callable[P, R]: + # Determine if function is async at decoration time + is_async = inspect.iscoroutinefunction(func) + + if is_async: + async_func = cast("Callable[P, Awaitable[object]]", func) + + @wraps(func) + async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> object: + # Check for audit logger on instance + instance = args[0] if args else None + audit_logger = getattr(instance, "_audit_logger", None) + + # No logger? Execute without audit + if audit_logger is None: + return await async_func(*args, **kwargs) + + result: object | None = None + + try: + result = await async_func(*args, **kwargs) + except Exception as e: + if log_failure: + ctx = AuditContext.from_call( + func=func, + instance=instance, + args=args, + kwargs=kwargs, + result=result, + exception=e, + ) + task = asyncio.create_task( + audit_logger.alog_action( + action=ctx.action, + resource=ctx.resource, + details=ctx.details, + correlation_id=ctx.correlation_id, + ) + ) + task.add_done_callback(lambda t: t.exception()) + raise + else: + if log_success: + ctx = AuditContext.from_call( + func=func, + instance=instance, + args=args, + kwargs=kwargs, + result=result, + exception=None, + ) + task = asyncio.create_task( + audit_logger.alog_action( + action=ctx.action, + resource=ctx.resource, + details=ctx.details, + correlation_id=ctx.correlation_id, + ) + ) + task.add_done_callback(lambda t: t.exception()) + return result + + return cast("Callable[P, R]", async_wrapper) + + else: + sync_func = cast("Callable[P, object]", func) + + @wraps(func) + def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> object: + # Check for audit logger on instance + instance = args[0] if args else None + audit_logger = getattr(instance, "_audit_logger", None) + + # No logger? Execute without audit + if audit_logger is None: + return sync_func(*args, **kwargs) + + result: object | None = None + + try: + result = sync_func(*args, **kwargs) + except Exception as e: + if log_failure: + ctx = AuditContext.from_call( + func=func, + instance=instance, + args=args, + kwargs=kwargs, + result=result, + exception=e, + ) + audit_logger.log_action( + action=ctx.action, + resource=ctx.resource, + details=ctx.details, + correlation_id=ctx.correlation_id, + ) + raise + else: + if log_success: + ctx = AuditContext.from_call( + func=func, + instance=instance, + args=args, + kwargs=kwargs, + result=result, + exception=None, + ) + audit_logger.log_action( + action=ctx.action, + resource=ctx.resource, + details=ctx.details, + correlation_id=ctx.correlation_id, + ) + return result + + return cast("Callable[P, R]", sync_wrapper) + + return decorator + + +# ===== Sanitization ===== + + +def _sanitize_details(details: dict[str, Any]) -> dict[str, Any]: + """Recursively sanitize sensitive data using lazy copy-on-write. + + :param details: Dictionary to sanitize + :return: Sanitized dictionary (maybe same instance if no changes) + """ + if not details: + return details + + # Pre-compute lowercase sensitive keys for performance + sensitive_keys = {k.lower() for k in DEFAULT_SENSITIVE_KEYS} + sanitized: dict[str, Any] | None = None + + for key, value in details.items(): + # Check if key contains sensitive pattern + if any(pattern in key.lower() for pattern in sensitive_keys): + # Lazy copy on first modification + if sanitized is None: + sanitized = dict(details) + sanitized[key] = "***REDACTED***" + continue + + # Recursively sanitize nested dicts + if isinstance(value, dict): + nested = _sanitize_details(value) + if nested is not value: # Only copy if changed + if sanitized is None: + sanitized = dict(details) + sanitized[key] = nested + + return sanitized or details + + +# ===== Context-based Logger Management ===== + + +def set_audit_logger(logger_instance: AuditLogger) -> None: + """Set audit logger for current async context. + + Thread-safe and async-safe using contextvars. + Preferred over global state for high-load applications. + + :param logger_instance: Audit logger instance + """ + _audit_logger_context.set(logger_instance) + + +def get_audit_logger() -> AuditLogger | None: + """Get audit logger from current context. + + :return: Audit logger instance or None + """ + return _audit_logger_context.get() + + +def get_or_create_audit_logger(instance_id: int | None = None) -> AuditLogger: + """Get or create audit logger with weak reference caching. + + :param instance_id: Instance ID for caching (optional) + :return: Audit logger instance + """ + # Try context first + ctx_logger = _audit_logger_context.get() + if ctx_logger is not None: + return ctx_logger + + # Try cache if instance_id provided + if instance_id is not None: + cached = _logger_cache.get(instance_id) + if cached is not None: + return cached + + # Create new logger + logger_instance = DefaultAuditLogger() + + # Cache if instance_id provided + if instance_id is not None: + _logger_cache[instance_id] = cast(AuditLogger, logger_instance) + + return cast(AuditLogger, logger_instance) + + +__all__ = [ + "AuditContext", + "AuditLogger", + "DefaultAuditLogger", + "NoOpAuditLogger", + "audited", + "get_audit_logger", + "get_or_create_audit_logger", + "set_audit_logger", +] diff --git a/pyoutlineapi/base_client.py b/pyoutlineapi/base_client.py new file mode 100644 index 0000000..23257c6 --- /dev/null +++ b/pyoutlineapi/base_client.py @@ -0,0 +1,1164 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import secrets +import ssl +import time +from asyncio import Semaphore +from contextlib import suppress +from contextvars import ContextVar +from typing import TYPE_CHECKING, Protocol, cast +from urllib.parse import urlparse + +import aiohttp +from aiohttp import ClientResponse + +from .audit import AuditLogger, NoOpAuditLogger +from .common_types import ( + Constants, + CredentialSanitizer, + JsonPayload, + MetricsTags, + QueryParams, + ResponseData, + SecureIDGenerator, + SSRFProtection, + Validators, +) +from .exceptions import ( + APIError, + CircuitOpenError, + OutlineConnectionError, + OutlineTimeoutError, +) + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + from types import SimpleNamespace + + from aiohttp import ( + ClientSession, + TraceConnectionCreateEndParams, + TraceConnectionReuseconnParams, + ) + from pydantic import SecretStr + + from .circuit_breaker import CircuitBreaker, CircuitConfig + +logger = logging.getLogger(__name__) + +# Context variable for correlation ID tracking (thread-safe) +correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") + + +class MetricsCollector(Protocol): + """Protocol for metrics collection. + + Allows dependency injection of custom metrics backends. + """ + + def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None: + """Increment counter metric.""" + ... # pragma: no cover + + def timing( + self, metric: str, value: float, *, tags: MetricsTags | None = None + ) -> None: + """Record timing metric.""" + ... # pragma: no cover + + def gauge( + self, metric: str, value: float, *, tags: MetricsTags | None = None + ) -> None: + """Set gauge metric.""" + ... # pragma: no cover + + +class NoOpMetrics: + """No-op metrics collector (zero-overhead default). + + Uses __slots__ to minimize memory footprint. + """ + + __slots__ = () + + def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None: + """No-op increment (zero overhead).""" + + def timing( + self, metric: str, value: float, *, tags: MetricsTags | None = None + ) -> None: + """No-op timing (zero overhead).""" + + def gauge( + self, metric: str, value: float, *, tags: MetricsTags | None = None + ) -> None: + """No-op gauge (zero overhead).""" + + +class TokenBucketRateLimiter: + """Token bucket algorithm for requests-per-second rate limiting.""" + + __slots__ = ("_capacity", "_last_update", "_lock", "_rate", "_tokens") + + def __init__( + self, + rate: float = Constants.DEFAULT_RATE_LIMIT_RPS, + capacity: int = Constants.DEFAULT_RATE_LIMIT_BURST, + ) -> None: + """Initialize rate limiter. + + :param rate: Tokens per second (requests/second) + :param capacity: Maximum burst capacity + :raises ValueError: If parameters are invalid + """ + if rate <= 0: + raise ValueError("Rate must be positive") + if capacity <= 0: + raise ValueError("Capacity must be positive") + + self._rate: float = rate + self._capacity: int = capacity + self._tokens: float = float(capacity) + self._last_update: float = time.monotonic() + self._lock: asyncio.Lock = asyncio.Lock() + + async def acquire(self, tokens: float = 1.0) -> None: + """Acquire tokens, waiting if necessary. + + Uses monotonic clock for accurate timing. + + :param tokens: Number of tokens to acquire + """ + async with self._lock: + # Cache loop reference (minor optimization) + now = time.monotonic() + elapsed = now - self._last_update + + # Refill tokens based on elapsed time (O(1) calculation) + self._tokens = min(self._capacity, self._tokens + elapsed * self._rate) + self._last_update = now + + # Wait if not enough tokens + if self._tokens < tokens: + wait_time = (tokens - self._tokens) / self._rate + await asyncio.sleep(wait_time) + self._tokens = 0.0 + else: + self._tokens -= tokens + + @property + def available_tokens(self) -> float: + """Get currently available tokens (approximate, lock-free). + + Lock-free read for minimal overhead. May be slightly stale. + + :return: Number of available tokens + """ + now = time.monotonic() + elapsed = now - self._last_update + return min(self._capacity, self._tokens + elapsed * self._rate) + + +class RateLimiter: + """Concurrent request limiter with dynamic limit adjustment. + + Uses Semaphore for efficient concurrent limiting. + """ + + __slots__ = ("_limit", "_lock", "_semaphore") + + def __init__(self, limit: int) -> None: + """Initialize rate limiter. + + :param limit: Maximum concurrent operations + :raises ValueError: If limit is less than 1 + """ + if limit < 1: + raise ValueError("Rate limit must be at least 1") + + self._limit = limit + self._semaphore = Semaphore(limit) + self._lock = asyncio.Lock() + + async def __aenter__(self) -> RateLimiter: + """Enter rate limiter context.""" + await self._semaphore.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> None: + """Exit rate limiter context.""" + self._semaphore.release() + + @property + def limit(self) -> int: + """Get current rate limit.""" + return self._limit + + @property + def available(self) -> int: + """Get available slots (lock-free read). + + Uses getattr for safety - may return 0 if unable to read. + """ + try: + value = getattr(self._semaphore, "_value", None) + return value if isinstance(value, int) else 0 + except (AttributeError, TypeError): + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning("Cannot access semaphore value", exc_info=True) + return 0 + + @property + def active(self) -> int: + """Get active operations count.""" + return max(0, self._limit - self.available) + + async def set_limit(self, new_limit: int) -> None: + """Change rate limit dynamically. + + Creates new semaphore to avoid complex state management. + + :param new_limit: New rate limit value + :raises ValueError: If new_limit is less than 1 + """ + if new_limit < 1: + raise ValueError("Rate limit must be at least 1") + + async with self._lock: + if new_limit == self._limit: + return + + old_limit = self._limit + self._limit = new_limit + self._semaphore = Semaphore(new_limit) + + if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG): + logger.debug("Rate limit changed from %d to %d", old_limit, new_limit) + + +class RetryHelper: + """Helper class for retry logic with exponential backoff.""" + + __slots__ = () + + @staticmethod + async def execute_with_retry( + func: Callable[[], Awaitable[ResponseData]], + endpoint: str, + retry_attempts: int, + metrics: MetricsCollector, + ) -> ResponseData: + """Execute request with retry logic and comprehensive error metrics. + + Implements exponential backoff with jitter for distributed systems. + + :param func: Request function to execute + :param endpoint: API endpoint + :param retry_attempts: Number of retry attempts + :param metrics: Metrics collector + :return: Response data + :raises APIError: If all retry attempts fail + """ + last_error: Exception | None = None + + for attempt in range(retry_attempts + 1): + try: + return await func() + + except (OutlineTimeoutError, OutlineConnectionError, APIError) as error: + last_error = error + + # Track error metrics + metrics.increment( + "outline.request.error", + tags={ + "endpoint": endpoint, + "error_type": type(error).__name__, + "attempt": str(attempt + 1), + }, + ) + + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "Request to %s failed (attempt %d/%d): %s", + endpoint, + attempt + 1, + retry_attempts + 1, + error, + ) + + # Check if error is retryable + if ( + isinstance(error, APIError) + and error.status_code + and error.status_code not in Constants.RETRY_STATUS_CODES + ): + # Non-retryable error - fail fast + metrics.increment( + "outline.request.non_retryable", + tags={"endpoint": endpoint, "status": str(error.status_code)}, + ) + raise + + if attempt < retry_attempts: + delay = RetryHelper._calculate_delay(attempt) + metrics.increment( + "outline.request.retry", + tags={"endpoint": endpoint, "attempt": str(attempt + 1)}, + ) + await asyncio.sleep(delay) + + # All retries exhausted + metrics.increment("outline.request.exhausted", tags={"endpoint": endpoint}) + + raise APIError( + f"Request failed after {retry_attempts + 1} attempts", + endpoint=endpoint, + ) from last_error + + @staticmethod + def _calculate_delay(attempt: int) -> float: + """Calculate retry delay with exponential backoff and jitter. + + Jitter prevents thundering herd problem in distributed systems. + + :param attempt: Current attempt number (0-indexed) + :return: Delay in seconds + """ + base_delay = Constants.DEFAULT_RETRY_DELAY * (attempt + 1) + # Secure random jitter: ±20% of base delay + jitter = base_delay * 0.2 * (secrets.randbelow(40) - 20) / 100 + return max(0.1, base_delay + jitter) + + +class SSLFingerprintValidator: + """Enhanced SSL validation with strict fingerprint pinning. + + SECURITY CRITICAL: + - Outline VPN uses self-signed certificates + - We disable CA verification but enforce fingerprint pinning + - Constant-time comparison prevents timing attacks + - SecretStr keeps fingerprint secure in memory + - TLS 1.2+ enforcement + + MITM Prevention: + - Fingerprint verified on every connection + - Mismatch raises ValueError (connection aborted) + - Certificate pinning per OWASP recommendations + """ + + __slots__ = ("_expected_fingerprint_secret", "_ssl_context") + + def __init__(self, cert_sha256: SecretStr) -> None: + """Initialize SSL validator with fingerprint pinning. + + :param cert_sha256: Pre-validated SHA-256 fingerprint as SecretStr + + SECURITY: Fingerprint must be pre-validated by + Validators.validate_cert_fingerprint() before calling this. + SecretStr maintained for memory protection. + """ + self._expected_fingerprint_secret: SecretStr | None = cert_sha256 + + # Create SSL context WITHOUT CA verification (self-signed certs) + # Security is ensured by strict fingerprint pinning + self._ssl_context: ssl.SSLContext | None = ssl.create_default_context() + self._ssl_context.check_hostname = False # We verify via fingerprint + self._ssl_context.verify_mode = ssl.CERT_NONE # Accept self-signed + + # SECURITY: Enforce minimum TLS 1.2 (TLS 1.3 preferred if available) + self._ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + + # Enable TLS 1.3 if available + with suppress(AttributeError): + self._ssl_context.maximum_version = ssl.TLSVersion.TLSv1_3 + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> None: + """Clean up sensitive data on exit.""" + # Clear sensitive references + self._expected_fingerprint_secret = None + self._ssl_context = None + + @property + def ssl_context(self) -> ssl.SSLContext: + """Get SSL context for aiohttp.""" + if self._ssl_context is None: + raise RuntimeError("SSL context is no longer available") + return self._ssl_context + + def _verify_cert_fingerprint(self, cert_der: bytes) -> None: + """Verify certificate fingerprint matches expected. + + :param cert_der: Certificate in DER format + :raises ValueError: If fingerprint doesn't match (MITM detected) + """ + import hashlib + + # Compute actual fingerprint + actual_fingerprint = hashlib.sha256(cert_der).hexdigest() + + # Get expected fingerprint from secure storage + if self._expected_fingerprint_secret is None: + raise RuntimeError("Expected fingerprint is no longer available") + expected_fingerprint = self._expected_fingerprint_secret.get_secret_value() + + # SECURITY: Constant-time comparison prevents timing attacks + if not secrets.compare_digest(actual_fingerprint, expected_fingerprint): + raise ValueError( + "Certificate fingerprint mismatch - possible MITM attack detected" + ) + + async def verify_connection( + self, + session: ClientSession, + trace_config_ctx: SimpleNamespace, + params: TraceConnectionCreateEndParams | TraceConnectionReuseconnParams, + ) -> None: + """Verify certificate fingerprint during connection. + + :param session: aiohttp session + :param trace_config_ctx: Trace context + :param params: Request parameters + :raises ValueError: If fingerprint doesn't match (MITM detected) + """ + # Get peer certificate from transport (create/reuse hooks) + transport = getattr(params, "transport", None) + if transport is None: + connection = getattr(params, "connection", None) + transport = getattr(connection, "transport", None) if connection else None + if transport is None: + return + + ssl_object = transport.get_extra_info("ssl_object") + if ssl_object is None: + return + + # Get certificate in DER format (binary) + cert_der = ssl_object.getpeercert(binary_form=True) + if cert_der: + # SECURITY: Verify fingerprint (raises on mismatch) + self._verify_cert_fingerprint(cert_der) + + +class BaseHTTPClient: + """HTTP client with comprehensive security features.""" + + __slots__ = ( + "_active_requests", + "_active_requests_lock", + "_allow_private_networks", + "_api_hostname", + "_api_url", + "_audit_logger", + "_cert_sha256", + "_circuit_breaker", + "_enable_logging", + "_max_connections", + "_metrics", + "_rate_limiter", + "_rate_limiter_tps", + "_resolve_dns_for_ssrf", + "_retry_attempts", + "_retry_helper", + "_session", + "_session_lock", + "_shutdown_event", + "_ssl_validator", + "_timeout", + "_user_agent", + ) + + def __init__( + self, + api_url: str, + cert_sha256: SecretStr, + *, + timeout: int = Constants.DEFAULT_TIMEOUT, + retry_attempts: int = Constants.DEFAULT_RETRY_ATTEMPTS, + max_connections: int = Constants.DEFAULT_MAX_CONNECTIONS, + user_agent: str | None = None, + enable_logging: bool = False, + circuit_config: CircuitConfig | None = None, + rate_limit: int = 100, + allow_private_networks: bool = True, + resolve_dns_for_ssrf: bool = False, + audit_logger: AuditLogger | None = None, + metrics: MetricsCollector | None = None, + ) -> None: + """Initialize base HTTP client with enhanced security. + + :param api_url: Outline server API URL + :param cert_sha256: SHA-256 certificate fingerprint (as SecretStr) + :param timeout: Request timeout in seconds + :param retry_attempts: Number of retry attempts + :param max_connections: Connection pool size + :param user_agent: Custom user agent string + :param enable_logging: Enable debug logging + :param circuit_config: Circuit breaker configuration + :param rate_limit: Maximum concurrent requests + :param allow_private_networks: Allow private/local network api_url + :param resolve_dns_for_ssrf: Resolve DNS for SSRF checks (strict mode) + :param audit_logger: Custom audit logger + :param metrics: Custom metrics collector + :raises ValueError: If parameters are invalid + """ + # Validate and sanitize URL (removes trailing slash) + self._api_url = Validators.validate_url( + api_url, + allow_private_networks=allow_private_networks, + resolve_dns=False, + ).rstrip("/") + self._api_hostname = urlparse(self._api_url).hostname + self._allow_private_networks = allow_private_networks + self._resolve_dns_for_ssrf = resolve_dns_for_ssrf + + # SECURITY: Validate fingerprint and keep as SecretStr + # Never expose as plain string - SecretStr protects memory + self._cert_sha256 = Validators.validate_cert_fingerprint(cert_sha256) + + # Validate numeric parameters + self._validate_numeric_params(timeout, retry_attempts, max_connections) + + self._timeout = aiohttp.ClientTimeout(total=float(timeout)) + self._retry_attempts = retry_attempts + self._max_connections = max_connections + self._user_agent = user_agent or Constants.DEFAULT_USER_AGENT + self._enable_logging = enable_logging + + # SECURITY: Pass SecretStr directly - maintains security + self._ssl_validator = SSLFingerprintValidator(self._cert_sha256) + + self._session: aiohttp.ClientSession | None = None + self._session_lock = asyncio.Lock() + self._circuit_breaker: CircuitBreaker | None = None + + if circuit_config is not None: + self._init_circuit_breaker(circuit_config) + + # Rate limiting: concurrent + token bucket + self._rate_limiter = RateLimiter(rate_limit) + self._rate_limiter_tps = TokenBucketRateLimiter() + + # Audit logging and metrics + self._audit_logger = audit_logger or NoOpAuditLogger() + self._metrics = metrics or NoOpMetrics() + self._retry_helper = RetryHelper() + + # Active request tracking for graceful shutdown + self._active_requests: set[asyncio.Task[ResponseData]] = set() + self._active_requests_lock = asyncio.Lock() + self._shutdown_event = asyncio.Event() + + @staticmethod + def _validate_numeric_params( + timeout: int, retry_attempts: int, max_connections: int + ) -> None: + """Validate numeric parameters. + + :param timeout: Timeout value + :param retry_attempts: Retry attempts value + :param max_connections: Max connections value + :raises ValueError: If any parameter is invalid + """ + if timeout < 1: + raise ValueError("Timeout must be at least 1 second") + if retry_attempts < 0: + raise ValueError("Retry attempts cannot be negative") + if max_connections < 1: + raise ValueError("Max connections must be at least 1") + + def _init_circuit_breaker(self, config: CircuitConfig) -> None: + """Initialize circuit breaker with adjusted timeout. + + Ensures circuit breaker timeout accounts for retries. + + :param config: Circuit breaker configuration + """ + from .circuit_breaker import CircuitBreaker, CircuitConfig + + # Calculate maximum possible request time including retries + total_timeout = self._timeout.total or 0.0 + max_retry_time = total_timeout * (self._retry_attempts + 1) + max_delays = sum( + Constants.DEFAULT_RETRY_DELAY * (i + 1) for i in range(self._retry_attempts) + ) + cb_timeout = max_retry_time + max_delays + 5.0 # +5s safety margin + + # Adjust circuit breaker timeout if too low + if config.call_timeout < cb_timeout: + adjusted_config = CircuitConfig( + failure_threshold=config.failure_threshold, + recovery_timeout=config.recovery_timeout, + success_threshold=config.success_threshold, + call_timeout=cb_timeout, + ) + self._circuit_breaker = CircuitBreaker("outline_api", adjusted_config) + else: + self._circuit_breaker = CircuitBreaker("outline_api", config) + + async def __aenter__(self) -> BaseHTTPClient: + """Context manager entry - initialize session.""" + await self._ensure_session() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> None: + """Context manager exit - cleanup session.""" + await self.shutdown() + + async def _ensure_session(self) -> None: + """Ensure aiohttp session is initialized with enhanced security. + + Double-checked locking pattern for thread-safe lazy initialization. + """ + if self._session is not None and not self._session.closed: + return # Fast path - no lock needed + + async with self._session_lock: + # Double-check after acquiring lock + if self._session is not None and not self._session.closed: + return + + # Create connector with security and performance settings + connector = aiohttp.TCPConnector( + ssl=self._ssl_validator.ssl_context, + limit=self._max_connections, + limit_per_host=max(1, self._max_connections // 2), + ttl_dns_cache=Constants.DNS_CACHE_TTL, + enable_cleanup_closed=True, + force_close=False, # Reuse connections for performance + ) + + # Verifies certificate on every request (MITM prevention) + trace_config = aiohttp.TraceConfig() + trace_config.on_connection_create_end.append( + self._ssl_validator.verify_connection + ) + trace_config.on_connection_reuseconn.append( + self._ssl_validator.verify_connection + ) + + self._session = aiohttp.ClientSession( + connector=connector, + timeout=self._timeout, + raise_for_status=False, # Manual status handling + trace_configs=[trace_config], + ) + + if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG): + logger.debug("HTTP session initialized") + + async def _request( + self, + method: str, + endpoint: str, + *, + json: JsonPayload = None, + params: QueryParams | None = None, + ) -> ResponseData: + """Make HTTP request with comprehensive protection. + + Request flow: + 1. Ensure session initialized + 2. Generate secure correlation ID + 3. Apply token bucket rate limiting + 4. Apply circuit breaker (if configured) + 5. Execute request with retry logic + 6. Track metrics and audit log + + :param method: HTTP method (GET, POST, PUT, DELETE) + :param endpoint: API endpoint path + :param json: JSON payload for request body + :param params: Query parameters + :return: Response data as dict + :raises APIError: If request fails after retries + :raises CircuitOpenError: If circuit breaker is open + :raises TimeoutError: If request times out + :raises ConnectionError: If connection fails + """ + # Strict SSRF re-check at request time (DNS rebinding protection) + if ( + self._resolve_dns_for_ssrf + and not self._allow_private_networks + and self._api_hostname + and SSRFProtection.is_blocked_hostname_uncached(self._api_hostname) + ): + raise ValueError( + f"Access to {self._api_hostname} is blocked (SSRF protection)" + ) + + await self._ensure_session() + + # SECURITY: Generate secure correlation ID for distributed tracing + request_id = SecureIDGenerator.generate_correlation_id() + correlation_id.set(request_id) + + # Apply token bucket rate limiting (requests per second) + await self._rate_limiter_tps.acquire() + + # Circuit breaker protection (if configured) + if self._circuit_breaker: + try: + return await self._circuit_breaker.call( + self._make_request_inner, + method, + endpoint, + json=json, + params=params, + correlation_id=request_id, + ) + except CircuitOpenError: + # Track circuit breaker open event + self._metrics.increment( + "outline.circuit.open", + tags={"endpoint": endpoint, "method": method}, + ) + if logger.isEnabledFor(Constants.LOG_LEVEL_ERROR): + logger.error( + "Circuit breaker OPEN for %s - rejecting request", endpoint + ) + raise + + # No circuit breaker - direct execution + return await self._make_request_inner( + method, endpoint, json=json, params=params, correlation_id=request_id + ) + + async def _make_request_inner( + self, + method: str, + endpoint: str, + *, + json: JsonPayload = None, + params: QueryParams | None = None, + correlation_id: str, + ) -> ResponseData: + """Inner request method with comprehensive error handling. + + Implements retry logic, metrics, audit logging, and error handling. + + :param method: HTTP method + :param endpoint: API endpoint + :param json: JSON payload + :param params: Query parameters + :param correlation_id: Request correlation ID + :return: Response data + """ + + async def _make_request() -> ResponseData: + """Actual request execution (closure for retry logic).""" + await self._ensure_session() + + url = self._build_url(endpoint) + start_time = time.monotonic() + + # Track active request for graceful shutdown + current_task = asyncio.current_task() + if current_task: + async with self._active_requests_lock: + self._active_requests.add(current_task) + + try: + # Apply concurrent rate limiting + async with self._rate_limiter: + # SECURITY: Headers with security and tracing info + headers = { + "User-Agent": self._user_agent, + "X-Request-ID": correlation_id, + "X-Content-Type-Options": "nosniff", # Security header + "X-Frame-Options": "DENY", # Security header + "Accept": "application/json", + } + + if self._session is None: + raise APIError( + "HTTP session not initialized", endpoint=endpoint + ) + + async with self._session.request( + method, url, json=json, params=params, headers=headers + ) as response: + duration = time.monotonic() - start_time + + # Debug logging (if enabled) + if self._enable_logging and logger.isEnabledFor( + Constants.LOG_LEVEL_DEBUG + ): + safe_endpoint = Validators.sanitize_endpoint_for_logging( + endpoint + ) + logger.debug( + "[%s] %s %s -> %d", + correlation_id, + method, + safe_endpoint, + response.status, + extra={"correlation_id": correlation_id}, + ) + + # Metrics: request duration + self._metrics.timing( + "outline.request.duration", + duration, + tags={"method": method, "endpoint": endpoint}, + ) + + # Handle HTTP errors (4xx, 5xx) + if response.status >= 400: + self._metrics.increment( + "outline.request.http_error", + tags={ + "method": method, + "status": str(response.status), + "endpoint": endpoint, + "status_class": f"{response.status // 100}xx", + }, + ) + await self._handle_error(response, endpoint) + + # Metrics: successful request + self._metrics.increment( + "outline.request.success", + tags={"method": method, "endpoint": endpoint}, + ) + + # Handle 204 No Content + if response.status == 204: + return {"success": True} + + # Parse JSON response with size limits + return await self._parse_response_safe(response, endpoint) + + except asyncio.TimeoutError as e: + duration = time.monotonic() - start_time + + # Track timeout metrics + self._metrics.timing( + "outline.request.timeout", + duration, + tags={"method": method, "endpoint": endpoint}, + ) + self._metrics.increment( + "outline.request.timeout_error", + tags={ + "endpoint": endpoint, + "method": method, + "timeout_value": str(self._timeout.total), + }, + ) + + raise OutlineTimeoutError( + f"Request to {endpoint} timed out", + timeout=self._timeout.total, + ) from e + + except aiohttp.ClientConnectionError as e: + # Track connection errors + self._metrics.increment( + "outline.connection.error", + tags={ + "endpoint": endpoint, + "error_type": type(e).__name__, + "method": method, + }, + ) + + # SECURITY: Sanitize hostname and error message + hostname = Validators.sanitize_url_for_logging(url) + safe_message = CredentialSanitizer.sanitize(str(e)) + + raise OutlineConnectionError( + f"Failed to connect: {safe_message}", + host=hostname, + ) from e + + except aiohttp.ClientError as e: + # Track client errors + self._metrics.increment( + "outline.request.client_error", + tags={ + "endpoint": endpoint, + "error_type": type(e).__name__, + "method": method, + }, + ) + + # SECURITY: Sanitize error message + safe_message = CredentialSanitizer.sanitize(str(e)) + + raise APIError( + f"Request failed: {safe_message}", endpoint=endpoint + ) from e + + finally: + # Remove from active requests + if current_task: + async with self._active_requests_lock: + self._active_requests.discard(current_task) + + # Execute with retry logic + return await self._retry_helper.execute_with_retry( + _make_request, endpoint, self._retry_attempts, self._metrics + ) + + @staticmethod + async def _parse_response_safe( + response: ClientResponse, endpoint: str + ) -> ResponseData: + """Parse response with size limits and validation. + + :param response: HTTP response + :param endpoint: API endpoint + :return: Parsed JSON data + :raises APIError: If parsing fails or size exceeds limit + """ + # Check Content-Length header + content_length = response.headers.get("Content-Length") + if content_length: + try: + length_value = int(content_length) + except ValueError: + length_value = None + + if length_value is not None and length_value > Constants.MAX_RESPONSE_SIZE: + raise APIError( + f"Response too large: {length_value} bytes " + f"(max {Constants.MAX_RESPONSE_SIZE})", + status_code=response.status, + endpoint=endpoint, + ) + + # Validate Content-Type + content_type = response.headers.get("Content-Type", "").lower() + if ( + content_type + and "application/json" not in content_type + and logger.isEnabledFor(Constants.LOG_LEVEL_WARNING) + ): + logger.warning("Unexpected Content-Type: %s", content_type) + + # Read response in chunks with size limit + chunks = [] + total_size = 0 + + async for chunk in response.content.iter_chunked( + Constants.MAX_RESPONSE_CHUNK_SIZE + ): + total_size += len(chunk) + if total_size > Constants.MAX_RESPONSE_SIZE: + raise APIError( + f"Response exceeded size limit: {total_size} bytes", + status_code=response.status, + endpoint=endpoint, + ) + chunks.append(chunk) + + data = b"".join(chunks) + + # Parse JSON + try: + parsed = json.loads(data) + if isinstance(parsed, dict): + return cast(ResponseData, parsed) + if 200 <= response.status < 300: + return {"success": True} + raise APIError( + f"Expected JSON object from {endpoint}, got {type(parsed).__name__}", + status_code=response.status, + endpoint=endpoint, + ) + except (json.JSONDecodeError, ValueError) as e: + # Success status but invalid JSON - return generic success + if 200 <= response.status < 300: + return {"success": True} + raise APIError( + f"Invalid JSON response from {endpoint}: {e}", + status_code=response.status, + endpoint=endpoint, + ) from e + + def _build_url(self, endpoint: str) -> str: + """Build full URL from endpoint. + + :param endpoint: API endpoint + :return: Full URL + """ + clean_endpoint = endpoint.lstrip("/") + return f"{self._api_url}/{clean_endpoint}" + + @staticmethod + async def _handle_error(response: ClientResponse, endpoint: str) -> None: + """Handle error response and raise appropriate exception. + + :param response: HTTP response + :param endpoint: API endpoint + :raises APIError: Always raises with error details + """ + try: + error_data = await response.json() + message = error_data.get("message", response.reason or "Unknown error") + except (ValueError, aiohttp.ContentTypeError, TypeError): + message = response.reason or "Unknown error" + + # SECURITY: Sanitize error message + safe_message = CredentialSanitizer.sanitize(message) + + raise APIError(safe_message, status_code=response.status, endpoint=endpoint) + + async def shutdown(self, timeout: float = 30.0) -> None: + """Graceful shutdown with timeout. + + Waits for active requests to complete before closing session. + + :param timeout: Maximum time to wait for active requests (seconds) + """ + if self._shutdown_event.is_set(): + return # Already shutting down + + self._shutdown_event.set() + + # Get snapshot of active requests + async with self._active_requests_lock: + active_requests = list(self._active_requests) + + if active_requests: + if logger.isEnabledFor(Constants.LOG_LEVEL_INFO): + logger.info("Waiting for %d active requests...", len(active_requests)) + + try: + # Wait for active requests with timeout + await asyncio.wait_for( + asyncio.gather(*active_requests, return_exceptions=True), + timeout=timeout, + ) + except asyncio.TimeoutError: + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "Shutdown timeout, cancelling %d requests", + len(active_requests), + ) + # Force cancel remaining requests + for task in active_requests: + if not task.done(): + task.cancel() + + # Close session + async with self._session_lock: + if self._session and not self._session.closed: + await self._session.close() + self._session = None + + if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG): + logger.debug("HTTP client shutdown complete") + + # ===== Properties ===== + + @property + def api_url(self) -> str: + """Get sanitized API URL without secret path.""" + return Validators.sanitize_url_for_logging(self._api_url) + + @property + def is_connected(self) -> bool: + """Check if session is connected.""" + return self._session is not None and not self._session.closed + + @property + def circuit_state(self) -> str | None: + """Get circuit breaker state.""" + if self._circuit_breaker: + return self._circuit_breaker.state.name + return None + + @property + def rate_limit(self) -> int: + """Get current concurrent rate limit.""" + return self._rate_limiter.limit + + @property + def active_requests(self) -> int: + """Get number of active requests.""" + return len(self._active_requests) + + @property + def available_slots(self) -> int: + """Get number of available rate limit slots.""" + return self._rate_limiter.available + + async def set_rate_limit(self, new_limit: int) -> None: + """Change concurrent rate limit dynamically.""" + await self._rate_limiter.set_limit(new_limit) + + def get_rate_limiter_stats(self) -> dict[str, int | float]: + """Get comprehensive rate limiter statistics. + + Includes both concurrent and token bucket metrics. + + :return: Rate limiter statistics + """ + return { + "limit": self._rate_limiter.limit, + "active": len(self._active_requests), + "available": self._rate_limiter.available, + "tokens_available": self._rate_limiter_tps.available_tokens, + } + + async def reset_circuit_breaker(self) -> bool: + """Reset circuit breaker to closed state. + + :return: True if circuit breaker was reset, False if not configured + """ + if self._circuit_breaker: + await self._circuit_breaker.reset() + return True + return False + + def get_circuit_metrics(self) -> dict[str, int | float | str] | None: + """Get circuit breaker metrics. + + :return: Circuit breaker metrics or None if not configured + """ + if not self._circuit_breaker: + return None + + metrics = self._circuit_breaker.metrics + return { + "state": self._circuit_breaker.state.name, + "total_calls": metrics.total_calls, + "successful_calls": metrics.successful_calls, + "failed_calls": metrics.failed_calls, + "success_rate": metrics.success_rate, + } + + +__all__ = ["BaseHTTPClient", "MetricsCollector", "NoOpMetrics", "correlation_id"] diff --git a/pyoutlineapi/batch_operations.py b/pyoutlineapi/batch_operations.py new file mode 100644 index 0000000..e54820b --- /dev/null +++ b/pyoutlineapi/batch_operations.py @@ -0,0 +1,684 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import asyncio +import logging +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Generic, TypedDict, TypeVar, cast + +from .common_types import Validators +from .models import DataLimit + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + + from .client import AsyncOutlineClient + from .models import AccessKey + +logger = logging.getLogger(__name__) + +T = TypeVar("T") +R = TypeVar("R") + + +class AccessKeyCreateConfig(TypedDict, total=False): + """Typed configuration for creating access keys in batch.""" + + name: str | None + password: str | None + port: int | None + method: str | None + limit: DataLimit | None + + +def _log_if_enabled(level: int, message: str) -> None: + """Centralized logging with level check (DRY). + + :param level: Logging level + :param message: Log message + """ + if logger.isEnabledFor(level): + logger.log(level, message) + + +@dataclass(slots=True, frozen=True) +class BatchResult(Generic[R]): + """Result of batch operation with enhanced tracking. + + Immutable result object to prevent accidental modification. + Uses cached_property for expensive computations. + """ + + total: int + successful: int + failed: int + results: tuple[R | BaseException, ...] = field(default_factory=tuple) + errors: tuple[str, ...] = field(default_factory=tuple) + validation_errors: tuple[str, ...] = field(default_factory=tuple) + + @property + def success_rate(self) -> float: + """Calculate success rate (cached). + + :return: Success rate as decimal (0.0 to 1.0) + """ + if self.total == 0: + return 1.0 + return self.successful / self.total + + @property + def has_errors(self) -> bool: + """Check if any operations failed (cached). + + :return: True if any failures occurred + """ + return self.failed > 0 + + @property + def has_validation_errors(self) -> bool: + """Check if any validation errors occurred (cached). + + :return: True if validation errors exist + """ + return len(self.validation_errors) > 0 + + def get_successful_results(self) -> list[R]: + """Get only successful results (type-safe). + + :return: List of successful results + """ + return [r for r in self.results if not isinstance(r, BaseException)] + + def get_failures(self) -> list[BaseException]: + """Get only failures. + + :return: List of exceptions + """ + return [r for r in self.results if isinstance(r, BaseException)] + + @property + def _dict_cache(self) -> dict[str, object]: + """Cached dictionary representation. + + :return: Dictionary representation + """ + return { + "total": self.total, + "successful": self.successful, + "failed": self.failed, + "success_rate": self.success_rate, + "has_errors": self.has_errors, + "has_validation_errors": self.has_validation_errors, + "validation_errors": list(self.validation_errors), + "errors": list(self.errors), + } + + def to_dict(self) -> dict[str, object]: + """Convert to dictionary for serialization (cached). + + :return: Dictionary representation + """ + return self._dict_cache + + +class BatchProcessor(Generic[T, R]): + """Generic batch processor with concurrency control and safety features.""" + + __slots__ = ("_max_concurrent", "_semaphore", "_semaphore_lock") + + def __init__(self, max_concurrent: int = 5) -> None: + """Initialize batch processor. + + :param max_concurrent: Maximum concurrent operations + :raises ValueError: If max_concurrent is less than 1 + """ + if max_concurrent < 1: + msg = "max_concurrent must be at least 1" + raise ValueError(msg) + + self._max_concurrent = max_concurrent + self._semaphore = asyncio.Semaphore(max_concurrent) + self._semaphore_lock = asyncio.Lock() + + async def process( + self, + items: list[T], + processor: Callable[[T], Awaitable[R]], + *, + fail_fast: bool = False, + ) -> list[R | BaseException]: + """Process items in batch with concurrency control. + + :param items: Items to process + :param processor: Async function to process each item + :param fail_fast: Stop on first error + :return: List of results or exceptions + """ + if not items: + return [] + + async def process_single(item: T, index: int) -> R | Exception: + async with self._semaphore: + try: + result = await processor(item) + _log_if_enabled( + logging.DEBUG, + f"Batch item {index} completed successfully", + ) + return result + except Exception as e: + _log_if_enabled( + logging.DEBUG, + f"Batch item {index} failed: {e}", + ) + if fail_fast: + raise + return e + + tasks = [ + asyncio.create_task(process_single(item, i)) for i, item in enumerate(items) + ] + + try: + results = await asyncio.gather(*tasks, return_exceptions=not fail_fast) + + return list(results) + except Exception: + for task in tasks: + if isinstance(task, asyncio.Task) and not task.done(): + task.cancel() + raise + + async def set_concurrency(self, new_limit: int) -> None: + """Change concurrency limit dynamically. + + :param new_limit: New concurrency limit + :raises ValueError: If new_limit is less than 1 + """ + if new_limit < 1: + msg = "Concurrency limit must be at least 1" + raise ValueError(msg) + + async with self._semaphore_lock: + if new_limit == self._max_concurrent: + return + + self._max_concurrent = new_limit + self._semaphore = asyncio.Semaphore(new_limit) + + _log_if_enabled( + logging.DEBUG, + f"Batch concurrency changed to {new_limit}", + ) + + +class ValidationHelper: + """Helper class for batch validation logic (DRY).""" + + __slots__ = () + + @staticmethod + def validate_config_dict( + config: object, index: int, fail_fast: bool + ) -> dict[str, object] | None: + """Validate and process config dictionary. + + :param config: Configuration to validate + :param index: Config index for error reporting + :param fail_fast: Whether to raise on error + :return: Validated config or None if invalid + :raises ValueError: If fail_fast and validation fails + """ + if not isinstance(config, dict): + error_msg = ( + f"Config {index}: must be a dictionary, got {type(config).__name__}" + ) + if fail_fast: + raise ValueError(error_msg) + return None + + try: + validated_config = config.copy() + + if config.get("name"): + validated_name = Validators.validate_name(config["name"]) + validated_config["name"] = validated_name + + if "port" in config and config["port"] is not None: + validated_config["port"] = Validators.validate_port(config["port"]) + + return validated_config + + except ValueError as e: + error_msg = f"Config {index}: {e}" + if fail_fast: + raise ValueError(error_msg) from e + return None + + @staticmethod + def validate_key_id(key_id: object, index: int, fail_fast: bool) -> str | None: + """Validate key ID. + + :param key_id: Key ID to validate + :param index: Key index for error reporting + :param fail_fast: Whether to raise on error + :return: Validated key ID or None if invalid + :raises ValueError: If fail_fast and validation fails + """ + if not isinstance(key_id, str): + error_msg = f"Key {index}: must be a string, got {type(key_id).__name__}" + if fail_fast: + raise ValueError(error_msg) + return None + + try: + return Validators.validate_key_id(key_id) + except ValueError as e: + error_msg = f"Key {index} ({key_id}): {e}" + if fail_fast: + raise ValueError(error_msg) from e + return None + + @staticmethod + def validate_tuple_pair( + pair: object, index: int, expected_types: tuple[type, ...], fail_fast: bool + ) -> tuple[object, ...] | None: + """Validate tuple pair. + + :param pair: Pair to validate + :param index: Pair index for error reporting + :param expected_types: Expected types for tuple elements + :param fail_fast: Whether to raise on error + :return: Validated pair or None if invalid + :raises ValueError: If fail_fast and validation fails + """ + if not isinstance(pair, tuple) or len(pair) != len(expected_types): + error_msg = ( + f"Pair {index}: must be a {len(expected_types)}-tuple, " + f"got {type(pair).__name__}" + ) + if fail_fast: + raise ValueError(error_msg) + return None + + for i, (element, expected_type) in enumerate( + zip(pair, expected_types, strict=False) + ): + if not isinstance(element, expected_type): + error_msg = ( + f"Pair {index}: element {i} must be {expected_type.__name__}, " + f"got {type(element).__name__}" + ) + if fail_fast: + raise ValueError(error_msg) + return None + + return pair + + +class BatchOperations: + """Enhanced batch operations for AsyncOutlineClient with validation.""" + + __slots__ = ("_client", "_max_concurrent", "_processor", "_validation_helper") + + def __init__( + self, + client: AsyncOutlineClient, + *, + max_concurrent: int = 5, + ) -> None: + """Initialize batch operations. + + :param client: AsyncOutlineClient instance + :param max_concurrent: Maximum concurrent operations + :raises ValueError: If max_concurrent is invalid + """ + if max_concurrent < 1: + msg = "max_concurrent must be at least 1" + raise ValueError(msg) + + self._client = client + self._max_concurrent = max_concurrent + self._processor: BatchProcessor[dict[str, object], AccessKey] = BatchProcessor( + max_concurrent + ) + self._validation_helper = ValidationHelper() + + async def create_multiple_keys( + self, + configs: list[dict[str, object]], + *, + fail_fast: bool = False, + ) -> BatchResult[AccessKey | BaseException]: + """Create multiple access keys in batch. + + :param configs: List of key configuration dictionaries + :param fail_fast: Stop on first error + :return: Batch operation result + """ + if not configs: + return self._build_empty_result() + + validation_errors: list[str] = [] + valid_configs: list[dict[str, object]] = [] + + for i, config in enumerate(configs): + validated = self._validation_helper.validate_config_dict( + config, i, fail_fast + ) + if validated is None: + validation_errors.append(f"Config {i}: validation failed") + else: + valid_configs.append(validated) + + async def create_key(config: dict[str, object]) -> AccessKey: + result = await self._client.create_access_key( + **cast(AccessKeyCreateConfig, cast(object, config)) + ) + if TYPE_CHECKING: + assert isinstance(result, AccessKey) + return result + + processor: BatchProcessor[dict[str, object], AccessKey] = BatchProcessor( + self._max_concurrent + ) + results = await processor.process( + valid_configs, create_key, fail_fast=fail_fast + ) + + return self._build_result(results, validation_errors) + + async def delete_multiple_keys( + self, + key_ids: list[str], + *, + fail_fast: bool = False, + ) -> BatchResult[object] | BatchResult[bool]: + """Delete multiple access keys in batch. + + :param key_ids: List of key IDs to delete + :param fail_fast: Stop on first error + :return: Batch operation result + """ + if not key_ids: + return self._build_empty_result() + + validated_ids: list[str] = [] + validation_errors: list[str] = [] + + for i, key_id in enumerate(key_ids): + validated = self._validation_helper.validate_key_id(key_id, i, fail_fast) + if validated is None: + validation_errors.append(f"Key {i}: validation failed") + else: + validated_ids.append(validated) + + async def delete_key(key_id: str) -> bool: + return await self._client.delete_access_key(key_id) + + processor: BatchProcessor[str, bool] = BatchProcessor(self._max_concurrent) + process_results = await processor.process( + validated_ids, delete_key, fail_fast=fail_fast + ) + + return self._build_result(process_results, validation_errors) + + async def rename_multiple_keys( + self, + key_name_pairs: list[tuple[str, str]], + *, + fail_fast: bool = False, + ) -> BatchResult[object] | BatchResult[bool]: + """Rename multiple access keys in batch. + + :param key_name_pairs: List of (key_id, new_name) tuples + :param fail_fast: Stop on first error + :return: Batch operation result + """ + if not key_name_pairs: + return self._build_empty_result() + + validated_pairs: list[tuple[str, str]] = [] + validation_errors: list[str] = [] + + for i, pair in enumerate(key_name_pairs): + validated = self._validation_helper.validate_tuple_pair( + pair, i, (str, str), fail_fast + ) + if validated is None: + validation_errors.append(f"Pair {i}: validation failed") + continue + + key_id, name = validated[0], validated[1] + if not isinstance(key_id, str) or not isinstance(name, str): + validation_errors.append(f"Pair {i}: invalid types") + continue + + try: + validated_id = Validators.validate_key_id(key_id) + validated_name = Validators.validate_name(name) + validated_pairs.append((validated_id, validated_name)) + + except ValueError as e: + error_msg = f"Pair {i}: {e}" + if fail_fast: + raise ValueError(error_msg) from e + validation_errors.append(error_msg) + + async def rename_key(pair: tuple[str, str]) -> bool: + key_id, name = pair + return await self._client.rename_access_key(key_id, name) + + processor: BatchProcessor[tuple[str, str], bool] = BatchProcessor( + self._max_concurrent + ) + results = await processor.process( + validated_pairs, rename_key, fail_fast=fail_fast + ) + + return self._build_result(results, validation_errors) + + async def set_multiple_data_limits( + self, + key_limit_pairs: list[tuple[str, int]], + *, + fail_fast: bool = False, + ) -> BatchResult[object] | BatchResult[bool]: + """Set data limits for multiple keys in batch. + + :param key_limit_pairs: List of (key_id, bytes_limit) tuples + :param fail_fast: Stop on first error + :return: Batch operation result + """ + if not key_limit_pairs: + return self._build_empty_result() + + validated_pairs: list[tuple[str, int]] = [] + validation_errors: list[str] = [] + + for i, pair in enumerate(key_limit_pairs): + validated = self._validation_helper.validate_tuple_pair( + pair, i, (str, int), fail_fast + ) + if validated is None: + validation_errors.append(f"Pair {i}: validation failed") + continue + + key_id, bytes_limit = validated[0], validated[1] + if not isinstance(key_id, str) or not isinstance(bytes_limit, int): + validation_errors.append(f"Pair {i}: invalid types") + continue + + try: + validated_id = Validators.validate_key_id(key_id) + validated_bytes = Validators.validate_non_negative( + bytes_limit, "bytes_limit" + ) + validated_pairs.append((validated_id, validated_bytes)) + + except ValueError as e: + error_msg = f"Pair {i}: {e}" + if fail_fast: + raise ValueError(error_msg) from e + validation_errors.append(error_msg) + + async def set_limit(pair: tuple[str, int]) -> bool: + key_id, bytes_limit = pair + return await self._client.set_access_key_data_limit( + key_id, DataLimit(bytes=bytes_limit) + ) + + processor: BatchProcessor[tuple[str, int], bool] = BatchProcessor( + self._max_concurrent + ) + results = await processor.process( + validated_pairs, set_limit, fail_fast=fail_fast + ) + + return self._build_result(results, validation_errors) + + async def fetch_multiple_keys( + self, + key_ids: list[str], + *, + fail_fast: bool = False, + ) -> BatchResult[AccessKey | BaseException]: + """Fetch multiple access keys in batch. + + :param key_ids: List of key IDs to fetch + :param fail_fast: Stop on first error + :return: Batch operation result + """ + if not key_ids: + return self._build_empty_result() + + validated_ids: list[str] = [] + validation_errors: list[str] = [] + + for i, key_id in enumerate(key_ids): + validated = self._validation_helper.validate_key_id(key_id, i, fail_fast) + if validated is None: + validation_errors.append(f"Key {i}: validation failed") + else: + validated_ids.append(validated) + + async def fetch_key(key_id: str) -> AccessKey: + result = await self._client.get_access_key(key_id) + if TYPE_CHECKING: + assert isinstance(result, AccessKey) + return result + + processor: BatchProcessor[str, AccessKey] = BatchProcessor(self._max_concurrent) + results = await processor.process(validated_ids, fetch_key, fail_fast=fail_fast) + + return self._build_result(results, validation_errors) + + async def execute_custom_operations( + self, + operations: list[Callable[[], Awaitable[object]]], + *, + fail_fast: bool = False, + ) -> BatchResult[object]: + """Execute custom batch operations. + + :param operations: List of async callables + :param fail_fast: Stop on first error + :return: Batch operation result + """ + if not operations: + return self._build_empty_result() + + validation_errors: list[str] = [] + valid_operations: list[Callable[[], Awaitable[object]]] = [] + + for op in operations: + valid_operations.append(op) + + async def execute_op(op: Callable[[], Awaitable[object]]) -> object: + return await op() + + processor: BatchProcessor[Callable[[], Awaitable[object]], object] = ( + BatchProcessor(self._max_concurrent) + ) + results = await processor.process( + valid_operations, execute_op, fail_fast=fail_fast + ) + + return self._build_result(results, validation_errors) + + async def set_concurrency(self, new_limit: int) -> None: + """Change batch concurrency limit dynamically. + + :param new_limit: New concurrency limit + :raises ValueError: If new_limit is invalid + """ + await self._processor.set_concurrency(new_limit) + self._max_concurrent = new_limit + + @staticmethod + def _build_result( + results: list[R | BaseException], + validation_errors: list[str], + ) -> BatchResult[R]: + """Build BatchResult from results list. + + :param results: List of results and exceptions + :param validation_errors: List of validation error messages + :return: Batch result object + """ + successful = 0 + errors_list: list[str] = [] + + for r in results: + if isinstance(r, BaseException): + errors_list.append(str(r)) + else: + successful += 1 + + failed = len(results) - successful + + return BatchResult( + total=len(results) + len(validation_errors), + successful=successful, + failed=failed + len(validation_errors), + results=tuple(results), + errors=tuple(errors_list), + validation_errors=tuple(validation_errors), + ) + + @staticmethod + def _build_empty_result() -> BatchResult[R]: + """Build empty BatchResult for empty input. + + :return: Empty batch result + """ + return cast( + BatchResult[R], + BatchResult( + total=0, + successful=0, + failed=0, + results=(), + errors=(), + validation_errors=(), + ), + ) + + +__all__ = [ + "BatchOperations", + "BatchProcessor", + "BatchResult", +] diff --git a/pyoutlineapi/circuit_breaker.py b/pyoutlineapi/circuit_breaker.py new file mode 100644 index 0000000..a2a133f --- /dev/null +++ b/pyoutlineapi/circuit_breaker.py @@ -0,0 +1,505 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from dataclasses import dataclass +from enum import Enum, auto +from typing import TYPE_CHECKING, ParamSpec, TypeVar + +from .common_types import Constants +from .exceptions import CircuitOpenError + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + +logger = logging.getLogger(__name__) + +P = ParamSpec("P") +T = TypeVar("T") + + +class CircuitState(Enum): + """Circuit breaker states. + + CLOSED: Normal operation, requests pass through (hot path) + OPEN: Failures exceeded threshold, requests blocked + HALF_OPEN: Testing recovery, limited requests allowed + """ + + CLOSED = auto() + OPEN = auto() + HALF_OPEN = auto() + + +@dataclass(frozen=True, slots=True) +class CircuitConfig: + """Circuit breaker configuration with validation. + + Immutable configuration to prevent runtime modification. + Uses slots for memory efficiency (~40 bytes per instance). + """ + + failure_threshold: int = 5 + recovery_timeout: float = 60.0 + success_threshold: int = 2 + call_timeout: float = 10.0 + + def __post_init__(self) -> None: + """Validate configuration at creation time. + + :raises ValueError: If any configuration value is invalid + """ + if self.failure_threshold < 1: + raise ValueError("failure_threshold must be >= 1") + if self.recovery_timeout < 1.0: + raise ValueError("recovery_timeout must be >= 1.0") + if self.success_threshold < 1: + raise ValueError("success_threshold must be >= 1") + if self.call_timeout < 0.1: + raise ValueError("call_timeout must be >= 0.1") + + +@dataclass(slots=True) +class CircuitMetrics: + """Circuit breaker metrics with efficient storage. + + Uses slots for memory efficiency (~80 bytes per instance). + All calculations are O(1) with no allocations. + """ + + total_calls: int = 0 + successful_calls: int = 0 + failed_calls: int = 0 + state_changes: int = 0 + last_failure_time: float = 0.0 + last_success_time: float = 0.0 + + @property + def success_rate(self) -> float: + """Calculate success rate (O(1), no allocations). + + :return: Success rate as decimal (0.0 to 1.0) + """ + if self.total_calls == 0: + return 1.0 + return self.successful_calls / self.total_calls + + @property + def failure_rate(self) -> float: + """Calculate failure rate (O(1), no allocations). + + :return: Failure rate as decimal (0.0 to 1.0) + """ + return 1.0 - self.success_rate + + def to_dict(self) -> dict[str, int | float]: + """Convert metrics to dictionary for serialization. + + Pre-computes rates to avoid repeated calculations. + + :return: Dictionary representation + """ + success_rate = self.success_rate # Calculate once + return { + "total_calls": self.total_calls, + "successful_calls": self.successful_calls, + "failed_calls": self.failed_calls, + "state_changes": self.state_changes, + "success_rate": success_rate, + "failure_rate": 1.0 - success_rate, # Reuse calculation + "last_failure_time": self.last_failure_time, + "last_success_time": self.last_success_time, + } + + +class CircuitBreaker: + """High-performance circuit breaker with lock-free fast path. + + Implements the circuit breaker pattern to prevent cascading failures + in distributed systems with minimal overhead for the common case. + """ + + __slots__ = ( + "_config", + "_failure_count", + "_last_failure_time", + "_last_state_change", + "_lock", + "_metrics", + "_name", + "_state", + "_success_count", + ) + + def __init__(self, name: str, config: CircuitConfig | None = None) -> None: + """Initialize circuit breaker. + + :param name: Circuit breaker name (for logging and identification) + :param config: Circuit breaker configuration + :raises ValueError: If name is empty + """ + if not name or not name.strip(): + raise ValueError("Circuit breaker name cannot be empty") + + self._name = name.strip() + self._config = config or CircuitConfig() + + self._state = CircuitState.CLOSED + self._failure_count = 0 + self._success_count = 0 + self._last_failure_time = 0.0 + self._last_state_change = 0.0 + + self._metrics = CircuitMetrics() + self._lock = asyncio.Lock() + + @property + def name(self) -> str: + """Get circuit breaker name. + + :return: Circuit name + """ + return self._name + + @property + def config(self) -> CircuitConfig: + """Get configuration. + + :return: Circuit configuration (immutable) + """ + return self._config + + @property + def state(self) -> CircuitState: + """Get current state (lock-free read). + + :return: Current circuit state + """ + return self._state + + @property + def metrics(self) -> CircuitMetrics: + """Get metrics snapshot. + + :return: Circuit metrics object + """ + return self._metrics + + def get_metrics_snapshot(self) -> dict[str, int | float]: + """Get thread-safe metrics snapshot. + + :return: Dictionary with current metrics + """ + return self._metrics.to_dict() + + async def call( + self, + func: Callable[P, Awaitable[T]], + *args: P.args, + **kwargs: P.kwargs, + ) -> T: + """Execute function with circuit breaker protection. + + :param func: Async function to execute + :param args: Positional arguments + :param kwargs: Keyword arguments + :return: Function result + :raises CircuitOpenError: If circuit is open + :raises TimeoutError: If call exceeds timeout + """ + current_state = self._state # Atomic read + + if ( + current_state == CircuitState.CLOSED + and self._failure_count < self._config.failure_threshold + ): + # Fast path: no state checking needed for closed circuit + return await self._execute_call(func, args, kwargs) + + # Slow path: need state checking/transition + await self._check_state() + + if self._state == CircuitState.OPEN: + # Calculate time until recovery + current_time = time.monotonic() + time_since_failure = current_time - self._last_failure_time + retry_after = max(0.0, self._config.recovery_timeout - time_since_failure) + + raise CircuitOpenError( + f"Circuit '{self._name}' is open", + retry_after=retry_after, + ) + + return await self._execute_call(func, args, kwargs) + + async def _execute_call( + self, + func: Callable[P, Awaitable[T]], + args: tuple, + kwargs: dict, + ) -> T: + """Execute the actual function call with timeout and metrics. + + Extracted to separate method for code reuse between fast and slow paths. + + :param func: Function to execute + :param args: Positional arguments + :param kwargs: Keyword arguments + :return: Function result + :raises TimeoutError: If call exceeds timeout + """ + start_time = time.monotonic() + + try: + # Use wait_for for timeout enforcement + result = await asyncio.wait_for( + func(*args, **kwargs), + timeout=self._config.call_timeout, + ) + + duration = time.monotonic() - start_time + await self._record_success(duration) + + return result + + except asyncio.TimeoutError as e: + duration = time.monotonic() - start_time + + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "Circuit '%s': timeout after %.2fs (limit: %.2fs)", + self._name, + duration, + self._config.call_timeout, + ) + + await self._record_failure(duration, e) + + from .exceptions import OutlineTimeoutError as OutlineTimeoutError + + raise OutlineTimeoutError( + f"Circuit '{self._name}': timeout after {self._config.call_timeout}s", + timeout=self._config.call_timeout, + operation=self._name, + ) from e + + except Exception as e: + duration = time.monotonic() - start_time + await self._record_failure(duration, e) + raise + + async def _check_state(self) -> None: + """Check and transition state if needed. + + Uses pattern matching for clear state transitions. + Only called on slow path (not in CLOSED state fast path). + """ + async with self._lock: + # Cache time calculation + current_time = time.monotonic() + + match self._state: + case CircuitState.OPEN: + # Check if recovery timeout has elapsed + time_since_failure = current_time - self._last_failure_time + if time_since_failure >= self._config.recovery_timeout: + if logger.isEnabledFor(Constants.LOG_LEVEL_INFO): + logger.info( + "Circuit '%s': attempting recovery after %.1fs", + self._name, + time_since_failure, + ) + await self._transition_to(CircuitState.HALF_OPEN) + + case CircuitState.CLOSED: + # Check if failure threshold exceeded + if self._failure_count >= self._config.failure_threshold: + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "Circuit '%s': opening due to %d failures (threshold: %d)", + self._name, + self._failure_count, + self._config.failure_threshold, + ) + await self._transition_to(CircuitState.OPEN) + + case CircuitState.HALF_OPEN: + # Half-open state is stable, no automatic transitions + pass + + async def _record_success(self, duration: float) -> None: + """Record successful call with metrics update. + + :param duration: Call duration in seconds + """ + # Always update metrics (atomic operations on integers are safe) + self._metrics.total_calls += 1 + self._metrics.successful_calls += 1 + self._metrics.last_success_time = time.monotonic() + + # Fast path: CLOSED state with no failures + if self._state == CircuitState.CLOSED and self._failure_count == 0: + return # No lock needed, no state change + + # Slow path: need state transition logic + async with self._lock: + if self._state == CircuitState.CLOSED: + # Reset failure count on success in closed state + if self._failure_count > 0: + if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG): + logger.debug( + "Circuit '%s': resetting %d failures after success", + self._name, + self._failure_count, + ) + self._failure_count = 0 + + elif self._state == CircuitState.HALF_OPEN: + # Count successes in half-open state + self._success_count += 1 + + if self._success_count >= self._config.success_threshold: + if logger.isEnabledFor(Constants.LOG_LEVEL_INFO): + logger.info( + "Circuit '%s': closing after %d consecutive successes (threshold: %d)", + self._name, + self._success_count, + self._config.success_threshold, + ) + await self._transition_to(CircuitState.CLOSED) + + async def _record_failure(self, duration: float, error: Exception) -> None: + """Record failed call with metrics update. + + :param duration: Call duration in seconds + :param error: Exception that occurred + """ + async with self._lock: + # Update metrics + self._metrics.total_calls += 1 + self._metrics.failed_calls += 1 + + self._failure_count += 1 + + # Cache time calculation + current_time = time.monotonic() + self._last_failure_time = current_time + self._metrics.last_failure_time = current_time + + # Log failure + if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG): + error_type = type(error).__name__ + logger.debug( + "Circuit '%s': failure #%d (%s) after %.2fs", + self._name, + self._failure_count, + error_type, + duration, + ) + + # In half-open state, any failure reopens the circuit + if self._state == CircuitState.HALF_OPEN: + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "Circuit '%s': recovery failed, reopening", self._name + ) + await self._transition_to(CircuitState.OPEN) + + async def _transition_to(self, new_state: CircuitState) -> None: + """Transition to new state with cleanup. + + :param new_state: Target state + """ + if self._state == new_state: + return + + old_state = self._state + self._state = new_state + self._metrics.state_changes += 1 + self._last_state_change = time.monotonic() + + if logger.isEnabledFor(Constants.LOG_LEVEL_INFO): + logger.info( + "Circuit '%s': %s -> %s", + self._name, + old_state.name, + new_state.name, + ) + + # State-specific cleanup using pattern matching + match new_state: + case CircuitState.CLOSED: + self._failure_count = 0 + self._success_count = 0 + + case CircuitState.HALF_OPEN: + self._success_count = 0 + self._failure_count = 0 + + case CircuitState.OPEN: + self._success_count = 0 + # Keep failure_count for metrics + + async def reset(self) -> None: + """Manually reset circuit breaker to closed state. + + Clears all counters and metrics. Use with caution in production. + """ + async with self._lock: + if logger.isEnabledFor(Constants.LOG_LEVEL_INFO): + logger.info("Circuit '%s': manual reset", self._name) + + await self._transition_to(CircuitState.CLOSED) + self._metrics = CircuitMetrics() + self._failure_count = 0 + self._success_count = 0 + + def is_open(self) -> bool: + """Check if circuit is open (lock-free read). + + :return: True if circuit is open + """ + return self._state == CircuitState.OPEN + + def is_half_open(self) -> bool: + """Check if circuit is half-open (lock-free read). + + :return: True if circuit is half-open + """ + return self._state == CircuitState.HALF_OPEN + + def is_closed(self) -> bool: + """Check if circuit is closed (lock-free read). + + :return: True if circuit is closed + """ + return self._state == CircuitState.CLOSED + + def get_time_since_last_state_change(self) -> float: + """Get time elapsed since last state change. + + :return: Time in seconds + """ + return time.monotonic() - self._last_state_change + + +__all__ = [ + "CircuitBreaker", + "CircuitConfig", + "CircuitMetrics", + "CircuitState", +] diff --git a/pyoutlineapi/client.py b/pyoutlineapi/client.py index 849e291..2d39289 100644 --- a/pyoutlineapi/client.py +++ b/pyoutlineapi/client.py @@ -1,5 +1,4 @@ -""" -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. Copyright (c) 2025 Denis Rozhnovskiy All rights reserved. @@ -15,1183 +14,947 @@ from __future__ import annotations import asyncio -import binascii import logging -import time from contextlib import asynccontextmanager -from functools import wraps -from typing import ( - Any, - AsyncGenerator, - Literal, - TypeAlias, - Union, - overload, - Optional, - ParamSpec, - TypeVar, - Callable, - Final, - Set, - Awaitable, -) -from urllib.parse import urlparse - -import aiohttp -from aiohttp import ClientResponse, Fingerprint -from pydantic import BaseModel - -from .exceptions import APIError, OutlineError -from .models import ( - AccessKey, - AccessKeyCreateRequest, - AccessKeyList, - AccessKeyNameRequest, - DataLimit, - DataLimitRequest, - ErrorResponse, - ExperimentalMetrics, - HostnameRequest, - MetricsEnabledRequest, - MetricsStatusResponse, - PortRequest, - Server, - ServerMetrics, - ServerNameRequest, -) - -# Type variables for decorator -P = ParamSpec("P") -T = TypeVar("T") - -# Type aliases -JsonDict: TypeAlias = dict[str, Any] -ResponseType = Union[JsonDict, BaseModel] - -# Constants -MIN_PORT: Final[int] = 1025 -MAX_PORT: Final[int] = 65535 -DEFAULT_RETRY_ATTEMPTS: Final[int] = 3 -DEFAULT_RETRY_DELAY: Final[float] = 1.0 -RETRY_STATUS_CODES: Final[Set[int]] = {408, 429, 500, 502, 503, 504} - -# Setup logger -logger = logging.getLogger(__name__) - - -def ensure_context(func: Callable[P, T]) -> Callable[P, T]: - """Decorator to ensure client session is initialized.""" - - @wraps(func) - async def wrapper(self: AsyncOutlineClient, *args: P.args, **kwargs: P.kwargs) -> T: - if not self._session or self._session.closed: - raise RuntimeError("Client session is not initialized or already closed.") - return await func(self, *args, **kwargs) - - return wrapper +from typing import TYPE_CHECKING, Any, Final +from weakref import WeakValueDictionary +from .api_mixins import AccessKeyMixin, DataLimitMixin, MetricsMixin, ServerMixin +from .audit import AuditLogger +from .base_client import BaseHTTPClient, MetricsCollector +from .common_types import ConfigOverrides, Validators, build_config_overrides +from .config import OutlineClientConfig +from .exceptions import ConfigurationError +from .models import AccessKeyList, MetricsStatusResponse -def log_method_call(func: Callable[P, T]) -> Callable[P, T]: - """Decorator to log method calls with performance metrics.""" +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Sequence + from pathlib import Path - @wraps(func) - async def wrapper(self: AsyncOutlineClient, *args: P.args, **kwargs: P.kwargs) -> T: - if not self._enable_logging: - return await func(self, *args, **kwargs) + from typing_extensions import Unpack - method_name = func.__name__ - start_time = time.perf_counter() +logger = logging.getLogger(__name__) - # Log method call (excluding sensitive data) - safe_kwargs = {k: v for k, v in kwargs.items() if k not in {'password', 'cert_sha256'}} - logger.debug(f"Calling {method_name} with args={args[1:]} kwargs={safe_kwargs}") +# Constants for multi-server management +_MAX_SERVERS: Final[int] = 50 +_DEFAULT_SERVER_TIMEOUT: Final[float] = 5.0 - try: - result = await func(self, *args, **kwargs) - duration = time.perf_counter() - start_time - logger.debug(f"{method_name} completed in {duration:.3f}s") - return result - except Exception as e: - duration = time.perf_counter() - start_time - logger.error(f"{method_name} failed after {duration:.3f}s: {e}") - raise +_client_cache: WeakValueDictionary[int, AsyncOutlineClient] = WeakValueDictionary() - return wrapper +class AsyncOutlineClient( + BaseHTTPClient, + ServerMixin, + AccessKeyMixin, + DataLimitMixin, + MetricsMixin, +): + """High-performance async client for Outline VPN Server API.""" -class AsyncOutlineClient: - """ - Asynchronous client for the Outline VPN Server API. - - Args: - api_url: Base URL for the Outline server API - cert_sha256: SHA-256 fingerprint of the server's TLS certificate - json_format: Return raw JSON instead of Pydantic models - timeout: Request timeout in seconds - retry_attempts: Number of retry attempts connecting to the API - enable_logging: Enable debug logging for API calls - user_agent: Custom user agent string - max_connections: Maximum number of connections in the pool - rate_limit_delay: Minimum delay between requests (seconds) - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34...", - ... enable_logging=True - ... ) as client: - ... server_info = await client.get_server_info() - ... print(f"Server: {server_info.name}") - ... - ... # Or use as context manager factory - ... async with AsyncOutlineClient.create( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... await client.get_server_info() - - """ + __slots__ = ( + "_audit_logger_instance", + "_config", + "_default_json_format", + ) def __init__( - self, - api_url: str, - cert_sha256: str, - *, - json_format: bool = False, - timeout: int = 30, - retry_attempts: int = 3, - enable_logging: bool = False, - user_agent: Optional[str] = None, - max_connections: int = 10, - rate_limit_delay: float = 0.0, + self, + config: OutlineClientConfig | None = None, + *, + api_url: str | None = None, + cert_sha256: str | None = None, + audit_logger: AuditLogger | None = None, + metrics: MetricsCollector | None = None, + **overrides: Unpack[ConfigOverrides], ) -> None: + """Initialize Outline client with modern configuration approach. - # Validate api_url - if not api_url or not api_url.strip(): - raise ValueError("api_url cannot be empty or whitespace") - - # Validate cert_sha256 - if not cert_sha256 or not cert_sha256.strip(): - raise ValueError("cert_sha256 cannot be empty or whitespace") - - # Additional validation for cert_sha256 format (should be hex) - cert_sha256_clean = cert_sha256.strip() - if not all(c in '0123456789abcdefABCDEF' for c in cert_sha256_clean): - raise ValueError("cert_sha256 must contain only hexadecimal characters") - - # Check cert_sha256 length (SHA-256 should be 64 hex characters) - if len(cert_sha256_clean) != 64: - raise ValueError("cert_sha256 must be exactly 64 hexadecimal characters (SHA-256)") - - self._api_url = api_url.rstrip("/") - self._cert_sha256 = cert_sha256 - self._json_format = json_format - self._timeout = aiohttp.ClientTimeout(total=timeout) - self._ssl_context: Optional[Fingerprint] = None - self._session: Optional[aiohttp.ClientSession] = None - self._retry_attempts = retry_attempts - self._enable_logging = enable_logging - self._user_agent = user_agent or f"PyOutlineAPI/0.3.0" - self._max_connections = max_connections - self._rate_limit_delay = rate_limit_delay - self._last_request_time: float = 0.0 - - # Health check state - self._last_health_check: float = 0.0 - self._health_check_interval: float = 300.0 # 5 minutes - self._is_healthy: bool = True - - if enable_logging: - self._setup_logging() - - @staticmethod - def _setup_logging() -> None: - """Setup logging configuration if not already configured.""" - if not logger.handlers: - handler = logging.StreamHandler() - formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.setLevel(logging.DEBUG) + Uses structural pattern matching for configuration resolution. - @classmethod - @asynccontextmanager - async def create( - cls, - api_url: str, - cert_sha256: str, - **kwargs - ) -> AsyncGenerator[AsyncOutlineClient, None]: - """ - Factory method that returns an async context manager. + :param config: Client configuration object + :param api_url: API URL (alternative to config) + :param cert_sha256: Certificate fingerprint (alternative to config) + :param audit_logger: Custom audit logger + :param metrics: Custom metrics collector + :param overrides: Configuration overrides (timeout, retry_attempts, etc.) + :raises ConfigurationError: If configuration is invalid - This is the recommended way to create clients for one-off operations. + Example: + >>> async with AsyncOutlineClient.from_env() as client: + ... info = await client.get_server_info() """ - client = cls(api_url, cert_sha256, **kwargs) - async with client: - yield client - - async def __aenter__(self) -> AsyncOutlineClient: - """Set up client session.""" - headers = {"User-Agent": self._user_agent} + # Build config_kwargs using utility function (DRY) + config_kwargs = build_config_overrides(**overrides) - connector = aiohttp.TCPConnector( - ssl=self._get_ssl_context(), - limit=self._max_connections, - limit_per_host=self._max_connections // 2, - enable_cleanup_closed=True, + # Validate configuration using pattern matching + resolved_config = self._resolve_configuration( + config, api_url, cert_sha256, config_kwargs ) - self._session = aiohttp.ClientSession( - timeout=self._timeout, - raise_for_status=False, - connector=connector, - headers=headers, + self._config = resolved_config + self._audit_logger_instance = audit_logger + self._default_json_format = resolved_config.json_format + + # Initialize base HTTP client + super().__init__( + api_url=resolved_config.api_url, + cert_sha256=resolved_config.cert_sha256, + timeout=resolved_config.timeout, + retry_attempts=resolved_config.retry_attempts, + max_connections=resolved_config.max_connections, + user_agent=resolved_config.user_agent, + enable_logging=resolved_config.enable_logging, + circuit_config=resolved_config.circuit_config, + rate_limit=resolved_config.rate_limit, + allow_private_networks=resolved_config.allow_private_networks, + resolve_dns_for_ssrf=resolved_config.resolve_dns_for_ssrf, + audit_logger=audit_logger, + metrics=metrics, ) - if self._enable_logging: - logger.info(f"Initialized OutlineAPI client for {self._api_url}") + # Cache instance for weak reference tracking (automatic cleanup) + _client_cache[id(self)] = self - return self + if resolved_config.enable_logging and logger.isEnabledFor(logging.INFO): + safe_url = Validators.sanitize_url_for_logging(self.api_url) + logger.info("Client initialized for %s", safe_url) - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - """Clean up client session.""" - if self._session: - await self._session.close() - self._session = None + @staticmethod + def _resolve_configuration( + config: OutlineClientConfig | None, + api_url: str | None, + cert_sha256: str | None, + kwargs: dict[str, Any], + ) -> OutlineClientConfig: + """Resolve and validate configuration using pattern matching. + + :param config: Configuration object + :param api_url: Direct API URL + :param cert_sha256: Direct certificate + :param kwargs: Additional kwargs + :return: Resolved configuration + :raises ConfigurationError: If configuration is invalid + """ + match config, api_url, cert_sha256: + # Pattern 1: Direct parameters provided (most common case) + case None, str(url), str(cert) if url and cert: + return OutlineClientConfig.create_minimal(url, cert, **kwargs) + + # Pattern 2: Config object provided + case OutlineClientConfig() as cfg, None, None: + return cfg + + # Pattern 3: Missing required parameters + case None, None, _: + raise ConfigurationError( + "Missing required 'api_url'", + field="api_url", + security_issue=False, + ) + case None, _, None: + raise ConfigurationError( + "Missing required 'cert_sha256'", + field="cert_sha256", + security_issue=True, + ) + + # Pattern 4: Conflicting parameters + case OutlineClientConfig(), str() | None, str() | None: + raise ConfigurationError( + "Cannot specify both 'config' and direct parameters" + ) + + # Pattern 5: Invalid combination (catch-all) + case _: + raise ConfigurationError("Invalid parameter combination") - if self._enable_logging: - logger.info("OutlineAPI client session closed") + @property + def config(self) -> OutlineClientConfig: + """Get immutable copy of configuration. - async def _apply_rate_limiting(self) -> None: - """Apply rate limiting if configured.""" - if self._rate_limit_delay <= 0: - return + :return: Deep copy of configuration + """ + return self._config.model_copy_immutable() - time_since_last = time.time() - self._last_request_time - if time_since_last < self._rate_limit_delay: - delay = self._rate_limit_delay - time_since_last - await asyncio.sleep(delay) + @property + def get_sanitized_config(self) -> dict[str, Any]: + """Delegate to config's sanitized representation. - self._last_request_time = time.time() + See: OutlineClientConfig.get_sanitized_config(). - async def health_check(self, force: bool = False) -> bool: + :return: Sanitized configuration from underlying config object """ - Perform a health check on the Outline server. + return self._config.get_sanitized_config - Args: - force: Force health check even if recently performed + @property + def json_format(self) -> bool: + """Get JSON format preference. - Returns: - True if server is healthy + :return: True if raw JSON format is preferred """ - current_time = time.time() + return self._default_json_format - if not force and (current_time - self._last_health_check) < self._health_check_interval: - return self._is_healthy + # ===== Factory Methods ===== - try: - await self.get_server_info() - self._is_healthy = True - if self._enable_logging: - logger.info("Health check passed") + @classmethod + @asynccontextmanager + async def create( + cls, + api_url: str | None = None, + cert_sha256: str | None = None, + *, + config: OutlineClientConfig | None = None, + audit_logger: AuditLogger | None = None, + metrics: MetricsCollector | None = None, + **overrides: Unpack[ConfigOverrides], + ) -> AsyncGenerator[AsyncOutlineClient, None]: + """Create and initialize client as async context manager. + + Automatically handles initialization and cleanup. + Recommended way to create clients in async contexts. + + :param api_url: API URL + :param cert_sha256: Certificate fingerprint + :param config: Configuration object + :param audit_logger: Custom audit logger + :param metrics: Custom metrics collector + :param overrides: Configuration overrides (timeout, retry_attempts, etc.) + :yield: Initialized client instance + :raises ConfigurationError: If configuration is invalid + + Example: + >>> async with AsyncOutlineClient.from_env() as client: + ... keys = await client.get_access_keys() + """ + if config is not None: + client = cls(config=config, audit_logger=audit_logger, metrics=metrics) + else: + client = cls( + api_url=api_url, + cert_sha256=cert_sha256, + audit_logger=audit_logger, + metrics=metrics, + **overrides, + ) - return self._is_healthy - except Exception as e: - self._is_healthy = False - if self._enable_logging: - logger.warning(f"Health check failed: {e}") - - return self._is_healthy - finally: - self._last_health_check = current_time - - @overload - async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[True], - ) -> JsonDict: - ... - - @overload - async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: Literal[False], - ) -> BaseModel: - ... - - @overload - async def _parse_response( - self, response: ClientResponse, model: type[BaseModel], json_format: bool - ) -> ResponseType: - ... - - @ensure_context - async def _parse_response( - self, - response: ClientResponse, - model: type[BaseModel], - json_format: bool = False, - ) -> ResponseType: - """ - Parse and validate API response data. + async with client: + yield client - Args: - response: API response to parse - model: Pydantic model for validation - json_format: Whether to return raw JSON + @classmethod + def from_env( + cls, + *, + env_file: str | Path | None = None, + audit_logger: AuditLogger | None = None, + metrics: MetricsCollector | None = None, + **overrides: Unpack[ConfigOverrides], + ) -> AsyncOutlineClient: + """Create client from environment variables. + + Reads configuration from environment or .env file. + Modern approach using **overrides for runtime configuration. + + :param env_file: Path to environment file (.env) + :param audit_logger: Custom audit logger + :param metrics: Custom metrics collector + :param overrides: Configuration overrides (timeout, enable_logging, etc.) + :return: Configured client instance + :raises ConfigurationError: If environment configuration is invalid + + Example: + >>> async with AsyncOutlineClient.from_env( + ... env_file=".env.production", + ... timeout=20, + ... ) as client: + ... info = await client.get_server_info() + """ + config = OutlineClientConfig.from_env(env_file=env_file, **overrides) + return cls(config=config, audit_logger=audit_logger, metrics=metrics) + + # ===== Context Manager Methods ===== + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> None: + """Async context manager exit with comprehensive cleanup. + + Ensures graceful shutdown even on exceptions. Uses ordered cleanup + sequence for proper resource deallocation. - Returns: - Validated response data + Cleanup order: + 1. Audit logger shutdown (drain queue) + 2. HTTP client shutdown (close connections) + 3. Emergency cleanup if steps 1-2 failed - Raises: - ValueError: If response validation fails + :param exc_type: Exception type if error occurred + :param exc_val: Exception instance if error occurred + :param exc_tb: Exception traceback + :return: False to propagate exceptions """ - try: - data = await response.json() - validated = model.model_validate(data) - return validated.model_dump(by_alias=True) if json_format else validated - except aiohttp.ContentTypeError as content_error: - raise ValueError("Invalid response format") from content_error - except Exception as exception: - raise ValueError(f"Validation error: {exception}") from exception + cleanup_errors: list[str] = [] - @staticmethod - async def _handle_error_response(response: ClientResponse) -> None: - """Handle error responses from the API.""" + # Step 1: Graceful audit logger shutdown + if self._audit_logger_instance is not None: + try: + if hasattr(self._audit_logger_instance, "shutdown"): + shutdown_method = self._audit_logger_instance.shutdown + if asyncio.iscoroutinefunction(shutdown_method): + await shutdown_method() + except Exception as e: + error_msg = f"Audit logger shutdown error: {e}" + cleanup_errors.append(error_msg) + if logger.isEnabledFor(logging.WARNING): + logger.warning(error_msg) + + # Step 2: Shutdown HTTP client try: - error_data = await response.json() - error = ErrorResponse.model_validate(error_data) - raise APIError(f"{error.code}: {error.message}", response.status) - except (ValueError, aiohttp.ContentTypeError): - raise APIError( - f"HTTP {response.status}: {response.reason}", response.status + await self.shutdown(timeout=30.0) + except Exception as e: + error_msg = f"HTTP client shutdown error: {e}" + cleanup_errors.append(error_msg) + if logger.isEnabledFor(logging.ERROR): + logger.error(error_msg) + + # Step 3: Emergency cleanup if shutdown failed + if cleanup_errors and hasattr(self, "_session"): + try: + if self._session and not self._session.closed: + await self._session.close() + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Emergency session cleanup completed") + except Exception as e: + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Emergency cleanup error: %s", e) + + # Log summary of cleanup issues + if cleanup_errors and logger.isEnabledFor(logging.WARNING): + logger.warning( + "Cleanup completed with %d error(s): %s", + len(cleanup_errors), + "; ".join(cleanup_errors), ) - @ensure_context - async def _request( - self, - method: str, - endpoint: str, - *, - json: Any = None, - params: Optional[JsonDict] = None, - ) -> Any: - """Make an API request.""" - await self._apply_rate_limiting() - url = self._build_url(endpoint) - return await self._make_request(method, url, json, params) - - async def _make_request( - self, - method: str, - url: str, - json: Any = None, - params: Optional[JsonDict] = None, - ) -> Any: - """Internal method to execute the actual request with retry logic.""" - - async def _do_request() -> Any: - if self._enable_logging: - # Don't log sensitive data - safe_url = url.split('?')[0] if '?' in url else url - logger.debug(f"Making {method} request to {safe_url}") - - async with self._session.request( - method, - url, - json=json, - params=params, - raise_for_status=False, - ) as response: - if self._enable_logging: - logger.debug(f"Response: {response.status} {response.reason}") - - if response.status >= 400: - await self._handle_error_response(response) - - if response.status == 204: - return True + # Always propagate the original exception + return None - try: - # See #b1746e6 - await response.json() - return response - except Exception as exception: - raise APIError( - f"Failed to process response: {exception}", response.status - ) + # ===== Utility Methods ===== - return await self._retry_request(_do_request, attempts=self._retry_attempts) + async def health_check(self) -> dict[str, Any]: + """Perform basic health check. - @staticmethod - async def _retry_request( - request_func: Callable[[], Awaitable[T]], - *, - attempts: int = DEFAULT_RETRY_ATTEMPTS, - delay: float = DEFAULT_RETRY_DELAY, - ) -> T: + Non-intrusive check that tests server connectivity without + modifying any state. Returns comprehensive health metrics. + + :return: Health check result dictionary with response time + + Example result: + { + "timestamp": 1234567890.123, + "healthy": True, + "response_time_ms": 45.2, + "connected": True, + "circuit_state": "closed", + "active_requests": 2, + "rate_limit_available": 98 + } """ - Execute request with retry logic. + import time - Args: - request_func: Async function to execute - attempts: Maximum number of retry attempts - delay: Delay between retries in seconds + health_data: dict[str, Any] = { + "timestamp": time.time(), + "connected": self.is_connected, + "circuit_state": self.circuit_state, + "active_requests": self.active_requests, + "rate_limit_available": self.available_slots, + } - Returns: - Response from the successful request + try: + start_time = time.monotonic() + await self.get_server_info() + duration = time.monotonic() - start_time - Raises: - APIError: If all retry attempts fail - """ - last_error = None + health_data["healthy"] = True + health_data["response_time_ms"] = round(duration * 1000, 2) - for attempt in range(attempts): - try: - return await request_func() - except (aiohttp.ClientError, APIError) as error: - last_error = error - - # Don't retry if it's not a retriable error - if isinstance(error, APIError) and ( - error.status_code not in RETRY_STATUS_CODES - ): - raise - - # Don't sleep on the last attempt - if attempt < attempts - 1: - await asyncio.sleep(delay * (attempt + 1)) - - raise APIError( - f"Request failed after {attempts} attempts: {last_error}", - getattr(last_error, "status_code", None), + except Exception as e: + health_data["healthy"] = False + health_data["error"] = str(e) + health_data["error_type"] = type(e).__name__ + + return health_data + + async def get_server_summary(self) -> dict[str, Any]: + """Get comprehensive server overview. + + Aggregates multiple API calls into a single summary. + Continues on partial failures to return maximum information. + Executes non-dependent calls concurrently for performance. + + :return: Server summary dictionary with aggregated data + + Example result: + { + "timestamp": 1234567890.123, + "healthy": True, + "server": {...}, + "access_keys_count": 10, + "metrics_enabled": True, + "transfer_metrics": {...}, + "client_status": {...}, + "errors": [] + } + """ + import time + + summary: dict[str, Any] = { + "timestamp": time.time(), + "healthy": True, + "errors": [], + } + + server_task = self.get_server_info(as_json=True) + keys_task = self.get_access_keys(as_json=True) + metrics_status_task = self.get_metrics_status(as_json=True) + + server_result, keys_result, metrics_status_result = await asyncio.gather( + server_task, keys_task, metrics_status_task, return_exceptions=True ) - def _build_url(self, endpoint: str) -> str: - """Build and validate the full URL for the API request.""" - if not isinstance(endpoint, str): - raise ValueError("Endpoint must be a string") + # Process server info + if isinstance(server_result, Exception): + summary["healthy"] = False + summary["errors"].append(f"Server info error: {server_result}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Failed to fetch server info: %s", server_result) + else: + summary["server"] = server_result - url = f"{self._api_url}/{endpoint.lstrip('/')}" - parsed_url = urlparse(url) + # Process access keys + if isinstance(keys_result, Exception): + summary["healthy"] = False + summary["errors"].append(f"Access keys error: {keys_result}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Failed to fetch access keys: %s", keys_result) + elif isinstance(keys_result, dict): + keys_list = keys_result.get("accessKeys", []) + summary["access_keys_count"] = ( + len(keys_list) if isinstance(keys_list, list) else 0 + ) + elif isinstance(keys_result, AccessKeyList): + summary["access_keys_count"] = len(keys_result.access_keys) + else: + summary["access_keys_count"] = 0 + + # Process metrics status + if isinstance(metrics_status_result, Exception): + summary["errors"].append(f"Metrics status error: {metrics_status_result}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "Failed to fetch metrics status: %s", metrics_status_result + ) + elif isinstance(metrics_status_result, dict): + metrics_enabled = bool(metrics_status_result.get("metricsEnabled", False)) + summary["metrics_enabled"] = metrics_enabled + + # Fetch transfer metrics if enabled (dependent call - sequential) + if metrics_enabled: + try: + transfer = await self.get_transfer_metrics(as_json=True) + summary["transfer_metrics"] = transfer + except Exception as e: + summary["errors"].append(f"Transfer metrics error: {e}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Failed to fetch transfer metrics: %s", e) + elif isinstance(metrics_status_result, MetricsStatusResponse): + summary["metrics_enabled"] = metrics_status_result.metrics_enabled + if metrics_status_result.metrics_enabled: + try: + transfer = await self.get_transfer_metrics(as_json=True) + summary["transfer_metrics"] = transfer + except Exception as e: + summary["errors"].append(f"Transfer metrics error: {e}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("Failed to fetch transfer metrics: %s", e) + else: + summary["metrics_enabled"] = False + + # Add client status (synchronous, no API call) + summary["client_status"] = { + "connected": self.is_connected, + "circuit_state": self.circuit_state, + "active_requests": self.active_requests, + "rate_limit": { + "limit": self.rate_limit, + "available": self.available_slots, + }, + } - if not all([parsed_url.scheme, parsed_url.netloc]): - raise ValueError(f"Invalid URL: {url}") + return summary - return url + def get_status(self) -> dict[str, Any]: + """Get current client status (synchronous). + + Returns immediate status without making API calls. + Useful for monitoring and debugging. + + :return: Status dictionary with all client metrics + + Example result: + { + "connected": True, + "circuit_state": "closed", + "active_requests": 2, + "rate_limit": { + "limit": 100, + "available": 98, + "active": 2 + }, + "circuit_metrics": {...} + } + """ + return { + "connected": self.is_connected, + "circuit_state": self.circuit_state, + "active_requests": self.active_requests, + "rate_limit": { + "limit": self.rate_limit, + "available": self.available_slots, + "active": self.active_requests, + }, + "circuit_metrics": self.get_circuit_metrics(), + } - def _get_ssl_context(self) -> Optional[Fingerprint]: - """Create an SSL context if a certificate fingerprint is provided.""" - if not self._cert_sha256: - return None + def __repr__(self) -> str: + """Safe string representation without secrets. - try: - return Fingerprint(binascii.unhexlify(self._cert_sha256)) - except binascii.Error as validation_error: - raise ValueError( - f"Invalid certificate SHA256: {self._cert_sha256}" - ) from validation_error - except Exception as exception: - raise OutlineError("Failed to create SSL context") from exception - - # Server Management Methods - - @log_method_call - async def get_server_info(self) -> Union[JsonDict, Server]: - """ - Get server information. - - Returns: - Server information including name, ID, and configuration. - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... server = await client.get_server_info() - ... print(f"Server {server.name} running version {server.version}") - """ - response = await self._request("GET", "server") - return await self._parse_response( - response, Server, json_format=self._json_format - ) + Does not expose any sensitive information (URLs, certificates, tokens). - @log_method_call - async def rename_server(self, name: str) -> bool: + :return: String representation """ - Rename the server. - - Args: - name: New server name - - Returns: - True if successful - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... success = await client.rename_server("My VPN Server") - ... if success: - ... print("Server renamed successfully") - """ - request = ServerNameRequest(name=name) - return await self._request( - "PUT", "name", json=request.model_dump(by_alias=True) - ) + status = "connected" if self.is_connected else "disconnected" + parts = [f"status={status}"] - @log_method_call - async def set_hostname(self, hostname: str) -> bool: - """ - Set server hostname for access keys. - - Args: - hostname: New hostname or IP address - - Returns: - True if successful - - Raises: - APIError: If hostname is invalid - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... await client.set_hostname("vpn.example.com") - ... # Or use IP address - ... await client.set_hostname("203.0.113.1") - """ - request = HostnameRequest(hostname=hostname) - return await self._request( - "PUT", - "server/hostname-for-access-keys", - json=request.model_dump(by_alias=True), - ) + if self.circuit_state: + parts.append(f"circuit={self.circuit_state}") - @log_method_call - async def set_default_port(self, port: int) -> bool: - """ - Set default port for new access keys. + if self.active_requests: + parts.append(f"requests={self.active_requests}") - Args: - port: Port number (1025-65535) + return f"AsyncOutlineClient({', '.join(parts)})" - Returns: - True if successful - Raises: - APIError: If port is invalid or in use +# ===== Multi-Server Manager ===== - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... await client.set_default_port(8388) - """ - if port < MIN_PORT or port > MAX_PORT: - raise ValueError( - f"Privileged ports are not allowed. Use range: {MIN_PORT}-{MAX_PORT}" - ) - request = PortRequest(port=port) - return await self._request( - "PUT", - "server/port-for-new-access-keys", - json=request.model_dump(by_alias=True), - ) +class MultiServerManager: + """High-performance manager for multiple Outline servers. - # Metrics Methods + Features: + - Concurrent operations across all servers + - Health checking and automatic failover + - Aggregated metrics and status + - Graceful shutdown with cleanup + - Thread-safe operations - @log_method_call - async def get_metrics_status(self) -> Union[JsonDict, MetricsStatusResponse]: - """ - Get whether metrics collection is enabled. - - Returns: - Current metrics collection status - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... status = await client.get_metrics_status() - ... if status.metrics_enabled: - ... print("Metrics collection is enabled") - """ - response = await self._request("GET", "metrics/enabled") - return await self._parse_response( - response, MetricsStatusResponse, json_format=self._json_format - ) + Limits: + - Maximum 50 servers (configurable via _MAX_SERVERS) + - Automatic cleanup with weak references + """ - @log_method_call - async def set_metrics_status(self, enabled: bool) -> bool: - """ - Enable or disable metrics collection. - - Args: - enabled: Whether to enable metrics - - Returns: - True if successful - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... # Enable metrics - ... await client.set_metrics_status(True) - ... # Check new status - ... status = await client.get_metrics_status() - """ - request = MetricsEnabledRequest(metricsEnabled=enabled) - return await self._request( - "PUT", "metrics/enabled", json=request.model_dump(by_alias=True) - ) + __slots__ = ( + "_audit_logger", + "_clients", + "_configs", + "_default_timeout", + "_lock", + "_metrics", + ) - @log_method_call - async def get_transfer_metrics(self) -> Union[JsonDict, ServerMetrics]: - """ - Get transfer metrics for all access keys. - - Returns: - Transfer metrics data for each access key - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... metrics = await client.get_transfer_metrics() - ... for user_id, bytes_transferred in metrics.bytes_transferred_by_user_id.items(): - ... print(f"User {user_id}: {bytes_transferred / 1024**3:.2f} GB") - """ - response = await self._request("GET", "metrics/transfer") - return await self._parse_response( - response, ServerMetrics, json_format=self._json_format - ) + def __init__( + self, + configs: Sequence[OutlineClientConfig], + *, + audit_logger: AuditLogger | None = None, + metrics: MetricsCollector | None = None, + default_timeout: float = _DEFAULT_SERVER_TIMEOUT, + ) -> None: + """Initialize multiserver manager. - @log_method_call - async def get_experimental_metrics( - self, since: str - ) -> Union[JsonDict, ExperimentalMetrics]: + :param configs: Sequence of server configurations + :param audit_logger: Shared audit logger for all servers + :param metrics: Shared metrics collector for all servers + :param default_timeout: Default timeout for operations (seconds) + :raises ConfigurationError: If too many servers or invalid configs """ - Get experimental server metrics. - - Args: - since: Required time range filter (e.g., "24h", "7d", "30d", or ISO timestamp) - - Returns: - Detailed server and access key metrics - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... # Get metrics for the last 24 hours - ... metrics = await client.get_experimental_metrics("24h") - ... print(f"Server tunnel time: {metrics.server.tunnel_time.seconds}s") - ... print(f"Server data transferred: {metrics.server.data_transferred.bytes} bytes") - ... - ... # Get metrics for the last 7 days - ... metrics = await client.get_experimental_metrics("7d") - ... - ... # Get metrics since specific timestamp - ... metrics = await client.get_experimental_metrics("2024-01-01T00:00:00Z") - """ - if not since or not since.strip(): - raise ValueError("Parameter 'since' is required and cannot be empty") + if len(configs) > _MAX_SERVERS: + raise ConfigurationError( + f"Too many servers: {len(configs)} (max: {_MAX_SERVERS})" + ) - params = {"since": since} - response = await self._request( - "GET", "experimental/server/metrics", params=params - ) - return await self._parse_response( - response, ExperimentalMetrics, json_format=self._json_format - ) + if not configs: + raise ConfigurationError("At least one server configuration required") - # Access Key Management Methods - - @log_method_call - async def create_access_key( - self, - *, - name: Optional[str] = None, - password: Optional[str] = None, - port: Optional[int] = None, - method: Optional[str] = None, - limit: Optional[DataLimit] = None, - ) -> Union[JsonDict, AccessKey]: - """ - Create a new access key. - - Args: - name: Optional key name - password: Optional password - port: Optional port number (1-65535) - method: Optional encryption method - limit: Optional data transfer limit - - Returns: - New access key details - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... # Create basic key - ... key = await client.create_access_key(name="User 1") - ... - ... # Create key with data limit - ... lim = DataLimit(bytes=5 * 1024**3) # 5 GB - ... key = await client.create_access_key( - ... name="Limited User", - ... port=8388, - ... limit=lim - ... ) - ... print(f"Created key: {key.access_url}") - """ - request = AccessKeyCreateRequest( - name=name, password=password, port=port, method=method, limit=limit - ) - response = await self._request( - "POST", - "access-keys", - json=request.model_dump(exclude_none=True, by_alias=True), - ) - return await self._parse_response( - response, AccessKey, json_format=self._json_format - ) - - @log_method_call - async def create_access_key_with_id( - self, - key_id: str, - *, - name: Optional[str] = None, - password: Optional[str] = None, - port: Optional[int] = None, - method: Optional[str] = None, - limit: Optional[DataLimit] = None, - ) -> Union[JsonDict, AccessKey]: - """ - Create a new access key with specific ID. - - Args: - key_id: Specific ID for the access key - name: Optional key name - password: Optional password - port: Optional port number (1-65535) - method: Optional encryption method - limit: Optional data transfer limit - - Returns: - New access key details - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... key = await client.create_access_key_with_id( - ... "my-custom-id", - ... name="Custom Key" - ... ) - """ - request = AccessKeyCreateRequest( - name=name, password=password, port=port, method=method, limit=limit - ) - response = await self._request( - "PUT", - f"access-keys/{key_id}", - json=request.model_dump(exclude_none=True, by_alias=True), - ) - return await self._parse_response( - response, AccessKey, json_format=self._json_format - ) + self._configs = list(configs) + self._clients: dict[str, AsyncOutlineClient] = {} + self._audit_logger = audit_logger + self._metrics = metrics + self._default_timeout = default_timeout + self._lock = asyncio.Lock() - @log_method_call - async def get_access_keys(self) -> Union[JsonDict, AccessKeyList]: - """ - Get all access keys. - - Returns: - List of all access keys - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... keys = await client.get_access_keys() - ... for key in keys.access_keys: - ... print(f"Key {key.id}: {key.name or 'unnamed'}") - ... if key.data_limit: - ... print(f" Limit: {key.data_limit.bytes / 1024**3:.1f} GB") - """ - response = await self._request("GET", "access-keys") - return await self._parse_response( - response, AccessKeyList, json_format=self._json_format - ) + @property + def server_count(self) -> int: + """Get total number of configured servers. - @log_method_call - async def get_access_key(self, key_id: str) -> Union[JsonDict, AccessKey]: - """ - Get specific access key. - - Args: - key_id: Access key ID - - Returns: - Access key details - - Raises: - APIError: If key doesn't exist - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... key = await client.get_access_key("1") - ... print(f"Port: {key.port}") - ... print(f"URL: {key.access_url}") + :return: Number of servers """ - response = await self._request("GET", f"access-keys/{key_id}") - return await self._parse_response( - response, AccessKey, json_format=self._json_format - ) + return len(self._configs) - @log_method_call - async def rename_access_key(self, key_id: str, name: str) -> bool: - """ - Rename access key. - - Args: - key_id: Access key ID - name: New name - - Returns: - True if successful - - Raises: - APIError: If key doesn't exist - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... # Rename key - ... await client.rename_access_key("1", "Alice") - ... - ... # Verify new name - ... key = await client.get_access_key("1") - ... assert key.name == "Alice" - """ - request = AccessKeyNameRequest(name=name) - return await self._request( - "PUT", f"access-keys/{key_id}/name", json=request.model_dump(by_alias=True) - ) + @property + def active_servers(self) -> int: + """Get number of active (connected) servers. - @log_method_call - async def delete_access_key(self, key_id: str) -> bool: + :return: Number of active servers """ - Delete access key. + return sum(1 for client in self._clients.values() if client.is_connected) - Args: - key_id: Access key ID + def get_server_names(self) -> list[str]: + """Get list of sanitized server URLs. - Returns: - True if successful + URLs are sanitized to remove sensitive path information. - Raises: - APIError: If key doesn't exist - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... if await client.delete_access_key("1"): - ... print("Key deleted") + :return: List of safe server identifiers """ - return await self._request("DELETE", f"access-keys/{key_id}") + return [ + Validators.sanitize_url_for_logging(config.api_url) + for config in self._configs + ] - @log_method_call - async def set_access_key_data_limit(self, key_id: str, bytes_limit: int) -> bool: - """ - Set data transfer limit for access key. - - Args: - key_id: Access key ID - bytes_limit: Limit in bytes (must be non-negative) - - Returns: - True if successful - - Raises: - APIError: If key doesn't exist or limit is invalid - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... # Set 5 GB limit - ... limit = 5 * 1024**3 # 5 GB in bytes - ... await client.set_access_key_data_limit("1", limit) - ... - ... # Verify limit - ... key = await client.get_access_key("1") - ... assert key.data_limit and key.data_limit.bytes == limit - """ - request = DataLimitRequest(limit=DataLimit(bytes=bytes_limit)) - return await self._request( - "PUT", - f"access-keys/{key_id}/data-limit", - json=request.model_dump(by_alias=True), - ) + async def __aenter__(self) -> MultiServerManager: + """Async context manager entry. - @log_method_call - async def remove_access_key_data_limit(self, key_id: str) -> bool: + :return: Self reference + :raises ConfigurationError: If NO servers can be initialized """ - Remove data transfer limit from access key. + async with self._lock: + # Create initialization tasks for concurrent execution + init_tasks = [] + for config in self._configs: + client = AsyncOutlineClient( + config=config, + audit_logger=self._audit_logger, + metrics=self._metrics, + ) + init_tasks.append((config, client.__aenter__())) - Args: - key_id: Access key ID + results = await asyncio.gather( + *[task for _, task in init_tasks], + return_exceptions=True, + ) - Returns: - True if successful + # Process results + errors: list[str] = [] + for idx, ((config, _), result) in enumerate( + zip(init_tasks, results, strict=True) + ): + safe_url = Validators.sanitize_url_for_logging(config.api_url) + + if isinstance(result, Exception): + error_msg = f"Failed to initialize server {safe_url}: {result}" + errors.append(error_msg) + if logger.isEnabledFor(logging.WARNING): + logger.warning(error_msg) + else: + # Get the client that was initialized + client = AsyncOutlineClient( + config=config, + audit_logger=self._audit_logger, + metrics=self._metrics, + ) + self._clients[safe_url] = client + + if logger.isEnabledFor(logging.INFO): + logger.info( + "Server %d/%d initialized: %s", + idx + 1, + len(self._configs), + safe_url, + ) + + if not self._clients: + raise ConfigurationError( + f"Failed to initialize any servers. Errors: {'; '.join(errors)}" + ) + + if logger.isEnabledFor(logging.INFO): + logger.info( + "MultiServerManager ready: %d/%d servers active", + len(self._clients), + len(self._configs), + ) - Raises: - APIError: If key doesn't exist + return self - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... await client.remove_access_key_data_limit("1") - """ - return await self._request("DELETE", f"access-keys/{key_id}/data-limit") + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> bool: + """Async context manager exit. + + :param exc_type: Exception type + :param exc_val: Exception value + :param exc_tb: Exception traceback + :return: False to propagate exceptions + """ + async with self._lock: + shutdown_tasks = [ + client.__aexit__(None, None, None) for client in self._clients.values() + ] + + results = await asyncio.gather(*shutdown_tasks, return_exceptions=True) + + errors = [ + f"{server_id}: {result}" + for (server_id, _), result in zip( + self._clients.items(), results, strict=False + ) + if isinstance(result, Exception) + ] + + self._clients.clear() + + if errors and logger.isEnabledFor(logging.WARNING): + logger.warning("Shutdown completed with %d error(s)", len(errors)) + + return False + + def get_client(self, server_identifier: str | int) -> AsyncOutlineClient: + """Get client by server identifier or index. + + :param server_identifier: Server URL (sanitized) or 0-based index + :return: Client instance + :raises KeyError: If server not found + :raises IndexError: If index out of range + """ + # Try as index first (fast path for common case) + if isinstance(server_identifier, int): + if 0 <= server_identifier < len(self._configs): + config = self._configs[server_identifier] + safe_url = Validators.sanitize_url_for_logging(config.api_url) + return self._clients[safe_url] + raise IndexError( + f"Server index {server_identifier} out of range (0-{len(self._configs) - 1})" + ) - # Global Data Limit Methods + # Try as server ID + if server_identifier in self._clients: + return self._clients[server_identifier] - @log_method_call - async def set_global_data_limit(self, bytes_limit: int) -> bool: - """ - Set global data transfer limit for all access keys. - - Args: - bytes_limit: Limit in bytes (must be non-negative) - - Returns: - True if successful - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... # Set 100 GB global limit - ... await client.set_global_data_limit(100 * 1024**3) - """ - request = DataLimitRequest(limit=DataLimit(bytes=bytes_limit)) - return await self._request( - "PUT", - "server/access-key-data-limit", - json=request.model_dump(by_alias=True), - ) + raise KeyError(f"Server not found: {server_identifier}") - @log_method_call - async def remove_global_data_limit(self) -> bool: - """ - Remove global data transfer limit. - - Returns: - True if successful - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... await client.remove_global_data_limit() + def get_all_clients(self) -> list[AsyncOutlineClient]: + """Get all active clients. + + :return: List of client instances """ - return await self._request("DELETE", "server/access-key-data-limit") + return list(self._clients.values()) - # Batch Operations + async def health_check_all( + self, + timeout: float | None = None, + ) -> dict[str, dict[str, Any]]: + """Perform health check on all servers concurrently. - async def batch_create_access_keys( - self, - keys_config: list[dict[str, Any]], - fail_fast: bool = True - ) -> list[Union[AccessKey, Exception]]: + :param timeout: Timeout for each health check + :return: Dictionary mapping server IDs to health check results """ - Create multiple access keys in batch. - - Args: - keys_config: List of key configurations (same as create_access_key kwargs) - fail_fast: If True, stop on first error. If False, continue and return errors. - - Returns: - List of created keys or exceptions - - Examples: - >>> async def main(): - ... async with AsyncOutlineClient( - ... "https://example.com:1234/secret", - ... "ab12cd34..." - ... ) as client: - ... configs = [ - ... {"name": "User1", "limit": DataLimit(bytes=1024**3)}, - ... {"name": "User2", "port": 8388}, - ... ] - ... res = await client.batch_create_access_keys(configs) - """ - results = [] + timeout = timeout or self._default_timeout - for config in keys_config: - try: - key = await self.create_access_key(**config) - results.append(key) - except Exception as e: - if fail_fast: - raise - results.append(e) + tasks = [ + self._health_check_single(server_id, client, timeout) + for server_id, client in self._clients.items() + ] - return results + # Execute concurrently + results_list = await asyncio.gather(*tasks, return_exceptions=True) - async def get_server_summary(self, metrics_since: str = "24h") -> dict[str, Any]: - """ - Get comprehensive server summary including info, metrics, and key count. + # Build result dictionary + results: dict[str, dict[str, Any]] = {} + for (server_id, _), result in zip( + self._clients.items(), results_list, strict=False + ): + if isinstance(result, BaseException): + results[server_id] = { + "healthy": False, + "error": str(result), + "error_type": type(result).__name__, + } + else: + results[server_id] = result - Args: - metrics_since: Time range for experimental metrics (default: "24h") + return results - Returns: - Dictionary with server info, health status, and statistics + @staticmethod + async def _health_check_single( + server_id: str, + client: AsyncOutlineClient, + timeout: float, + ) -> dict[str, Any]: + """Perform health check on a single server with timeout. + + :param server_id: Server identifier + :param client: Client instance + :param timeout: Timeout for operation + :return: Health check result """ - summary = {} - try: - # Get basic server info - server_info = await self.get_server_info() - summary["server"] = server_info.model_dump() if isinstance(server_info, BaseModel) else server_info + result = await asyncio.wait_for( + client.health_check(), + timeout=timeout, + ) + result["server_id"] = server_id + return result + except asyncio.TimeoutError: + return { + "server_id": server_id, + "healthy": False, + "error": f"Health check timeout after {timeout}s", + "error_type": "TimeoutError", + } + except Exception as e: + return { + "server_id": server_id, + "healthy": False, + "error": str(e), + "error_type": type(e).__name__, + } + + async def get_healthy_servers( + self, + timeout: float | None = None, + ) -> list[AsyncOutlineClient]: + """Get list of healthy servers after health check. + + :param timeout: Timeout for health checks + :return: List of healthy clients + """ + health_results = await self.health_check_all(timeout=timeout) + + healthy_clients: list[AsyncOutlineClient] = [] + for server_id, result in health_results.items(): + if result.get("healthy", False): + try: + client = self.get_client(server_id) + healthy_clients.append(client) + except (KeyError, IndexError): + continue - # Get access keys count - keys = await self.get_access_keys() - key_list = keys.access_keys if isinstance(keys, BaseModel) else keys.get("accessKeys", []) - summary["access_keys_count"] = len(key_list) + return healthy_clients - # Get metrics if available - try: - metrics_status = await self.get_metrics_status() - if (isinstance(metrics_status, BaseModel) and metrics_status.metrics_enabled) or \ - (isinstance(metrics_status, dict) and metrics_status.get("metricsEnabled")): - transfer_metrics = await self.get_transfer_metrics() - summary["transfer_metrics"] = transfer_metrics.model_dump() if isinstance(transfer_metrics, - BaseModel) else transfer_metrics - - # Try to get experimental metrics - try: - experimental_metrics = await self.get_experimental_metrics(metrics_since) - summary["experimental_metrics"] = experimental_metrics.model_dump() if isinstance( - experimental_metrics, - BaseModel) else experimental_metrics - except Exception: - summary["experimental_metrics"] = None - except Exception: - summary["transfer_metrics"] = None - summary["experimental_metrics"] = None - - summary["healthy"] = True + def get_status_summary(self) -> dict[str, Any]: + """Get aggregated status summary for all servers. - except Exception as e: - summary["healthy"] = False - summary["error"] = str(e) + Synchronous operation - no API calls made. - return summary + :return: Status summary dictionary + """ + return { + "total_servers": len(self._configs), + "active_servers": self.active_servers, + "server_statuses": { + server_id: client.get_status() + for server_id, client in self._clients.items() + }, + } - # Utility and management methods + def __repr__(self) -> str: + """String representation. - def configure_logging(self, level: str = "INFO", format_string: Optional[str] = None) -> None: + :return: String representation """ - Configure logging for the client. + active = self.active_servers + total = self.server_count + return f"MultiServerManager(servers={active}/{total} active)" - Args: - level: Logging level (DEBUG, INFO, WARNING, ERROR) - format_string: Custom format string for log messages - """ - self._enable_logging = True - # Clear existing handlers - logger.handlers.clear() +# ===== Convenience Functions ===== - handler = logging.StreamHandler() - if format_string: - formatter = logging.Formatter(format_string) - else: - formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.setLevel(getattr(logging, level.upper())) - @property - def is_healthy(self) -> bool: - """Check if the last health check passed.""" - return self._is_healthy +def create_client( + api_url: str, + cert_sha256: str, + *, + audit_logger: AuditLogger | None = None, + metrics: MetricsCollector | None = None, + **overrides: Unpack[ConfigOverrides], +) -> AsyncOutlineClient: + """Create client with minimal parameters. - @property - def session(self) -> Optional[aiohttp.ClientSession]: - """Access the current client session.""" - return self._session + Convenience function for quick client creation without + explicit configuration object. Uses modern **overrides approach. - @property - def api_url(self) -> str: - """Get the API URL (without sensitive parts).""" - from urllib.parse import urlparse - parsed = urlparse(self._api_url) - return f"{parsed.scheme}://{parsed.netloc}" + :param api_url: API URL with secret path + :param cert_sha256: SHA-256 certificate fingerprint + :param audit_logger: Custom audit logger (optional) + :param metrics: Custom metrics collector (optional) + :param overrides: Configuration overrides (timeout, retry_attempts, etc.) + :return: Configured client instance (use with async context manager) + :raises ConfigurationError: If parameters are invalid - def __repr__(self) -> str: - """String representation of the client.""" - status = "connected" if self._session and not self._session.closed else "disconnected" - return f"AsyncOutlineClient(url={self.api_url}, status={status})" + Example (advanced, prefer from_env for production): + >>> async with AsyncOutlineClient.from_env() as client: + ... info = await client.get_server_info() + """ + return AsyncOutlineClient( + api_url=api_url, + cert_sha256=cert_sha256, + audit_logger=audit_logger, + metrics=metrics, + **overrides, + ) + + +def create_multi_server_manager( + configs: Sequence[OutlineClientConfig], + *, + audit_logger: AuditLogger | None = None, + metrics: MetricsCollector | None = None, + default_timeout: float = _DEFAULT_SERVER_TIMEOUT, +) -> MultiServerManager: + """Create multiserver manager with configurations. + + Convenience function for creating a manager for multiple servers. + + :param configs: Sequence of server configurations + :param audit_logger: Shared audit logger + :param metrics: Shared metrics collector + :param default_timeout: Default operation timeout + :return: MultiServerManager instance (use with async context manager) + :raises ConfigurationError: If configurations are invalid + + Example: + >>> configs = [ + ... OutlineClientConfig.create_minimal("https://s1.com/path", "a" * 64), + ... OutlineClientConfig.create_minimal("https://s2.com/path", "b" * 64), + ... ] + >>> async with create_multi_server_manager(configs) as manager: + ... health = await manager.health_check_all() + """ + return MultiServerManager( + configs=configs, + audit_logger=audit_logger, + metrics=metrics, + default_timeout=default_timeout, + ) + + +__all__ = [ + "AsyncOutlineClient", + "MultiServerManager", + "create_client", + "create_multi_server_manager", +] diff --git a/pyoutlineapi/common_types.py b/pyoutlineapi/common_types.py new file mode 100644 index 0000000..7d0cc32 --- /dev/null +++ b/pyoutlineapi/common_types.py @@ -0,0 +1,921 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import ipaddress +import logging +import re +import secrets +import socket +import sys +import time +import urllib.parse +from datetime import datetime +from functools import lru_cache +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Final, + TypeAlias, + TypedDict, + TypeGuard, +) +from urllib.parse import urlparse + +from pydantic import BaseModel, ConfigDict, Field, SecretStr + +if TYPE_CHECKING: + from .models import DataLimit + +if TYPE_CHECKING: + from collections.abc import Mapping + +# ===== Type Aliases - Core Types ===== + +Port: TypeAlias = Annotated[ + int, Field(ge=1, le=65535, description="Port number (1-65535)") +] +Bytes: TypeAlias = Annotated[int, Field(ge=0, description="Size in bytes")] + +TimestampMs: TypeAlias = Annotated[ + int, Field(ge=0, description="Unix timestamp in milliseconds") +] +TimestampSec: TypeAlias = Annotated[ + int, Field(ge=0, description="Unix timestamp in seconds") +] + +# Backward compatibility +Timestamp: TypeAlias = TimestampMs + +# ===== Type Aliases - JSON and API Types ===== + +JsonPrimitive: TypeAlias = str | int | float | bool | None +JsonDict: TypeAlias = dict[str, "JsonValue"] +JsonList: TypeAlias = list["JsonValue"] +JsonValue: TypeAlias = JsonPrimitive | JsonDict | JsonList +JsonPayload: TypeAlias = JsonDict | JsonList | None +ResponseData: TypeAlias = JsonDict +QueryParams: TypeAlias = dict[str, str | int | float | bool] + +# ===== Type Aliases - Common Structures ===== + +ChecksDict: TypeAlias = dict[str, dict[str, Any]] +BytesPerUserDict: TypeAlias = dict[str, int] +AuditDetails: TypeAlias = dict[str, str | int | float | bool] +MetricsTags: TypeAlias = dict[str, str] + + +# ===== Constants ===== + + +class Constants: + """Application-wide constants with security limits.""" + + # Port constraints + MIN_PORT: Final[int] = 1 + MAX_PORT: Final[int] = 65535 + + # Length limits + MAX_NAME_LENGTH: Final[int] = 255 + CERT_FINGERPRINT_LENGTH: Final[int] = 64 + MAX_KEY_ID_LENGTH: Final[int] = 255 + MAX_URL_LENGTH: Final[int] = 2048 + + # Network defaults + DEFAULT_TIMEOUT: Final[int] = 10 + DEFAULT_RETRY_ATTEMPTS: Final[int] = 2 + DEFAULT_MIN_CONNECTIONS: Final[int] = 1 + DEFAULT_MAX_CONNECTIONS: Final[int] = 100 + DEFAULT_RETRY_DELAY: Final[float] = 1.0 + DEFAULT_MIN_TIMEOUT: Final[int] = 1 + DEFAULT_MAX_TIMEOUT: Final[int] = 300 + DEFAULT_USER_AGENT: Final[str] = "PyOutlineAPI/0.4.0" + _MIN_RATE_LIMIT: Final[int] = 1 + _MAX_RATE_LIMIT: Final[int] = 1000 + _SAFETY_MARGIN: Final[float] = 10.0 + + # Resource limits + MAX_RECURSION_DEPTH: Final[int] = 10 + MAX_SNAPSHOT_SIZE_MB: Final[int] = 10 + + # HTTP retry codes + RETRY_STATUS_CODES: Final[frozenset[int]] = frozenset( + {408, 429, 500, 502, 503, 504} + ) + + # Logging levels + LOG_LEVEL_DEBUG: Final[int] = logging.DEBUG + LOG_LEVEL_INFO: Final[int] = logging.INFO + LOG_LEVEL_WARNING: Final[int] = logging.WARNING + LOG_LEVEL_ERROR: Final[int] = logging.ERROR + + # ===== Security limits ===== + + # Response size protection (DoS prevention) + MAX_RESPONSE_SIZE: Final[int] = 10 * 1024 * 1024 # 10 MB + MAX_RESPONSE_CHUNK_SIZE: Final[int] = 8192 # 8 KB chunks + + # Rate limiting defaults + DEFAULT_RATE_LIMIT_RPS: Final[float] = 100.0 # Requests per second + DEFAULT_RATE_LIMIT_BURST: Final[int] = 200 # Burst capacity + DEFAULT_RATE_LIMIT: Final[int] = 100 # Concurrent requests + + # Connection limits + MAX_CONNECTIONS_PER_HOST: Final[int] = 50 + DNS_CACHE_TTL: Final[int] = 300 # 5 minutes + + # Timeout strategies + TIMEOUT_WARNING_RATIO: Final[float] = 0.8 # Warn at 80% of timeout + MAX_TIMEOUT: Final[int] = 300 # 5 minutes absolute max + + +# ===== SSRF Protection (HIGH-002) ===== + + +class SSRFProtection: + """SSRF protection with blocked IP ranges.""" + + # Private and special-use IP ranges to block + BLOCKED_IP_RANGES: Final[list[ipaddress.IPv4Network | ipaddress.IPv6Network]] = [ + ipaddress.ip_network("0.0.0.0/8"), # Current network + ipaddress.ip_network("10.0.0.0/8"), # Private + ipaddress.ip_network("127.0.0.0/8"), # Loopback + ipaddress.ip_network("169.254.0.0/16"), # Link-local + ipaddress.ip_network("172.16.0.0/12"), # Private + ipaddress.ip_network("192.168.0.0/16"), # Private + ipaddress.ip_network("224.0.0.0/4"), # Multicast + ipaddress.ip_network("240.0.0.0/4"), # Reserved + ipaddress.ip_network("::1/128"), # IPv6 loopback + ipaddress.ip_network("fc00::/7"), # IPv6 private + ipaddress.ip_network("fe80::/10"), # IPv6 link-local + ] + + # Allowed localhost for development + ALLOWED_LOCALHOST: Final[frozenset[str]] = frozenset( + {"localhost", "127.0.0.1", "::1"} + ) + + @classmethod + @lru_cache(maxsize=256) + def is_blocked_ip(cls, hostname: str) -> bool: + """Check if hostname resolves to blocked IP range (CACHED). + + :param hostname: Hostname or IP address + :return: True if blocked + """ + # Allow localhost in development + if hostname in cls.ALLOWED_LOCALHOST: + return False + + try: + ip = ipaddress.ip_address(hostname) + return any(ip in blocked for blocked in cls.BLOCKED_IP_RANGES) + except ValueError: + # Not an IP address, hostname is OK at this stage + # DNS resolution happens at connection time + return False + + @classmethod + def _resolve_hostname_inner( + cls, hostname: str + ) -> tuple[ipaddress._BaseAddress, ...]: + """Resolve hostname to IPs. + + :param hostname: Hostname to resolve + :return: Tuple of resolved IP addresses + :raises ValueError: If resolution fails + """ + try: + infos = socket.getaddrinfo(hostname, None) + except socket.gaierror as e: + raise ValueError(f"Unable to resolve hostname: {hostname}") from e + + addresses: list[ipaddress._BaseAddress] = [] + for info in infos: + ip_str = info[4][0] + try: + addresses.append(ipaddress.ip_address(ip_str)) + except ValueError: + continue + + if not addresses: + raise ValueError(f"Unable to resolve hostname: {hostname}") + + return tuple(addresses) + + @classmethod + @lru_cache(maxsize=256) + def _resolve_hostname(cls, hostname: str) -> tuple[ipaddress._BaseAddress, ...]: + """Resolve hostname to IPs (cached). + + :param hostname: Hostname to resolve + :return: Tuple of resolved IP addresses + :raises ValueError: If resolution fails + """ + return cls._resolve_hostname_inner(hostname) + + @classmethod + def _resolve_hostname_uncached( + cls, hostname: str + ) -> tuple[ipaddress._BaseAddress, ...]: + """Resolve hostname to IPs (uncached). + + :param hostname: Hostname to resolve + :return: Tuple of resolved IP addresses + :raises ValueError: If resolution fails + """ + return cls._resolve_hostname_inner(hostname) + + @classmethod + def is_blocked_hostname(cls, hostname: str) -> bool: + """Resolve hostname and check if any IP is blocked. + + Blocks if any resolved IP is in a private/reserved range to guard against + DNS rebinding and mixed public/private records. + + :param hostname: Hostname to resolve and validate + :return: True if blocked + :raises ValueError: If resolution fails + """ + if hostname in cls.ALLOWED_LOCALHOST: + return False + + for ip in cls._resolve_hostname(hostname): + if any(ip in blocked for blocked in cls.BLOCKED_IP_RANGES): + return True + return False + + @classmethod + def is_blocked_hostname_uncached(cls, hostname: str) -> bool: + """Resolve hostname without cache and check if any IP is blocked. + + :param hostname: Hostname to resolve and validate + :return: True if blocked + :raises ValueError: If resolution fails + """ + if hostname in cls.ALLOWED_LOCALHOST: + return False + + for ip in cls._resolve_hostname_uncached(hostname): + if any(ip in blocked for blocked in cls.BLOCKED_IP_RANGES): + return True + return False + + +# ===== Credential Sanitization ===== + + +class CredentialSanitizer: + """Sanitize credentials from strings and exceptions.""" + + # Patterns for detecting credentials + PATTERNS: Final[list[tuple[re.Pattern[str], str]]] = [ + ( + re.compile( + r'api[_-]?key["\']?\s*[:=]\s*["\']?([a-zA-Z0-9_\-\.]{20,})', + re.IGNORECASE, + ), + "***API_KEY***", + ), + ( + re.compile( + r'token["\']?\s*[:=]\s*["\']?([a-zA-Z0-9_\-.]{20,})', re.IGNORECASE + ), + "***TOKEN***", + ), + ( + re.compile(r'password["\']?\s*[:=]\s*["\']?([^\s"\']+)', re.IGNORECASE), + "***PASSWORD***", + ), + ( + re.compile( + r'cert[_-]?sha256["\']?\s*[:=]\s*["\']?([a-f0-9]{64})', re.IGNORECASE + ), + "***CERT***", + ), + ( + re.compile(r"bearer\s+([a-zA-Z0-9\-._~+/]+=*)", re.IGNORECASE), + "Bearer ***TOKEN***", + ), + ( + re.compile(r"access_url['\"]?\s*[:=]\s*['\"]?([^\s'\"]+)", re.IGNORECASE), + "***ACCESS_URL***", + ), + ] + + @classmethod + @lru_cache(maxsize=512) + def sanitize(cls, text: str) -> str: + """Remove credentials from string. + + :param text: Text that may contain credentials + :return: Sanitized text + """ + if not text: + return text + + sanitized = text + for pattern, replacement in cls.PATTERNS: + sanitized = pattern.sub(replacement, sanitized) + return sanitized + + +# ===== Secure ID Generation ===== + + +class SecureIDGenerator: + """Cryptographically secure ID generation.""" + + __slots__ = () + + @staticmethod + def generate_correlation_id() -> str: + """Generate secure correlation ID with 128 bits entropy. + + Format: {timestamp_us}-{random_hex} + + :return: Correlation ID string + """ + # 16 bytes = 128 bits of entropy + random_part = secrets.token_hex(16) + + # Microsecond timestamp for uniqueness and ordering + timestamp = int(time.time() * 1_000_000) + + return f"{timestamp}-{random_part}" + + @staticmethod + def generate_request_id() -> str: + """Generate secure request ID. + + Alias for correlation ID for API compatibility. + + :return: Request ID string + """ + return SecureIDGenerator.generate_correlation_id() + + +# ===== Enhanced Sensitive Keys ===== + +DEFAULT_SENSITIVE_KEYS: Final[frozenset[str]] = frozenset( + { + "password", + "api_key", + "apiKey", + "apikey", + "token", + "secret", + "cert_sha256", + "certSha256", + "access_url", + "accessUrl", + "authorization", + "api_url", + "apiUrl", + } +) + + +# ===== Type Guards ===== + + +def is_valid_port(value: object) -> TypeGuard[int]: + """Type guard for valid port numbers. + + :param value: Value to check + :return: True if value is valid port + """ + return isinstance(value, int) and Constants.MIN_PORT <= value <= Constants.MAX_PORT + + +def is_valid_bytes(value: object) -> TypeGuard[int]: + """Type guard for valid byte counts. + + :param value: Value to check + :return: True if value is valid bytes + """ + return isinstance(value, int) and value >= 0 + + +def is_json_serializable(value: object) -> TypeGuard[JsonValue]: + """Type guard for JSON-serializable values. + + :param value: Value to check + :return: True if value is JSON-serializable + """ + if value is None or isinstance(value, str | int | float | bool): + return True + if isinstance(value, dict): + return all( + isinstance(k, str) and is_json_serializable(v) for k, v in value.items() + ) + if isinstance(value, list): + return all(is_json_serializable(item) for item in value) + return False + + +# ===== Validators ===== + + +class Validators: + """Input validation utilities with security hardening.""" + + __slots__ = () + + @staticmethod + @lru_cache(maxsize=64) + def validate_cert_fingerprint(fingerprint: SecretStr) -> SecretStr: + """Validate and normalize certificate fingerprint. + + :param fingerprint: SHA-256 fingerprint + :return: Normalized fingerprint (lowercase, no separators) + :raises ValueError: If format is invalid + """ + if not fingerprint: + raise ValueError("Certificate fingerprint cannot be empty") + + # Remove common separators + cleaned = fingerprint.get_secret_value().lower() + + # Validate hex format + if not re.match(r"^[a-f0-9]{64}$", cleaned): + raise ValueError( + f"Invalid certificate fingerprint format. " + f"Expected 64 hex characters, got: {len(cleaned)}" + ) + + return SecretStr(cleaned) + + @staticmethod + def validate_port(port: int) -> int: + """Validate port number. + + :param port: Port number + :return: Validated port + :raises ValueError: If port is out of range + """ + if not is_valid_port(port): + raise ValueError( + f"Port must be between {Constants.MIN_PORT} and {Constants.MAX_PORT}" + ) + return port + + @staticmethod + def validate_name(name: str) -> str: + """Validate name field. + + :param name: Name to validate + :return: Validated name + :raises ValueError: If name is invalid + """ + if not name or not name.strip(): + raise ValueError("Name cannot be empty") + + name = name.strip() + if len(name) > Constants.MAX_NAME_LENGTH: + raise ValueError( + f"Name too long: {len(name)} (max {Constants.MAX_NAME_LENGTH})" + ) + + return name + + @staticmethod + def validate_url( + url: str, + *, + allow_private_networks: bool = True, + resolve_dns: bool = False, + ) -> str: + """Validate and sanitize URL. + + :param url: URL to validate + :param allow_private_networks: Allow private/local network addresses + :param resolve_dns: Resolve hostname and block private/reserved IPs + :return: Validated URL + :raises ValueError: If URL is invalid + """ + if not url or not url.strip(): + raise ValueError("URL cannot be empty") + + url = url.strip() + + if len(url) > Constants.MAX_URL_LENGTH: + raise ValueError( + f"URL too long: {len(url)} (max {Constants.MAX_URL_LENGTH})" + ) + + # Check for null bytes + if "\x00" in url: + raise ValueError("URL contains null bytes") + + # Parse URL + try: + parsed = urlparse(url) + if not parsed.scheme or not parsed.netloc: + raise ValueError("Invalid URL format") + except Exception as e: + raise ValueError(f"Invalid URL: {e}") from e + + # SSRF protection for raw IPs in hostname (does not resolve DNS) + if ( + not allow_private_networks + and parsed.hostname + and SSRFProtection.is_blocked_ip(parsed.hostname) + ): + raise ValueError( + f"Access to {parsed.hostname} is blocked (SSRF protection)" + ) + + # Explicitly block localhost when private networks are disallowed + if ( + not allow_private_networks + and parsed.hostname in SSRFProtection.ALLOWED_LOCALHOST + ): + raise ValueError( + f"Access to {parsed.hostname} is blocked (SSRF protection)" + ) + + # Strict SSRF protection with DNS resolution (guards against rebinding) + if ( + resolve_dns + and not allow_private_networks + and parsed.hostname + and not SSRFProtection.is_blocked_ip(parsed.hostname) + and SSRFProtection.is_blocked_hostname(parsed.hostname) + ): + raise ValueError( + f"Access to {parsed.hostname} is blocked (SSRF protection)" + ) + + return url + + @staticmethod + def validate_string_not_empty(value: str, field_name: str) -> str: + """Validate string is not empty. + + :param value: String value + :param field_name: Field name for error messages + :return: Stripped string + :raises ValueError: If string is empty + """ + if not value or not value.strip(): + raise ValueError(f"{field_name} cannot be empty") + return value.strip() + + @staticmethod + def _validate_length(value: str, max_length: int, name: str) -> None: + """Validate string length. + + :param value: String value + :param max_length: Maximum allowed length + :param name: Field name for error messages + :raises ValueError: If string is too long + """ + if len(value) > max_length: + raise ValueError(f"{name} too long: {len(value)} (max {max_length})") + + @staticmethod + def _validate_no_null_bytes(value: str, name: str) -> None: + """Validate string contains no null bytes. + + :param value: String value + :param name: Field name for error messages + :raises ValueError: If string contains null bytes + """ + if "\x00" in value: + raise ValueError(f"{name} contains null bytes") + + @staticmethod + def validate_non_negative(value: DataLimit | int, name: str) -> int: + """Validate integer is non-negative. + + :param value: Integer value + :param name: Field name for error messages + :return: Validated value + :raises ValueError: If value is negative + """ + from .models import DataLimit + + raw_value = value.bytes if isinstance(value, DataLimit) else value + if raw_value < 0: + raise ValueError(f"{name} must be non-negative, got {raw_value}") + return raw_value + + @staticmethod + def validate_since(value: str) -> str: + """Validate experimental metrics 'since' parameter. + + Accepts: + - Relative durations: 24h, 7d, 30m, 15s + - ISO-8601 timestamps (e.g., 2024-01-01T00:00:00Z) + + :param value: Since parameter + :return: Sanitized since value + :raises ValueError: If value is invalid + """ + if not value or not value.strip(): + raise ValueError("'since' parameter cannot be empty") + + sanitized = value.strip() + + # Relative format (number + suffix) + if len(sanitized) >= 2 and sanitized[-1] in {"h", "d", "m", "s"}: + number = sanitized[:-1] + if number.isdigit(): + return sanitized + + # ISO-8601 timestamp (allow trailing Z) + iso_value = sanitized.replace("Z", "+00:00") + try: + datetime.fromisoformat(iso_value) + return sanitized + except ValueError: + raise ValueError( + "'since' must be a relative duration (e.g., '24h', '7d') " + "or ISO-8601 timestamp" + ) from None + + @classmethod + @lru_cache(maxsize=256) + def validate_key_id(cls, key_id: str) -> str: + """Enhanced key_id validation. + + :param key_id: Key ID to validate + :return: Validated key ID + :raises ValueError: If key ID is invalid + """ + clean_id = cls.validate_string_not_empty(key_id, "key_id") + cls._validate_length(clean_id, Constants.MAX_KEY_ID_LENGTH, "key_id") + cls._validate_no_null_bytes(clean_id, "key_id") + + try: + decoded = urllib.parse.unquote(clean_id) + double_decoded = urllib.parse.unquote(decoded) + + # Check all variants for malicious characters + for variant in [clean_id, decoded, double_decoded]: + if any(c in variant for c in {".", "/", "\\", "%", "\x00"}): + raise ValueError( + "key_id contains invalid characters (., /, \\, %, null)" + ) + except Exception as e: + raise ValueError(f"Invalid key_id encoding: {e}") from e + + # Strict whitelist approach + allowed_chars = frozenset( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" + ) + if not all(c in allowed_chars for c in clean_id): + raise ValueError("key_id must be alphanumeric, dashes, underscores only") + + return clean_id + + @staticmethod + @lru_cache(maxsize=256) + def sanitize_url_for_logging(url: str) -> str: + """Remove secret path from URL for safe logging. + + :param url: URL to sanitize + :return: Sanitized URL + """ + try: + parsed = urlparse(url) + return f"{parsed.scheme}://{parsed.netloc}/***" + except Exception: + return "***INVALID_URL***" + + @staticmethod + @lru_cache(maxsize=512) + def sanitize_endpoint_for_logging(endpoint: str) -> str: + """Sanitize endpoint for safe logging. + + :param endpoint: Endpoint to sanitize + :return: Sanitized endpoint + """ + if not endpoint: + return "***EMPTY***" + + parts = endpoint.split("/") + sanitized = [part if len(part) <= 20 else "***" for part in parts] + return "/".join(sanitized) + + +# ===== Base Models ===== + + +class BaseValidatedModel(BaseModel): + """Base model with strict validation.""" + + model_config = ConfigDict( + validate_assignment=True, + validate_default=True, + populate_by_name=True, + use_enum_values=True, + str_strip_whitespace=True, + arbitrary_types_allowed=False, + frozen=False, + ) + + +# ===== Configuration Types ===== + + +class ConfigOverrides(TypedDict, total=False): + """Type-safe configuration overrides. + + All fields are optional, allowing selective parameter overriding + while maintaining type safety. + """ + + timeout: int + retry_attempts: int + max_connections: int + rate_limit: int + user_agent: str + enable_circuit_breaker: bool + circuit_failure_threshold: int + circuit_recovery_timeout: float + circuit_success_threshold: int + circuit_call_timeout: float + enable_logging: bool + json_format: bool + allow_private_networks: bool + resolve_dns_for_ssrf: bool + + +class ClientDependencies(TypedDict, total=False): + """Type-safe client dependencies. + + Optional dependencies that can be injected into the client. + """ + + audit_logger: Any # AuditLogger protocol + metrics: Any # MetricsCollector protocol + + +# ===== Helper Functions ===== + + +def build_config_overrides( + **kwargs: int | str | bool | float | None, +) -> dict[str, int | str | bool | float | None]: + """Build configuration overrides dictionary from kwargs. + + DRY implementation - single source of truth for config building. + + :param kwargs: Configuration parameters + :return: Dictionary containing only non-None values + + Example: + >>> overrides = build_config_overrides(timeout=20, enable_logging=True) + >>> # Returns: {'timeout': 20, 'enable_logging': True} + """ + valid_keys = ConfigOverrides.__annotations__.keys() + return {k: v for k, v in kwargs.items() if k in valid_keys and v is not None} + + +def merge_config_kwargs( + base_kwargs: dict[str, Any], + overrides: ConfigOverrides, +) -> dict[str, Any]: + """Merge base kwargs with configuration overrides. + + :param base_kwargs: Base keyword arguments + :param overrides: Configuration overrides to apply + :return: Merged dictionary + """ + return {**base_kwargs, **overrides} + + +# ===== Masking Utilities ===== + + +def mask_sensitive_data( + data: Mapping[str, Any], + *, + sensitive_keys: frozenset[str] | None = None, + _depth: int = 0, +) -> dict[str, Any]: + """Sensitive data masking with lazy copying and optimized recursion. + + Uses lazy copying - only creates new dict when needed. + Includes recursion depth protection. + + :param data: Data dictionary to mask + :param sensitive_keys: Set of sensitive key names (case-insensitive matching) + :param _depth: Current recursion depth (internal) + :return: Masked data dictionary (may be same object if no sensitive data found) + """ + # Guard against infinite recursion + if _depth > Constants.MAX_RECURSION_DEPTH: + return {"_error": "Max recursion depth exceeded"} + + keys_to_mask = sensitive_keys or DEFAULT_SENSITIVE_KEYS + keys_lower = {k.lower() for k in keys_to_mask} + + masked: dict[str, Any] | None = None + + for key, value in data.items(): + # Check if key is sensitive + if key.lower() in keys_lower: + if masked is None: + masked = dict(data) + masked[key] = "***MASKED***" + continue + + # Recursively handle nested dicts + if isinstance(value, dict): + nested = mask_sensitive_data( + value, sensitive_keys=keys_to_mask, _depth=_depth + 1 + ) + if nested is not value: + if masked is None: + masked = dict(data) + masked[key] = nested + + # Handle lists containing dicts + elif isinstance(value, list): + new_list: list[Any] = [] + list_modified = False + + for item in value: + if isinstance(item, dict): + masked_item = mask_sensitive_data( + item, sensitive_keys=keys_to_mask, _depth=_depth + 1 + ) + if masked_item is not item: + list_modified = True + new_list.append(masked_item) + else: + new_list.append(item) + + if list_modified: + if masked is None: + masked = dict(data) + masked[key] = new_list + + return masked if masked is not None else dict(data) + + +def validate_snapshot_size(data: dict[str, Any]) -> None: + """Validate that data size is within limits. + + :param data: Data dictionary to validate + :raises ValueError: If data exceeds size limit + """ + size_bytes = sys.getsizeof(data) + max_bytes = Constants.MAX_SNAPSHOT_SIZE_MB * 1024 * 1024 + + if size_bytes > max_bytes: + raise ValueError( + f"Data too large: {size_bytes / 1024 / 1024:.2f} MB " + f"(max {Constants.MAX_SNAPSHOT_SIZE_MB} MB)" + ) + + +__all__ = [ + "DEFAULT_SENSITIVE_KEYS", + "AuditDetails", + "BaseValidatedModel", + "Bytes", + "BytesPerUserDict", + "ChecksDict", + "ClientDependencies", + "ConfigOverrides", + "Constants", + "CredentialSanitizer", + "JsonDict", + "JsonList", + "JsonPayload", + "JsonPrimitive", + "JsonValue", + "MetricsTags", + "Port", + "QueryParams", + "ResponseData", + "SSRFProtection", + "SecureIDGenerator", + "Timestamp", + "TimestampMs", + "TimestampSec", + "Validators", + "build_config_overrides", + "is_json_serializable", + "is_valid_bytes", + "is_valid_port", + "mask_sensitive_data", + "merge_config_kwargs", + "validate_snapshot_size", +] diff --git a/pyoutlineapi/config.py b/pyoutlineapi/config.py new file mode 100644 index 0000000..1491fac --- /dev/null +++ b/pyoutlineapi/config.py @@ -0,0 +1,669 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import logging +from functools import cached_property, lru_cache +from pathlib import Path +from typing import TYPE_CHECKING, Final, TypeAlias, cast + +from pydantic import Field, SecretStr, field_validator, model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + +from .circuit_breaker import CircuitConfig +from .common_types import ConfigOverrides, Constants, Validators +from .exceptions import ConfigurationError + +if TYPE_CHECKING: + from typing_extensions import Self + +logger = logging.getLogger(__name__) + +# Type aliases for cleaner signatures +ConfigValue: TypeAlias = int | str | bool | float +ConfigDict: TypeAlias = dict[str, ConfigValue] + +# Constants for validation and defaults (immutable) +_MIN_TIMEOUT: Final[int] = 1 +_MAX_TIMEOUT: Final[int] = 300 +_MIN_RETRY: Final[int] = 0 +_MAX_RETRY: Final[int] = 10 +_MIN_CONNECTIONS: Final[int] = 1 +_MAX_CONNECTIONS: Final[int] = 100 +_MIN_RATE_LIMIT: Final[int] = 1 +_MAX_RATE_LIMIT: Final[int] = 1000 +_SAFETY_MARGIN: Final[float] = 10.0 + +# Valid environment names (frozenset for O(1) lookup) +_VALID_ENVIRONMENTS: Final[frozenset[str]] = frozenset( + {"development", "dev", "production", "prod", "custom"} +) + +# Environment prefix constants +_ENV_PREFIX: Final[str] = "OUTLINE_" +_DEV_ENV_PREFIX: Final[str] = "DEV_OUTLINE_" +_PROD_ENV_PREFIX: Final[str] = "PROD_OUTLINE_" + + +@lru_cache(maxsize=128) +def _log_if_enabled(level: int, message: str) -> None: + """Centralized logging with level check and caching (DRY + Performance). + + Uses LRU cache to avoid repeated log checks for same messages. + Cache size of 128 is optimal for typical config scenarios. + + :param level: Logging level + :param message: Log message + """ + if logger.isEnabledFor(level): + logger.log(level, message) + + +class OutlineClientConfig(BaseSettings): + """Main configuration.""" + + model_config = SettingsConfigDict( + env_prefix=_ENV_PREFIX, + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="forbid", + validate_assignment=True, + validate_default=True, + frozen=False, + ) + + # ===== Core Settings (Required) ===== + + api_url: str = Field(..., description="Outline server API URL with secret path") + cert_sha256: SecretStr = Field(..., description="SHA-256 certificate fingerprint") + + # ===== Client Settings ===== + + timeout: int = Field( + default=10, + ge=_MIN_TIMEOUT, + le=_MAX_TIMEOUT, + description="Request timeout (seconds)", + ) + retry_attempts: int = Field( + default=2, + ge=_MIN_RETRY, + le=_MAX_RETRY, + description="Number of retries", + ) + max_connections: int = Field( + default=10, + ge=_MIN_CONNECTIONS, + le=_MAX_CONNECTIONS, + description="Connection pool size", + ) + rate_limit: int = Field( + default=100, + ge=_MIN_RATE_LIMIT, + le=_MAX_RATE_LIMIT, + description="Max concurrent requests", + ) + user_agent: str = Field( + default=Constants.DEFAULT_USER_AGENT, + min_length=1, + max_length=256, + description="Custom user agent string", + ) + + # ===== Optional Features ===== + + enable_circuit_breaker: bool = Field( + default=True, + description="Enable circuit breaker", + ) + enable_logging: bool = Field( + default=False, + description="Enable debug logging", + ) + json_format: bool = Field( + default=False, + description="Return raw JSON", + ) + allow_private_networks: bool = Field( + default=True, + description="Allow private or local network addresses in api_url", + ) + resolve_dns_for_ssrf: bool = Field( + default=False, + description="Resolve DNS for SSRF checks (strict mode)", + ) + + # ===== Circuit Breaker Settings ===== + + circuit_failure_threshold: int = Field( + default=5, + ge=1, + le=100, + description="Failures before opening", + ) + circuit_recovery_timeout: float = Field( + default=60.0, + ge=1.0, + le=3600.0, + description="Recovery wait time (seconds)", + ) + circuit_success_threshold: int = Field( + default=2, + ge=1, + le=10, + description="Successes needed to close", + ) + circuit_call_timeout: float = Field( + default=10.0, + ge=0.1, + le=300.0, + description="Circuit call timeout (seconds)", + ) + + # ===== Validators ===== + + @field_validator("api_url") + @classmethod + def validate_api_url(cls, v: str) -> str: + """Validate and normalize API URL with optimized regex. + + :param v: URL to validate + :return: Validated URL + :raises ValueError: If URL is invalid + """ + return Validators.validate_url(v) + + @field_validator("cert_sha256") + @classmethod + def validate_cert(cls, v: SecretStr) -> SecretStr: + """Validate certificate fingerprint with constant-time comparison. + + :param v: Certificate fingerprint + :return: Validated fingerprint + :raises ValueError: If fingerprint is invalid + """ + return Validators.validate_cert_fingerprint(v) + + @field_validator("user_agent") + @classmethod + def validate_user_agent(cls, v: str) -> str: + """Validate user agent string with efficient control char check. + + :param v: User agent to validate + :return: Validated user agent + :raises ValueError: If user agent is invalid + """ + v = Validators.validate_string_not_empty(v, "User agent") + + # Efficient control character check using generator + if any(ord(c) < 32 for c in v): + raise ValueError("User agent contains invalid control characters") + + return v + + @model_validator(mode="after") + def validate_config(self) -> Self: + """Additional validation after model creation with pattern matching. + + :return: Validated configuration instance + """ + # Security warning for HTTP using pattern matching + match (self.api_url, "localhost" in self.api_url): + case (url, False) if "http://" in url: + _log_if_enabled( + logging.WARNING, + "Using HTTP for non-localhost connection. " + "This is insecure and should only be used for testing.", + ) + + # Optional SSRF protection for private networks (no DNS resolution) + Validators.validate_url( + self.api_url, + allow_private_networks=self.allow_private_networks, + resolve_dns=False, + ) + + # Circuit breaker timeout adjustment with caching + if self.enable_circuit_breaker: + max_request_time = self._get_max_request_time() + + if self.circuit_call_timeout < max_request_time: + _log_if_enabled( + logging.WARNING, + f"Circuit timeout ({self.circuit_call_timeout}s) is less than " + f"max request time ({max_request_time}s). " + f"Auto-adjusting to {max_request_time}s.", + ) + object.__setattr__(self, "circuit_call_timeout", max_request_time) + + return self + + def _get_max_request_time(self) -> float: + """Calculate worst-case request time with instance caching. + + :return: Maximum request time in seconds + """ + if not hasattr(self, "_cached_max_request_time"): + self._cached_max_request_time = ( + self.timeout * (self.retry_attempts + 1) + _SAFETY_MARGIN + ) + return self._cached_max_request_time + + # ===== Custom __setattr__ for SecretStr Protection ===== + + def __setattr__(self, name: str, value: object) -> None: + """Prevent accidental string assignment to SecretStr fields. + + :param name: Attribute name + :param value: Attribute value + :raises TypeError: If trying to assign str to SecretStr field + """ + # Fast path: skip check for non-cert fields + if name != "cert_sha256": + super().__setattr__(name, value) + return + + if isinstance(value, str): + raise TypeError( + "cert_sha256 must be SecretStr, not str. Use: SecretStr('your_cert')" + ) + + super().__setattr__(name, value) + + # ===== Helper Methods ===== + + @cached_property + def get_sanitized_config(self) -> ConfigDict: + """Get configuration with sensitive data masked (cached). + + Safe for logging, debugging, and display. + + Performance: ~20x speedup with caching for repeated calls + Memory: Single cached result per instance + + :return: Sanitized configuration dictionary + """ + return { + "api_url": Validators.sanitize_url_for_logging(self.api_url), + "cert_sha256": "***MASKED***", + "timeout": self.timeout, + "retry_attempts": self.retry_attempts, + "max_connections": self.max_connections, + "rate_limit": self.rate_limit, + "user_agent": self.user_agent, + "enable_circuit_breaker": self.enable_circuit_breaker, + "enable_logging": self.enable_logging, + "json_format": self.json_format, + "allow_private_networks": self.allow_private_networks, + "circuit_failure_threshold": self.circuit_failure_threshold, + "circuit_recovery_timeout": self.circuit_recovery_timeout, + "circuit_success_threshold": self.circuit_success_threshold, + "circuit_call_timeout": self.circuit_call_timeout, + } + + def model_copy_immutable(self, **overrides: ConfigValue) -> OutlineClientConfig: + """Create immutable copy with overrides (optimized validation). + + :param overrides: Configuration parameters to override + :return: Deep copy of configuration with applied updates + :raises ValueError: If invalid override keys provided + + Example: + >>> new_config = config.model_copy_immutable(timeout=20) + """ + # Optimized: Use frozenset intersection for O(1) validation + valid_keys = frozenset(ConfigOverrides.__annotations__.keys()) + provided_keys = frozenset(overrides.keys()) + invalid = provided_keys - valid_keys + + if invalid: + raise ValueError( + f"Invalid configuration keys: {', '.join(sorted(invalid))}. " + f"Valid keys: {', '.join(sorted(valid_keys))}" + ) + + # Pydantic's model_copy is already optimized + return cast( # type: ignore[redundant-cast, unused-ignore] + OutlineClientConfig, self.model_copy(deep=True, update=overrides) + ) + + @property + def circuit_config(self) -> CircuitConfig | None: + """Get circuit breaker configuration if enabled. + + Returns None if circuit breaker is disabled, otherwise CircuitConfig instance. + Cached as property for performance. + + :return: Circuit config or None if disabled + """ + if not self.enable_circuit_breaker: + return None + + return CircuitConfig( + failure_threshold=self.circuit_failure_threshold, + recovery_timeout=self.circuit_recovery_timeout, + success_threshold=self.circuit_success_threshold, + call_timeout=self.circuit_call_timeout, + ) + + # ===== Factory Methods ===== + + @classmethod + def from_env( + cls, + env_file: str | Path | None = None, + **overrides: ConfigValue, + ) -> OutlineClientConfig: + """Load configuration from environment with overrides. + + :param env_file: Path to .env file + :param overrides: Configuration parameters to override + :return: Configuration instance + :raises ConfigurationError: If environment configuration is invalid + + Example: + >>> config = OutlineClientConfig.from_env( + ... env_file=".env.prod", + ... timeout=20, + ... enable_logging=True + ... ) + """ + # Fast path: validate overrides early + valid_keys = frozenset(ConfigOverrides.__annotations__.keys()) + filtered_overrides = cast( + ConfigOverrides, + {k: v for k, v in overrides.items() if k in valid_keys}, + ) + + if not env_file: + return cls( # type: ignore[call-arg, unused-ignore] + **filtered_overrides + ) + + match env_file: + case str(): + env_path = Path(env_file) + case Path(): + env_path = env_file + case _: + raise TypeError( + f"env_file must be str or Path, got {type(env_file).__name__}" + ) + + if not env_path.exists(): + raise ConfigurationError( + f"Environment file not found: {env_path}", + field="env_file", + ) + + return cls( # type: ignore[call-arg, unused-ignore] + _env_file=str(env_path), + **filtered_overrides, + ) + + @classmethod + def create_minimal( + cls, + api_url: str, + cert_sha256: str | SecretStr, + **overrides: ConfigValue, + ) -> OutlineClientConfig: + """Create minimal configuration (optimized validation). + + :param api_url: API URL + :param cert_sha256: Certificate fingerprint + :param overrides: Optional configuration parameters + :return: Configuration instance + :raises TypeError: If cert_sha256 is not str or SecretStr + + Example: + >>> config = OutlineClientConfig.create_minimal( + ... api_url="https://server.com/path", + ... cert_sha256="a" * 64, + ... timeout=20 + ... ) + """ + match cert_sha256: + case str(): + cert = SecretStr(cert_sha256) + case SecretStr(): + cert = cert_sha256 + case _: + raise TypeError( + f"cert_sha256 must be str or SecretStr, " + f"got {type(cert_sha256).__name__}" + ) + + valid_keys = frozenset(ConfigOverrides.__annotations__.keys()) + filtered_overrides = cast( + ConfigOverrides, + {k: v for k, v in overrides.items() if k in valid_keys}, + ) + + return cls( + api_url=api_url, + cert_sha256=cert, + **filtered_overrides, + ) + + +class DevelopmentConfig(OutlineClientConfig): + """Development configuration with relaxed security. + + Optimized for local development and testing with: + - Extended timeouts for debugging + - Detailed logging enabled by default + - Circuit breaker disabled for easier testing + """ + + model_config = SettingsConfigDict( + env_prefix=_DEV_ENV_PREFIX, + env_file=".env.dev", + case_sensitive=False, + extra="forbid", + ) + + enable_logging: bool = True + enable_circuit_breaker: bool = False + timeout: int = 30 + + +class ProductionConfig(OutlineClientConfig): + """Production configuration with strict security. + + Enforces HTTPS and enables all safety features: + - Circuit breaker enabled + - Logging disabled (performance) + - HTTPS enforcement + - Strict validation + """ + + model_config = SettingsConfigDict( + env_prefix=_PROD_ENV_PREFIX, + env_file=".env.prod", + case_sensitive=False, + extra="forbid", + ) + + enable_circuit_breaker: bool = True + enable_logging: bool = False + allow_private_networks: bool = False + resolve_dns_for_ssrf: bool = True + + @model_validator(mode="after") + def enforce_security(self) -> Self: + """Enforce production security with optimized checks. + + :return: Validated configuration + :raises ConfigurationError: If HTTP is used in production + """ + match self.api_url: + case url if "http://" in url: + raise ConfigurationError( + "Production environment must use HTTPS", + field="api_url", + security_issue=True, + ) + + if not self.enable_circuit_breaker: + _log_if_enabled( + logging.WARNING, + "Circuit breaker disabled in production. Not recommended.", + ) + + return self + + +# ===== Utility Functions ===== + + +@lru_cache(maxsize=1) +def _get_env_template() -> str: + """Get environment template (cached for performance). + + :return: Template string + """ + return """# PyOutlineAPI Configuration Template +# Generated by create_env_template() + +# ===== Required Settings ===== +OUTLINE_API_URL=https://your-server.com:12345/your-secret-path +OUTLINE_CERT_SHA256=your-64-character-sha256-fingerprint + +# ===== Client Settings ===== +# Timeout for API requests (1-300 seconds) +# OUTLINE_TIMEOUT=10 + +# Number of retry attempts (0-10) +# OUTLINE_RETRY_ATTEMPTS=2 + +# Connection pool size (1-100) +# OUTLINE_MAX_CONNECTIONS=10 + +# Maximum concurrent requests (1-1000) +# OUTLINE_RATE_LIMIT=100 + +# Custom user agent string +# OUTLINE_USER_AGENT=PyOutlineAPI/0.4.0 + +# ===== Feature Flags ===== +# Enable circuit breaker protection +# OUTLINE_ENABLE_CIRCUIT_BREAKER=true + +# Enable debug logging +# OUTLINE_ENABLE_LOGGING=false + +# Return raw JSON instead of models +# OUTLINE_JSON_FORMAT=false + +# Allow private/local network addresses in api_url +# OUTLINE_ALLOW_PRIVATE_NETWORKS=true + +# Resolve DNS for SSRF checks (strict mode) +# OUTLINE_RESOLVE_DNS_FOR_SSRF=false + +# ===== Circuit Breaker Settings ===== +# Failures before opening circuit (1-100) +# OUTLINE_CIRCUIT_FAILURE_THRESHOLD=5 + +# Recovery timeout in seconds (1.0-3600.0) +# OUTLINE_CIRCUIT_RECOVERY_TIMEOUT=60.0 + +# Successes needed to close circuit (1-10) +# OUTLINE_CIRCUIT_SUCCESS_THRESHOLD=2 + +# Call timeout in seconds (0.1-300.0) +# OUTLINE_CIRCUIT_CALL_TIMEOUT=10.0 +""" + + +def create_env_template(path: str | Path = ".env.example") -> None: + """Create .env template file (optimized I/O). + + Performance: Uses cached template and efficient Path operations + + :param path: Path to template file + """ + # Pattern matching for path handling + match path: + case str(): + target_path = Path(path) + case Path(): + target_path = path + case _: + raise TypeError(f"path must be str or Path, got {type(path).__name__}") + + # Use cached template + template = _get_env_template() + target_path.write_text(template, encoding="utf-8") + + _log_if_enabled( + logging.INFO, + f"Created configuration template: {target_path}", + ) + + +def load_config( + environment: str = "custom", + **overrides: ConfigValue, +) -> OutlineClientConfig: + """Load configuration for environment (optimized lookup). + + :param environment: Environment name (development, production, custom) + :param overrides: Configuration parameters to override + :return: Configuration instance + :raises ValueError: If environment name is invalid + + Example: + >>> config = load_config("production", timeout=20) + """ + env_lower = environment.lower() + + # Fast validation with frozenset + if env_lower not in _VALID_ENVIRONMENTS: + valid_envs = ", ".join(sorted(_VALID_ENVIRONMENTS)) + raise ValueError(f"Invalid environment '{environment}'. Valid: {valid_envs}") + + # Pattern matching for config selection (Python 3.10+) + config_class: type[OutlineClientConfig] + match env_lower: + case "development" | "dev": + config_class = DevelopmentConfig + case "production" | "prod": + config_class = ProductionConfig + case "custom": + config_class = OutlineClientConfig + case _: # Should never reach due to validation above + config_class = OutlineClientConfig + + # Optimized override filtering + valid_keys = frozenset(ConfigOverrides.__annotations__.keys()) + filtered_overrides = cast( + ConfigOverrides, + {k: v for k, v in overrides.items() if k in valid_keys}, + ) + + return config_class( # type: ignore[call-arg, unused-ignore] + **filtered_overrides + ) + + +__all__ = [ + "DevelopmentConfig", + "OutlineClientConfig", + "ProductionConfig", + "create_env_template", + "load_config", +] diff --git a/pyoutlineapi/exceptions.py b/pyoutlineapi/exceptions.py index 87d3568..d4003a5 100644 --- a/pyoutlineapi/exceptions.py +++ b/pyoutlineapi/exceptions.py @@ -1,39 +1,644 @@ -""" -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. +"""Exception hierarchy for PyOutlineAPI. + +Provides structured exceptions with rich error context, retry guidance, +and credential sanitization for secure error handling. + +Classes: + OutlineError: Base exception with context and retry support + APIError: HTTP API failures with status codes + CircuitOpenError: Circuit breaker open state + ConfigurationError: Invalid configuration + ValidationError: Data validation failures + OutlineConnectionError: Network connection issues + OutlineTimeoutError: Operation timeouts -Copyright (c) 2025 Denis Rozhnovskiy -All rights reserved. +Functions: + get_retry_delay: Get suggested retry delay for an error + is_retryable: Check if error should be retried + get_safe_error_dict: Extract safe error info for logging + format_error_chain: Format exception chain for structured logging -This software is licensed under the MIT License. -You can find the full license text at: - https://opensource.org/licenses/MIT +License: + MIT License - Copyright (c) 2025 Denis Rozhnovskiy -Source code repository: +Repository: https://github.com/orenlab/pyoutlineapi """ -from typing import Optional +from __future__ import annotations + +from types import MappingProxyType +from typing import Any, ClassVar, Final + +from .common_types import Constants, CredentialSanitizer + +# Maximum length for error messages to prevent DoS +_MAX_MESSAGE_LENGTH: Final[int] = 1024 + +_EMPTY_DICT: Final[MappingProxyType[str, Any]] = MappingProxyType({}) class OutlineError(Exception): - """Base exception for Outline client errors.""" + """Base exception for all PyOutlineAPI errors. + + Provides rich error context, retry guidance, and safe serialization + with automatic credential sanitization. + + Attributes: + is_retryable: Whether this error type should be retried + default_retry_delay: Suggested delay before retry in seconds + + Example: + >>> try: + ... raise OutlineError("Connection failed", details={"host": "server"}) + ... except OutlineError as e: + ... print(e.safe_details) # {'host': 'server'} + """ + + __slots__ = ("_cached_str", "_details", "_message", "_safe_details") + + _is_retryable: ClassVar[bool] = False + _default_retry_delay: ClassVar[float] = 1.0 + + def __init__( + self, + message: object, + *, + details: dict[str, Any] | None = None, + safe_details: dict[str, Any] | None = None, + ) -> None: + """Initialize exception with automatic credential sanitization. + + Args: + message: Error message (automatically sanitized) + details: Internal details (may contain sensitive data) + safe_details: Safe details for logging/display + + Raises: + ValueError: If message exceeds maximum length after sanitization + """ + # Validate and sanitize message + if not isinstance(message, str): + message = str(message) + + # Sanitize credentials from message + sanitized_message = CredentialSanitizer.sanitize(message) + + # Truncate if too long + if len(sanitized_message) > _MAX_MESSAGE_LENGTH: + sanitized_message = sanitized_message[:_MAX_MESSAGE_LENGTH] + "..." + + self._message = sanitized_message + super().__init__(sanitized_message) + + self._details: dict[str, Any] | MappingProxyType[str, Any] = ( + dict(details) if details else _EMPTY_DICT + ) + self._safe_details: dict[str, Any] | MappingProxyType[str, Any] = ( + dict(safe_details) if safe_details else _EMPTY_DICT + ) + + self._cached_str: str | None = None + + @property + def details(self) -> dict[str, Any]: + """Get internal error details (may contain sensitive data). + + Warning: + Use with caution - may contain credentials or sensitive information. + For logging, use ``safe_details`` instead. + + Returns: + Copy of internal details dictionary + """ + if self._details is _EMPTY_DICT: + return {} + return self._details.copy() + + @property + def safe_details(self) -> dict[str, Any]: + """Get sanitized error details safe for logging. + + Returns: + Copy of safe details dictionary + """ + if self._safe_details is _EMPTY_DICT: + return {} + return self._safe_details.copy() + + def _format_details(self) -> str: + """Format safe details for string representation. + + :return: Formatted details string + """ + if not self._safe_details: + return "" + + parts = [f"{k}={v}" for k, v in self._safe_details.items()] + return f" ({', '.join(parts)})" + + def __str__(self) -> str: + """Safe string representation using safe_details. + + Cached for performance on repeated access. + + :return: String representation + """ + if self._cached_str is None: + self._cached_str = f"{self._message}{self._format_details()}" + return self._cached_str + + def __repr__(self) -> str: + """Safe repr without sensitive data. + + :return: String representation + """ + class_name = self.__class__.__name__ + return f"{class_name}({self._message!r})" + + @property + def is_retryable(self) -> bool: + """Return whether this error type should be retried.""" + return self._is_retryable + + @property + def default_retry_delay(self) -> float: + """Return suggested delay before retry in seconds.""" + return self._default_retry_delay class APIError(OutlineError): - """Raised when API requests fail.""" + """HTTP API request failure. + + Automatically determines retry eligibility based on HTTP status code. + + Attributes: + status_code: HTTP status code (if available) + endpoint: API endpoint that failed + response_data: Raw response data (may contain sensitive info) + + Example: + >>> error = APIError("Not found", status_code=404, endpoint="/server") + >>> error.is_client_error # True + >>> error.is_retryable # False + """ + + __slots__ = ("endpoint", "response_data", "status_code") def __init__( self, message: str, - status_code: Optional[int] = None, - attempt: Optional[int] = None, + *, + status_code: int | None = None, + endpoint: str | None = None, + response_data: dict[str, Any] | None = None, ) -> None: - super().__init__(message) + """Initialize API error with sanitized endpoint. + + Args: + message: Error message + status_code: HTTP status code + endpoint: API endpoint (will be sanitized) + response_data: Response data (may contain sensitive info) + """ + from .common_types import Validators + + # Sanitize endpoint for safe logging + safe_endpoint = ( + Validators.sanitize_endpoint_for_logging(endpoint) if endpoint else None + ) + + # Build safe details (optimization: avoid dict creation if all None) + safe_details: dict[str, Any] | None = None + if status_code is not None or safe_endpoint is not None: + safe_details = {} + if status_code is not None: + safe_details["status_code"] = status_code + if safe_endpoint is not None: + safe_details["endpoint"] = safe_endpoint + + # Build internal details (optimization: avoid dict creation if all None) + details: dict[str, Any] | None = None + if status_code is not None or endpoint is not None: + details = {} + if status_code is not None: + details["status_code"] = status_code + if endpoint is not None: + details["endpoint"] = endpoint + + super().__init__(message, details=details, safe_details=safe_details) + + # Store attributes directly (faster access than dict lookups) self.status_code = status_code - self.attempt = attempt + self.endpoint = endpoint + self.response_data = response_data - def __str__(self) -> str: - msg = super().__str__() - if self.attempt is not None: - msg = f"[Attempt {self.attempt}] {msg}" - return msg + @property + def is_retryable(self) -> bool: + """Check if error is retryable based on status code.""" + return ( + self.status_code in Constants.RETRY_STATUS_CODES + if self.status_code + else False + ) + + @property + def is_client_error(self) -> bool: + """Check if error is a client error (4xx status). + + Returns: + True if status code is 400-499 + """ + return self.status_code is not None and 400 <= self.status_code < 500 + + @property + def is_server_error(self) -> bool: + """Check if error is a server error (5xx status). + + Returns: + True if status code is 500-599 + """ + return self.status_code is not None and 500 <= self.status_code < 600 + + @property + def is_rate_limit_error(self) -> bool: + """Check if error is a rate limit error (429 status). + + Returns: + True if status code is 429 + """ + return self.status_code == 429 + + +class CircuitOpenError(OutlineError): + """Circuit breaker is open due to repeated failures. + + Indicates temporary service unavailability. Clients should wait + for ``retry_after`` seconds before retrying. + + Attributes: + retry_after: Seconds to wait before retry + + Example: + >>> error = CircuitOpenError("Circuit open", retry_after=60.0) + >>> error.is_retryable # True + >>> error.retry_after # 60.0 + """ + + __slots__ = ("retry_after",) + + _is_retryable: ClassVar[bool] = True + + def __init__(self, message: str, *, retry_after: float = 60.0) -> None: + """Initialize circuit open error. + + Args: + message: Error message + retry_after: Seconds to wait before retry + + Raises: + ValueError: If retry_after is negative + """ + if retry_after < 0: + raise ValueError("retry_after must be non-negative") + + # Pre-round for safe_details (avoid repeated rounding) + rounded_retry = round(retry_after, 2) + safe_details = {"retry_after": rounded_retry} + super().__init__(message, safe_details=safe_details) + + self.retry_after = retry_after + + @property + def default_retry_delay(self) -> float: + """Suggested delay before retry.""" + return self.retry_after + + +class ConfigurationError(OutlineError): + """Invalid or missing configuration. + + Attributes: + field: Configuration field name that failed + security_issue: Whether this is a security-related issue + + Example: + >>> error = ConfigurationError( + ... "Missing API URL", field="api_url", security_issue=True + ... ) + """ + + __slots__ = ("field", "security_issue") + + def __init__( + self, + message: str, + *, + field: str | None = None, + security_issue: bool = False, + ) -> None: + """Initialize configuration error. + + Args: + message: Error message + field: Configuration field name + security_issue: Whether this is a security issue + """ + safe_details: dict[str, Any] | None = None + if field or security_issue: + safe_details = {} + if field: + safe_details["field"] = field + if security_issue: + safe_details["security_issue"] = True + + super().__init__(message, safe_details=safe_details) + + self.field = field + self.security_issue = security_issue + + +class ValidationError(OutlineError): + """Data validation failure. + + Raised when data fails validation against expected schema. + + Attributes: + field: Field name that failed validation + model: Model name + + Example: + >>> error = ValidationError( + ... "Invalid port number", field="port", model="ServerConfig" + ... ) + """ + + __slots__ = ("field", "model") + + def __init__( + self, + message: str, + *, + field: str | None = None, + model: str | None = None, + ) -> None: + """Initialize validation error. + + Args: + message: Error message + field: Field name that failed validation + model: Model name + """ + safe_details: dict[str, Any] | None = None + if field or model: + safe_details = {} + if field: + safe_details["field"] = field + if model: + safe_details["model"] = model + + super().__init__(message, safe_details=safe_details) + + self.field = field + self.model = model + + +class OutlineConnectionError(OutlineError): + """Network connection failure. + + Attributes: + host: Host that failed + port: Port that failed + + Example: + >>> error = OutlineConnectionError( + ... "Connection refused", host="server.com", port=443 + ... ) + >>> error.is_retryable # True + """ + + __slots__ = ("host", "port") + + _is_retryable: ClassVar[bool] = True + _default_retry_delay: ClassVar[float] = 2.0 + + def __init__( + self, + message: str, + *, + host: str | None = None, + port: int | None = None, + ) -> None: + """Initialize connection error. + + Args: + message: Error message + host: Host that failed + port: Port that failed + """ + safe_details: dict[str, Any] | None = None + if host or port is not None: + safe_details = {} + if host: + safe_details["host"] = host + if port is not None: + safe_details["port"] = port + + super().__init__(message, safe_details=safe_details) + + self.host = host + self.port = port + + +class OutlineTimeoutError(OutlineError): + """Operation timeout. + + Attributes: + timeout: Timeout value in seconds + operation: Operation that timed out + + Example: + >>> error = OutlineTimeoutError( + ... "Request timeout", timeout=30.0, operation="get_server_info" + ... ) + >>> error.is_retryable # True + """ + + __slots__ = ("operation", "timeout") + + _is_retryable: ClassVar[bool] = True + _default_retry_delay: ClassVar[float] = 2.0 + + def __init__( + self, + message: str, + *, + timeout: float | None = None, + operation: str | None = None, + ) -> None: + """Initialize timeout error. + + Args: + message: Error message + timeout: Timeout value in seconds + operation: Operation that timed out + """ + safe_details: dict[str, Any] | None = None + if timeout is not None or operation: + safe_details = {} + if timeout is not None: + safe_details["timeout"] = round(timeout, 2) + if operation: + safe_details["operation"] = operation + + super().__init__(message, safe_details=safe_details) + + self.timeout = timeout + self.operation = operation + + +# ===== Utility Functions ===== + + +def get_retry_delay(error: Exception) -> float | None: + """Get suggested retry delay for an error. + + Args: + error: Exception to check + + Returns: + Retry delay in seconds, or None if not retryable + + Example: + >>> error = OutlineTimeoutError("Timeout") + >>> get_retry_delay(error) # 2.0 + """ + if not isinstance(error, OutlineError): + return None + if not error.is_retryable: + return None + return error.default_retry_delay + + +def is_retryable(error: Exception) -> bool: + """Check if error should be retried. + + Args: + error: Exception to check + + Returns: + True if error is retryable + + Example: + >>> error = APIError("Server error", status_code=503) + >>> is_retryable(error) # True + """ + if isinstance(error, OutlineError): + return error.is_retryable + return False + + +def get_safe_error_dict(error: BaseException) -> dict[str, Any]: + """Extract safe error information for logging. + + Returns only safe information without sensitive data. + + Args: + error: Exception to convert + + Returns: + Safe error dictionary suitable for logging + + Example: + >>> error = APIError("Not found", status_code=404) + >>> get_safe_error_dict(error) + {'type': 'APIError', 'message': 'Not found', 'status_code': 404, ...} + """ + result: dict[str, Any] = { + "type": type(error).__name__, + "message": str(error), + } + + if not isinstance(error, OutlineError): + return result + + result.update( + { + "retryable": error.is_retryable, + "retry_delay": error.default_retry_delay, + "safe_details": error.safe_details, + } + ) + + match error: + case APIError(): + result["status_code"] = error.status_code + # Only compute these if status_code is not None + if error.status_code is not None: + result["is_client_error"] = error.is_client_error + result["is_server_error"] = error.is_server_error + case CircuitOpenError(): + result["retry_after"] = error.retry_after + case ConfigurationError(): + if error.field is not None: + result["field"] = error.field + result["security_issue"] = error.security_issue + case ValidationError(): + if error.field is not None: + result["field"] = error.field + if error.model is not None: + result["model"] = error.model + case OutlineConnectionError(): + if error.host is not None: + result["host"] = error.host + if error.port is not None: + result["port"] = error.port + case OutlineTimeoutError(): + if error.timeout is not None: + result["timeout"] = error.timeout + if error.operation is not None: + result["operation"] = error.operation + + return result + + +def format_error_chain(error: Exception) -> list[dict[str, Any]]: + """Format exception chain for structured logging. + + Args: + error: Exception to format + + Returns: + List of error dictionaries ordered from root to leaf + + Example: + >>> try: + ... raise ValueError("Inner") from KeyError("Outer") + ... except Exception as e: + ... chain = format_error_chain(e) + ... len(chain) # 2 + """ + # Pre-allocate with reasonable size hint (most chains are 1-3 errors) + chain: list[dict[str, Any]] = [] + current: BaseException | None = error + + while current is not None: + chain.append(get_safe_error_dict(current)) + current = current.__cause__ or current.__context__ + + return chain + + +__all__ = [ + "APIError", + "CircuitOpenError", + "ConfigurationError", + "OutlineConnectionError", + "OutlineError", + "OutlineTimeoutError", + "ValidationError", + "format_error_chain", + "get_retry_delay", + "get_safe_error_dict", + "is_retryable", +] diff --git a/pyoutlineapi/health_monitoring.py b/pyoutlineapi/health_monitoring.py new file mode 100644 index 0000000..6928b6f --- /dev/null +++ b/pyoutlineapi/health_monitoring.py @@ -0,0 +1,620 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Final + +if TYPE_CHECKING: + from collections.abc import Callable, Coroutine + + from .client import AsyncOutlineClient + +logger = logging.getLogger(__name__) + +_MIN_CACHE_TTL: Final[float] = 1.0 +_MAX_CACHE_TTL: Final[float] = 300.0 +_ALPHA: Final[float] = 0.1 # EMA smoothing factor (10% weight to new values) + +# Response time thresholds for status determination +_THRESHOLD_HEALTHY: Final[float] = 1.0 +_THRESHOLD_WARNING: Final[float] = 3.0 + +# Success rate thresholds +_SUCCESS_RATE_EXCELLENT: Final[float] = 0.95 +_SUCCESS_RATE_GOOD: Final[float] = 0.9 +_SUCCESS_RATE_ACCEPTABLE: Final[float] = 0.7 +_SUCCESS_RATE_DEGRADED: Final[float] = 0.5 + + +def _log_if_enabled(level: int, message: str) -> None: + """Centralized logging with log-level guard. + + :param level: Logging level + :param message: Log message + """ + if logger.isEnabledFor(level): + logger.log(level, message) + + +@dataclass(slots=True, frozen=True) +class HealthStatus: + """Immutable health check result with optimized properties.""" + + healthy: bool + timestamp: float + checks: dict[str, dict[str, Any]] = field(default_factory=dict) + metrics: dict[str, float] = field(default_factory=dict) + + @property + def failed_checks(self) -> list[str]: + """Get failed checks. + + :return: List of failed check names + """ + return [ + name + for name, result in self.checks.items() + if result.get("status") == "unhealthy" + ] + + @property + def is_degraded(self) -> bool: + """Check if service is degraded. + + :return: True if any check is degraded + """ + return any( + result.get("status") == "degraded" for result in self.checks.values() + ) + + @property + def warning_checks(self) -> list[str]: + """Get warning checks. + + :return: List of warning check names + """ + return [ + name + for name, result in self.checks.items() + if result.get("status") == "warning" + ] + + @property + def total_checks(self) -> int: + """Get total check count. + + :return: Total check count + """ + return len(self.checks) + + @property + def passed_checks(self) -> int: + """Get passed check count. + + :return: Passed check count + """ + return sum( + 1 for result in self.checks.values() if result.get("status") == "healthy" + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary with cached properties. + + :return: Dictionary representation + """ + return { + "healthy": self.healthy, + "degraded": self.is_degraded, # Cached + "timestamp": self.timestamp, + "checks": self.checks, + "metrics": self.metrics, + "failed_checks": self.failed_checks, # Cached + "warning_checks": self.warning_checks, # Cached + "total_checks": self.total_checks, # Cached + "passed_checks": self.passed_checks, # Cached + } + + +@dataclass(slots=True) +class PerformanceMetrics: + """Performance tracking with optimized EMA and properties.""" + + total_requests: int = 0 + successful_requests: int = 0 + failed_requests: int = 0 + avg_response_time: float = 0.0 + start_time: float = field(default_factory=time.monotonic) + + @property + def success_rate(self) -> float: + """Calculate success rate with fast path. + + :return: Success rate (0.0 to 1.0) + """ + if self.total_requests == 0: + return 1.0 # Fast path + return self.successful_requests / self.total_requests + + @property + def failure_rate(self) -> float: + """Calculate failure rate (uses success_rate). + + :return: Failure rate (0.0 to 1.0) + """ + return 1.0 - self.success_rate + + @property + def uptime(self) -> float: + """Get uptime in seconds. + + :return: Uptime in seconds + """ + return time.monotonic() - self.start_time + + +class HealthCheckHelper: + """Helper for status determination.""" + + __slots__ = () # No instance attributes + + @staticmethod + def determine_status_by_time(duration: float) -> str: + """Determine status using pattern matching (Python 3.10+). + + :param duration: Response time in seconds + :return: Status string + """ + match duration: + case d if d < _THRESHOLD_HEALTHY: + return "healthy" + case d if d < _THRESHOLD_WARNING: + return "warning" + case _: + return "degraded" + + @staticmethod + def determine_circuit_status(cb_state: str, success_rate: float) -> str: + """Determine circuit status. + + :param cb_state: Circuit breaker state + :param success_rate: Success rate (0.0 to 1.0) + :return: Status string + """ + match cb_state: + case "OPEN": + return "unhealthy" + case "HALF_OPEN": + return "warning" + case _: + # Closed state - check success rate + match success_rate: + case r if r >= _SUCCESS_RATE_GOOD: + return "healthy" + case r if r >= _SUCCESS_RATE_DEGRADED: + return "warning" + case _: + return "degraded" + + @staticmethod + def determine_performance_status(success_rate: float, avg_time: float) -> str: + """Determine performance status. + + :param success_rate: Success rate (0.0 to 1.0) + :param avg_time: Average response time + :return: Status string + """ + match (success_rate, avg_time): + case (r, t) if r > _SUCCESS_RATE_EXCELLENT and t < 1.0: + return "healthy" + case (r, t) if r > _SUCCESS_RATE_GOOD and t < 2.0: + return "warning" + case (r, _) if r > _SUCCESS_RATE_ACCEPTABLE: + return "degraded" + case _: + return "unhealthy" + + +class HealthMonitor: + """Health monitoring with caching and checks.""" + + __slots__ = ( + "_cache_ttl", + "_cached_result", + "_client", + "_custom_checks", + "_helper", + "_last_check_time", + "_metrics", + ) + + def __init__( + self, + client: AsyncOutlineClient, + *, + cache_ttl: float = 30.0, + ) -> None: + """Initialize health monitor with validation. + + :param client: AsyncOutlineClient instance + :param cache_ttl: Cache TTL in seconds (1.0-300.0) + :raises ValueError: If cache_ttl is invalid + """ + # Validate cache_ttl using pattern matching + match cache_ttl: + case ttl if _MIN_CACHE_TTL <= ttl <= _MAX_CACHE_TTL: + self._cache_ttl = ttl + case _: + raise ValueError( + f"cache_ttl must be between {_MIN_CACHE_TTL} and {_MAX_CACHE_TTL}" + ) + + self._client = client + self._metrics = PerformanceMetrics() + self._custom_checks: dict[ + str, Callable[[AsyncOutlineClient], Coroutine[Any, Any, dict[str, Any]]] + ] = {} + self._helper = HealthCheckHelper() + self._cached_result: HealthStatus | None = None + self._last_check_time: float = 0.0 + + async def check(self, *, use_cache: bool = True) -> HealthStatus: + """Perform comprehensive health check with caching. + + :param use_cache: Whether to use cached result + :return: Health status + """ + # Fast path: return cached result if valid + if use_cache and self.cache_valid: + cached = self._cached_result + if cached is not None: + return cached + + # Perform full health check + current_time = time.monotonic() + status_data: dict[str, Any] = { + "healthy": True, + "timestamp": current_time, + "checks": {}, + "metrics": {}, + } + + # Run all checks concurrently for speed + await asyncio.gather( + self._check_connectivity(status_data), + self._check_circuit_breaker(status_data), + self._check_performance(status_data), + self._run_custom_checks(status_data), + return_exceptions=True, # Don't fail if one check fails + ) + + # Create immutable result + result = HealthStatus( + healthy=status_data["healthy"], + timestamp=status_data["timestamp"], + checks=status_data["checks"], + metrics=status_data["metrics"], + ) + + # Update cache + self._cached_result = result + self._last_check_time = current_time + + return result + + async def quick_check(self) -> bool: + """Quick health check (connectivity only) with caching. + + :return: True if healthy + """ + # Fast path: use cached result if available + if self.cache_valid: + cached = self._cached_result + if cached is not None: + return cached.healthy + + try: + start = time.monotonic() + await self._client.get_server_info() + duration = time.monotonic() - start + + # Determine status using helper + status = self._helper.determine_status_by_time(duration) + return status == "healthy" + + except Exception: + return False + + async def _check_connectivity(self, status_data: dict[str, Any]) -> None: + """Check basic connectivity with timing. + + :param status_data: Status data to update + """ + try: + start = time.monotonic() + await self._client.get_server_info() + duration = time.monotonic() - start + + # Determine status using helper (pattern matching) + check_status = self._helper.determine_status_by_time(duration) + + status_data["checks"]["connectivity"] = { + "status": check_status, + "message": f"API accessible ({duration:.2f}s)", + "response_time": duration, + } + status_data["metrics"]["connectivity_time"] = duration + + except Exception as e: + status_data["healthy"] = False + status_data["checks"]["connectivity"] = { + "status": "unhealthy", + "message": f"API unreachable: {e}", + } + + async def _check_circuit_breaker(self, status_data: dict[str, Any]) -> None: + """Check circuit breaker status. + + :param status_data: Status data to update + """ + metrics = self._client.get_circuit_metrics() + + # Fast path: circuit breaker disabled + if metrics is None: + status_data["checks"]["circuit_breaker"] = { + "status": "disabled", + "message": "Circuit breaker not enabled", + } + return + + cb_state_raw = metrics.get("state", "unknown") + cb_state = str(cb_state_raw) + + success_rate_raw = metrics.get("success_rate", 0.0) + try: + success_rate = float(success_rate_raw) + except (TypeError, ValueError): + success_rate = 0.0 + + # Determine status using helper (pattern matching) + cb_status = self._helper.determine_circuit_status(cb_state, success_rate) + + if cb_status == "unhealthy": + status_data["healthy"] = False + + status_data["checks"]["circuit_breaker"] = { + "status": cb_status, + "state": cb_state, + "success_rate": success_rate, + "message": f"Circuit {cb_state.lower()}, {success_rate:.1%} success", + } + status_data["metrics"]["circuit_success_rate"] = success_rate + + async def _check_performance(self, status_data: dict[str, Any]) -> None: + """Check performance metrics. + + :param status_data: Status data to update + """ + success_rate = self._metrics.success_rate + avg_time = self._metrics.avg_response_time + + # Determine status using helper (pattern matching) + perf_status = self._helper.determine_performance_status(success_rate, avg_time) + + if perf_status == "unhealthy": + status_data["healthy"] = False + + status_data["checks"]["performance"] = { + "status": perf_status, + "success_rate": success_rate, + "failure_rate": self._metrics.failure_rate, + "total_requests": self._metrics.total_requests, + "avg_response_time": avg_time, + "uptime": self._metrics.uptime, + "message": f"{success_rate:.1%} success, {avg_time:.2f}s avg", + } + + status_data["metrics"]["success_rate"] = success_rate + status_data["metrics"]["avg_response_time"] = avg_time + + async def _run_custom_checks(self, status_data: dict[str, Any]) -> None: + """Run custom checks with error handling. + + :param status_data: Status data to update + """ + for name, check_func in self._custom_checks.items(): + try: + result = await check_func(self._client) + status_data["checks"][name] = result + + if result.get("status") == "unhealthy": + status_data["healthy"] = False + + except Exception as e: + _log_if_enabled(logging.ERROR, f"Custom check '{name}' failed: {e}") + status_data["checks"][name] = { + "status": "error", + "message": f"Check failed: {e}", + } + + def add_custom_check( + self, + name: str, + check_func: Callable[[AsyncOutlineClient], Coroutine[Any, Any, dict[str, Any]]], + ) -> None: + """Register custom health check with validation. + + :param name: Check name + :param check_func: Async check function + :raises ValueError: If name or function is invalid + """ + # Validate using pattern matching + match (name.strip(), callable(check_func)): + case ("", _): + raise ValueError("Check name cannot be empty") + case (_, False): + raise ValueError("Check function must be callable") + case (valid_name, True): + self._custom_checks[valid_name] = check_func + _log_if_enabled(logging.DEBUG, f"Registered custom check: {valid_name}") + + def remove_custom_check(self, name: str) -> bool: + """Remove custom check. + + :param name: Check name + :return: True if removed + """ + result = self._custom_checks.pop(name, None) is not None + + if result: + _log_if_enabled(logging.DEBUG, f"Removed custom check: {name}") + + return result + + def clear_custom_checks(self) -> int: + """Clear all custom checks. + + :return: Number cleared + """ + count = len(self._custom_checks) + self._custom_checks.clear() + + _log_if_enabled(logging.DEBUG, f"Cleared {count} custom check(s)") + + return count + + def record_request(self, success: bool, duration: float) -> None: + """Record request with optimized EMA calculation. + + :param success: Request success + :param duration: Request duration + :raises ValueError: If duration is negative + """ + if duration < 0: + raise ValueError("Duration cannot be negative") + + self._metrics.total_requests += 1 + + if success: + self._metrics.successful_requests += 1 + else: + self._metrics.failed_requests += 1 + + if self._metrics.avg_response_time == 0: + self._metrics.avg_response_time = duration + else: + self._metrics.avg_response_time = ( + _ALPHA * duration + (1 - _ALPHA) * self._metrics.avg_response_time + ) + + def get_metrics(self) -> dict[str, Any]: + """Get performance metrics. + + :return: Metrics dictionary + """ + return { + "total_requests": self._metrics.total_requests, + "successful_requests": self._metrics.successful_requests, + "failed_requests": self._metrics.failed_requests, + "success_rate": self._metrics.success_rate, + "failure_rate": self._metrics.failure_rate, + "avg_response_time": self._metrics.avg_response_time, + "uptime": self._metrics.uptime, + } + + def reset_metrics(self) -> None: + """Reset performance metrics.""" + self._metrics = PerformanceMetrics() + _log_if_enabled(logging.DEBUG, "Reset performance metrics") + + def invalidate_cache(self) -> None: + """Invalidate health check cache.""" + self._cached_result = None + self._last_check_time = 0.0 + + async def wait_for_healthy( + self, + timeout: float = 60.0, + check_interval: float = 5.0, + ) -> bool: + """Wait for service to become healthy with validation. + + :param timeout: Maximum wait time (seconds) + :param check_interval: Time between checks (seconds) + :return: True if healthy within timeout + :raises ValueError: If parameters invalid + """ + # Validate using pattern matching + match (timeout, check_interval): + case (t, _) if t <= 0: + raise ValueError("Timeout must be positive") + case (_, i) if i <= 0: + raise ValueError("Check interval must be positive") + + start_time = time.monotonic() + deadline = start_time + timeout + + last_error: Exception | None = None + + while True: + try: + if await self.quick_check(): + return True + except Exception as e: + last_error = e + _log_if_enabled(logging.DEBUG, f"Health check failed: {e}") + + remaining = deadline - time.monotonic() + if remaining <= 0: + break + await asyncio.sleep(min(check_interval, remaining)) + + if last_error is not None: + _log_if_enabled(logging.DEBUG, f"Health check failed: {last_error}") + else: + _log_if_enabled(logging.DEBUG, "Health check failed: timeout") + + return False + + @property + def custom_checks_count(self) -> int: + """Get custom check count. + + :return: Custom check count + """ + return len(self._custom_checks) + + @property + def cache_valid(self) -> bool: + """Check cache validity with fast path. + + :return: True if cache valid + """ + # Fast path: no cached result + if self._cached_result is None: + return False + + # Check TTL + current_time = time.monotonic() + return current_time - self._last_check_time < self._cache_ttl + + +__all__ = [ + "HealthMonitor", + "HealthStatus", + "PerformanceMetrics", +] diff --git a/pyoutlineapi/metrics_collector.py b/pyoutlineapi/metrics_collector.py new file mode 100644 index 0000000..b34726d --- /dev/null +++ b/pyoutlineapi/metrics_collector.py @@ -0,0 +1,1017 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import asyncio +import bisect +import logging +import sys +import time +from collections import deque +from contextlib import suppress +from dataclasses import dataclass, field +from functools import lru_cache +from typing import TYPE_CHECKING, Any, Final + +from .common_types import Constants, Validators + +if TYPE_CHECKING: + from collections.abc import Sequence + + from typing_extensions import Self + + from .client import AsyncOutlineClient + +logger = logging.getLogger(__name__) + +# Constants +_MIN_INTERVAL: Final[float] = 1.0 +_MAX_INTERVAL: Final[float] = 3600.0 +_MAX_HISTORY: Final[int] = 100_000 +_PROMETHEUS_CACHE_TTL: Final[int] = 30 # seconds +_WAIT_FOR_TIMEOUT_ERRORS: Final[tuple[type[BaseException], ...]] = ( + TimeoutError, + asyncio.TimeoutError, +) + + +def _log_if_enabled(level: int, message: str) -> None: + """Centralized logging with level check. + + :param level: Logging level + :param message: Log message + """ + if logger.isEnabledFor(level): + logger.log(level, message) + + +def _estimate_size(obj: object, *, max_bytes: int | None = None) -> int: + """Estimate deep size of an object graph (best-effort). + + :param obj: Object to size + :param max_bytes: Optional early-exit threshold + :return: Estimated size in bytes + """ + seen: set[int] = set() + stack: list[object] = [obj] + total = 0 + + while stack: + current = stack.pop() + obj_id = id(current) + if obj_id in seen: + continue + seen.add(obj_id) + + total += sys.getsizeof(current) + if max_bytes is not None and total > max_bytes: + return total + + if isinstance(current, dict): + stack.extend(current.keys()) + stack.extend(current.values()) + elif isinstance(current, (list, tuple, set, frozenset)): + stack.extend(current) + + return total + + +@dataclass(slots=True, frozen=True) +class MetricsSnapshot: + """Immutable metrics snapshot with size validation.""" + + timestamp: float + server_info: dict[str, Any] = field(default_factory=dict) + transfer_metrics: dict[str, Any] = field(default_factory=dict) + experimental_metrics: dict[str, Any] = field(default_factory=dict) + key_count: int = 0 + total_bytes_transferred: int = 0 + + def __post_init__(self) -> None: + """Validate snapshot size. + + :raises ValueError: If snapshot exceeds size limit + """ + max_bytes = Constants.MAX_SNAPSHOT_SIZE_MB * 1024 * 1024 + total_size = _estimate_size( + { + "server": self.server_info, + "transfer": self.transfer_metrics, + "experimental": self.experimental_metrics, + }, + max_bytes=max_bytes, + ) + + if total_size > max_bytes: + msg = ( + f"Snapshot too large: {total_size / 1024 / 1024:.2f} MB " + f"(max {Constants.MAX_SNAPSHOT_SIZE_MB} MB)" + ) + raise ValueError(msg) + + @property + def _dict_cache(self) -> dict[str, Any]: + """Dictionary representation. + + Note: slots + frozen dataclasses cannot use cached_property safely. + """ + return { + "timestamp": self.timestamp, + "server": self.server_info, + "transfer": self.transfer_metrics, + "experimental": self.experimental_metrics, + "keys_count": self.key_count, + "total_bytes": self.total_bytes_transferred, + } + + def to_dict(self) -> dict[str, Any]: + """Convert snapshot to dictionary (cached). + + :return: Dictionary representation + """ + return self._dict_cache + + +@dataclass(slots=True, frozen=True) +class UsageStats: + """Immutable usage statistics for a time period. + + Provides comprehensive traffic analysis with optimized calculations. + """ + + period_start: float + period_end: float + snapshots_count: int + total_bytes_transferred: int + avg_bytes_per_snapshot: float + peak_bytes: int + active_keys: frozenset[str] = field(default_factory=frozenset) + + @property + def duration(self) -> float: + """Get period duration in seconds (cached). + + :return: Duration in seconds + """ + return max(0.0, self.period_end - self.period_start) + + @property + def bytes_per_second(self) -> float: + """Calculate average bytes per second (cached). + + :return: Bytes per second + """ + duration = self.duration + return 0.0 if duration == 0 else self.total_bytes_transferred / duration + + @property + def megabytes_transferred(self) -> float: + """Get total in megabytes. + + :return: Total MB transferred + """ + return self.total_bytes_transferred / (1024**2) + + @property + def gigabytes_transferred(self) -> float: + """Get total in gigabytes. + + :return: Total GB transferred + """ + return self.total_bytes_transferred / (1024**3) + + @property + def _dict_cache(self) -> dict[str, Any]: + """Dictionary representation. + + Note: slots + frozen dataclasses cannot use cached_property safely. + """ + return { + "period_start": self.period_start, + "period_end": self.period_end, + "duration": self.duration, + "snapshots_count": self.snapshots_count, + "total_bytes_transferred": self.total_bytes_transferred, + "megabytes_transferred": self.megabytes_transferred, + "gigabytes_transferred": self.gigabytes_transferred, + "avg_bytes_per_snapshot": self.avg_bytes_per_snapshot, + "peak_bytes": self.peak_bytes, + "bytes_per_second": self.bytes_per_second, + "active_keys_count": len(self.active_keys), + } + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary (cached). + + :return: Dictionary representation + """ + return self._dict_cache + + +class PrometheusExporter: + """Helper class for Prometheus metrics export with caching. + + Optimized for high-frequency exports with minimal overhead. + """ + + __slots__ = ("_cache", "_cache_time", "_cache_ttl") + + def __init__(self, cache_ttl: int = _PROMETHEUS_CACHE_TTL) -> None: + """Initialize exporter with caching. + + :param cache_ttl: Cache TTL in seconds + """ + self._cache: dict[str, str] = {} + self._cache_time: dict[str, float] = {} + self._cache_ttl = cache_ttl + + @staticmethod + @lru_cache(maxsize=256) + def _format_single_metric( + name: str, + value: float | int, + metric_type: str, + help_text: str, + labels_tuple: tuple[tuple[str, str], ...] | None, + ) -> str: + """Format single Prometheus metric (cached via LRU). + + :param name: Metric name + :param value: Metric value + :param metric_type: Metric type + :param help_text: Help text + :param labels_tuple: Labels as tuple for hashability + :return: Formatted metric string + """ + lines: list[str] = [] + + if help_text: + lines.append(f"# HELP {name} {help_text}") + lines.append(f"# TYPE {name} {metric_type}") + + if labels_tuple: + label_str = ",".join(f'{k}="{v}"' for k, v in labels_tuple) + lines.append(f"{name}{{{label_str}}} {value}") + else: + lines.append(f"{name} {value}") + + return "\n".join(lines) + + def format_metric( + self, + name: str, + value: float | int, + metric_type: str = "gauge", + help_text: str = "", + labels: dict[str, str] | None = None, + ) -> list[str]: + """Format single Prometheus metric. + + :param name: Metric name + :param value: Metric value + :param metric_type: Metric type + :param help_text: Help text description + :param labels: Optional labels dictionary + :return: List of formatted metric lines + """ + # Convert labels dict to tuple for caching + labels_tuple = tuple(sorted(labels.items())) if labels else None + metric_str = self._format_single_metric( + name, value, metric_type, help_text, labels_tuple + ) + return metric_str.split("\n") + + def format_metrics_batch( + self, + metrics: Sequence[tuple[str, float | int, str, str, dict[str, str] | None]], + cache_key: str | None = None, + ) -> str: + """Format multiple metrics at once with optional caching. + + :param metrics: Sequence of (name, value, type, help, labels) tuples + :param cache_key: Optional cache key for result caching + :return: Formatted Prometheus metrics string + """ + # Check cache if key provided + if cache_key: + current_time = time.monotonic() + if cache_key in self._cache: + cache_age = current_time - self._cache_time.get(cache_key, 0) + if cache_age < self._cache_ttl: + return self._cache[cache_key] + + # Format metrics + all_lines: list[str] = [] + for name, value, metric_type, help_text, labels in metrics: + metric_lines = self.format_metric( + name, value, metric_type, help_text, labels + ) + all_lines.extend(metric_lines) + all_lines.append("") # Empty line between metrics + + result = "\n".join(all_lines) + + # Update cache if key provided + if cache_key: + self._cache[cache_key] = result + self._cache_time[cache_key] = time.monotonic() + + return result + + def clear_cache(self) -> None: + """Clear export cache.""" + self._cache.clear() + self._cache_time.clear() + + +class MetricsCollector: + """Metrics collector with optimized performance.""" + + __slots__ = ( + "_client", + "_experimental_since", + "_history", + "_interval", + "_max_history", + "_prometheus_exporter", + "_running", + "_shutdown_event", + "_start_time", + "_stats_cache", + "_stats_cache_time", + "_task", + ) + + def __init__( + self, + client: AsyncOutlineClient, + *, + interval: float = 60.0, + max_history: int = 1440, + experimental_since: str = "24h", + ) -> None: + """Initialize metrics collector. + + :param client: AsyncOutlineClient instance + :param interval: Collection interval in seconds + :param max_history: Maximum snapshots to keep + :param experimental_since: Time range for experimental metrics + :raises ValueError: If parameters invalid + """ + if not _MIN_INTERVAL <= interval <= _MAX_INTERVAL: + msg = f"Interval must be between {_MIN_INTERVAL} and {_MAX_INTERVAL}" + raise ValueError(msg) + + if not 1 <= max_history <= _MAX_HISTORY: + msg = f"max_history must be between 1 and {_MAX_HISTORY}" + raise ValueError(msg) + + self._client = client + self._interval = interval + self._max_history = max_history + self._experimental_since = Validators.validate_since(experimental_since) + + # Use deque for O(1) append/popleft operations + self._history: deque[MetricsSnapshot] = deque(maxlen=max_history) + + self._prometheus_exporter = PrometheusExporter() + self._running = False + self._shutdown_event = asyncio.Event() + self._task: asyncio.Task[None] | None = None + self._start_time: float = 0.0 + + # Stats cache + self._stats_cache: UsageStats | None = None + self._stats_cache_time: float = 0.0 + + async def _collect_single_snapshot(self) -> MetricsSnapshot | None: + """Collect a single metrics snapshot. + + :return: MetricsSnapshot or None on error + """ + try: + # Gather all metrics concurrently + server_task = asyncio.create_task( + self._client.get_server_info(as_json=True) + ) + transfer_task = asyncio.create_task( + self._client.get_transfer_metrics(as_json=True) + ) + keys_task = asyncio.create_task(self._client.get_access_keys(as_json=True)) + + # Use gather with return_exceptions for resilience + results = await asyncio.gather( + server_task, + transfer_task, + keys_task, + return_exceptions=True, + ) + + server_info, transfer_metrics, keys = results + + # Handle errors gracefully + server_dict = server_info if isinstance(server_info, dict) else {} + transfer_dict = ( + transfer_metrics if isinstance(transfer_metrics, dict) else {} + ) + keys_list = keys.get("accessKeys", []) if isinstance(keys, dict) else [] + + # Try to get experimental metrics (optional) + experimental_dict: dict[str, Any] = {} + with suppress(Exception): + exp_metrics = await self._client.get_experimental_metrics( + self._experimental_since, + as_json=True, + ) + experimental_dict = exp_metrics if isinstance(exp_metrics, dict) else {} + + # Calculate total bytes + total_bytes = transfer_dict.get("bytesTransferredByUserId", {}) + if isinstance(total_bytes, dict): + total_bytes_sum = sum( + value for value in total_bytes.values() if isinstance(value, int) + ) + else: + total_bytes_sum = 0 + + timestamp = time.monotonic() + + return MetricsSnapshot( + timestamp=timestamp, + server_info=server_dict, + transfer_metrics=transfer_dict, + experimental_metrics=experimental_dict, + key_count=len(keys_list) if isinstance(keys_list, list) else 0, + total_bytes_transferred=total_bytes_sum, + ) + + except Exception as exc: + _log_if_enabled( + logging.ERROR, + f"Failed to collect metrics snapshot: {exc}", + ) + return None + + async def _collect_loop(self) -> None: + """Main collection loop with error recovery.""" + consecutive_errors = 0 + max_consecutive_errors = 3 + + while self._running and not self._shutdown_event.is_set(): + try: + snapshot = await self._collect_single_snapshot() + + if snapshot is not None: + self._history.append(snapshot) + consecutive_errors = 0 # Reset error counter + _log_if_enabled( + logging.DEBUG, + f"Collected metrics snapshot (history size: {len(self._history)})", + ) + else: + consecutive_errors += 1 + if consecutive_errors >= max_consecutive_errors: + _log_if_enabled( + logging.WARNING, + f"Failed to collect metrics {consecutive_errors} times consecutively", + ) + # Don't break, keep trying + + # Invalidate stats cache + self._stats_cache = None + + except asyncio.CancelledError: + _log_if_enabled(logging.INFO, "Metrics collection cancelled") + break + except Exception as exc: + _log_if_enabled( + logging.ERROR, + f"Unexpected error in collection loop: {exc}", + ) + consecutive_errors += 1 + + # Wait for next collection + try: + await asyncio.wait_for( + self._shutdown_event.wait(), + timeout=self._interval, + ) + break # Shutdown signaled + except _WAIT_FOR_TIMEOUT_ERRORS: + pass # Normal timeout, continue loop + + if consecutive_errors > 0: + _log_if_enabled( + logging.WARNING, + f"Failed to collect metrics {consecutive_errors} times consecutively", + ) + + async def start(self) -> None: + """Start metrics collection. + + :raises RuntimeError: If already running + """ + if self._running: + msg = "Collector already running" + raise RuntimeError(msg) + + self._running = True + self._shutdown_event.clear() + self._start_time = time.monotonic() + self._task = asyncio.create_task(self._collect_loop()) + + _log_if_enabled( + logging.INFO, + f"Metrics collector started (interval={self._interval}s, max_history={self._max_history})", + ) + + async def stop(self) -> None: + """Stop metrics collection gracefully.""" + if not self._running: + return + + _log_if_enabled(logging.INFO, "Stopping metrics collector...") + + self._running = False + self._shutdown_event.set() + + if self._task and not self._task.done(): + # Give task time to finish gracefully + try: + await asyncio.wait_for(self._task, timeout=5.0) + except _WAIT_FOR_TIMEOUT_ERRORS: + _log_if_enabled( + logging.WARNING, + "Collection task did not finish gracefully, cancelling", + ) + self._task.cancel() + with suppress(asyncio.CancelledError): + await self._task + + self._task = None + _log_if_enabled(logging.INFO, "Metrics collector stopped") + + def get_snapshots( + self, + *, + start_time: float | None = None, + end_time: float | None = None, + limit: int | None = None, + ) -> list[MetricsSnapshot]: + """Get metrics snapshots with optional filtering. + + :param start_time: Filter snapshots after this timestamp + :param end_time: Filter snapshots before this timestamp + :param limit: Maximum snapshots to return + :return: List of snapshots + """ + snapshots = list(self._history) + + # Apply time filters using binary search for efficiency + if start_time is not None: + # Find first snapshot >= start_time + idx = bisect.bisect_left( + [s.timestamp for s in snapshots], + start_time, + ) + snapshots = snapshots[idx:] + + if end_time is not None: + # Find last snapshot <= end_time + idx = bisect.bisect_right( + [s.timestamp for s in snapshots], + end_time, + ) + snapshots = snapshots[:idx] + + if limit is not None and limit > 0: + snapshots = snapshots[-limit:] + + return snapshots + + def get_latest_snapshot(self) -> MetricsSnapshot | None: + """Get most recent snapshot. + + :return: Latest snapshot or None + """ + return self._history[-1] if self._history else None + + def get_usage_stats( + self, + *, + start_time: float | None = None, + end_time: float | None = None, + ) -> UsageStats: + """Calculate usage statistics for period (with caching). + + :param start_time: Period start timestamp + :param end_time: Period end timestamp + :return: Usage statistics + """ + # Check cache (only for full history queries) + if start_time is None and end_time is None: + current_time = time.monotonic() + cache_age = current_time - self._stats_cache_time + + if self._stats_cache is not None and cache_age < 5.0: + return self._stats_cache + + snapshots = self.get_snapshots(start_time=start_time, end_time=end_time) + + if not snapshots: + return UsageStats( + period_start=0.0, + period_end=0.0, + snapshots_count=0, + total_bytes_transferred=0, + avg_bytes_per_snapshot=0.0, + peak_bytes=0, + active_keys=frozenset(), + ) + + # Calculate stats efficiently + total_bytes = 0 + peak_bytes = 0 + active_keys_set: set[str] = set() + + for snapshot in snapshots: + total_bytes += snapshot.total_bytes_transferred + peak_bytes = max(peak_bytes, snapshot.total_bytes_transferred) + + # Extract active keys + bytes_by_user = snapshot.transfer_metrics.get( + "bytesTransferredByUserId", {} + ) + if isinstance(bytes_by_user, dict): + active_keys_set.update(k for k, v in bytes_by_user.items() if v > 0) + + avg_bytes = total_bytes / len(snapshots) if snapshots else 0.0 + + stats = UsageStats( + period_start=snapshots[0].timestamp, + period_end=snapshots[-1].timestamp, + snapshots_count=len(snapshots), + total_bytes_transferred=total_bytes, + avg_bytes_per_snapshot=avg_bytes, + peak_bytes=peak_bytes, + active_keys=frozenset(active_keys_set), + ) + + # Update cache for full history queries + if start_time is None and end_time is None: + self._stats_cache = stats + self._stats_cache_time = time.monotonic() + + return stats + + def export_prometheus( + self, + *, + include_per_key: bool = False, + ) -> str: + """Export all metrics in Prometheus format (with caching). + + :param include_per_key: Include per-key metrics + :return: Prometheus formatted metrics + """ + latest = self.get_latest_snapshot() + if latest is None: + return "" + + # Use cache key based on parameters + cache_key = f"full_{include_per_key}_{latest.timestamp}" + + base_metrics: list[tuple[str, float | int, str, str, dict[str, str] | None]] = [ + ( + "outline_keys_total", + latest.key_count, + "gauge", + "Total number of access keys", + None, + ), + ( + "outline_bytes_transferred_total", + latest.total_bytes_transferred, + "counter", + "Total bytes transferred across all keys", + None, + ), + ( + "outline_megabytes_transferred_total", + latest.total_bytes_transferred / (1024**2), + "counter", + "Total megabytes transferred", + None, + ), + ( + "outline_gigabytes_transferred_total", + latest.total_bytes_transferred / (1024**3), + "counter", + "Total gigabytes transferred", + None, + ), + ( + "outline_snapshots_collected_total", + len(self._history), + "counter", + "Total snapshots collected", + None, + ), + ( + "outline_collector_interval_seconds", + self._interval, + "gauge", + "Metrics collection interval in seconds", + None, + ), + ( + "outline_collector_uptime_seconds", + self.uptime, + "counter", + "Collector uptime in seconds", + None, + ), + ] + + # Add server info metrics if available + if "metricsEnabled" in latest.server_info: + metrics_enabled = latest.server_info["metricsEnabled"] + base_metrics.append( + ( + "outline_metrics_enabled", + 1 if metrics_enabled else 0, + "gauge", + "Whether metrics collection is enabled on server", + None, + ) + ) + + if "portForNewAccessKeys" in latest.server_info: + port = latest.server_info["portForNewAccessKeys"] + base_metrics.append( + ( + "outline_default_port", + port, + "gauge", + "Default port for new access keys", + None, + ) + ) + + # Add per-key metrics if requested + if include_per_key and "bytesTransferredByUserId" in latest.transfer_metrics: + bytes_by_user = latest.transfer_metrics["bytesTransferredByUserId"] + if isinstance(bytes_by_user, dict): + for key_id, bytes_transferred in bytes_by_user.items(): + base_metrics.extend( + [ + ( + "outline_key_bytes_total", + bytes_transferred, + "counter", + "Total bytes transferred by specific key", + {"key_id": str(key_id)}, + ), + ( + "outline_key_megabytes_total", + bytes_transferred / (1024**2), + "counter", + "Total megabytes transferred by specific key", + {"key_id": str(key_id)}, + ), + ] + ) + + # Add experimental metrics if available + if "server" in latest.experimental_metrics: + server_exp = latest.experimental_metrics["server"] + + # Tunnel time + if "tunnelTime" in server_exp and "seconds" in server_exp["tunnelTime"]: + tunnel_seconds = server_exp["tunnelTime"]["seconds"] + base_metrics.extend( + [ + ( + "outline_tunnel_time_seconds_total", + tunnel_seconds, + "counter", + "Total tunnel connection time in seconds", + None, + ), + ( + "outline_tunnel_time_hours_total", + tunnel_seconds / 3600, + "counter", + "Total tunnel connection time in hours", + None, + ), + ] + ) + + # Bandwidth - current + if "bandwidth" in server_exp: + bandwidth = server_exp["bandwidth"] + if "current" in bandwidth and "data" in bandwidth["current"]: + current_data = bandwidth["current"]["data"] + if "bytes" in current_data: + current_bw = current_data["bytes"] + base_metrics.append( + ( + "outline_bandwidth_current_bytes", + current_bw, + "gauge", + "Current bandwidth usage in bytes", + None, + ) + ) + + # Bandwidth - peak + if "peak" in bandwidth and "data" in bandwidth["peak"]: + peak_data = bandwidth["peak"]["data"] + if "bytes" in peak_data: + peak_bw = peak_data["bytes"] + base_metrics.append( + ( + "outline_bandwidth_peak_bytes", + peak_bw, + "gauge", + "Peak bandwidth usage in bytes", + None, + ) + ) + + # Location metrics + if "locations" in server_exp: + locations = server_exp["locations"] + if isinstance(locations, list): + for loc in locations: + if not isinstance(loc, dict): + continue + + location = loc.get("location", "unknown") + loc_bytes = 0 + loc_time = 0 + + if ( + "dataTransferred" in loc + and "bytes" in loc["dataTransferred"] + ): + loc_bytes = loc["dataTransferred"]["bytes"] + + if "tunnelTime" in loc and "seconds" in loc["tunnelTime"]: + loc_time = loc["tunnelTime"]["seconds"] + + if loc_bytes > 0 or loc_time > 0: + base_metrics.extend( + [ + ( + "outline_location_bytes_total", + loc_bytes, + "counter", + "Total bytes transferred by location", + {"location": str(location)}, + ), + ( + "outline_location_tunnel_seconds_total", + loc_time, + "counter", + "Total tunnel time by location", + {"location": str(location)}, + ), + ] + ) + + return self._prometheus_exporter.format_metrics_batch( + base_metrics, + cache_key=cache_key, + ) + + def export_prometheus_summary(self) -> str: + """Export summary metrics in Prometheus format (lightweight, cached). + + :return: Prometheus formatted summary metrics + """ + latest = self.get_latest_snapshot() + if latest is None: + return "" + + stats = self.get_usage_stats() + cache_key = f"summary_{latest.timestamp}" + + summary_metrics: list[ + tuple[str, float | int, str, str, dict[str, str] | None] + ] = [ + ( + "outline_keys_total", + latest.key_count, + "gauge", + "Total number of access keys", + None, + ), + ( + "outline_bytes_transferred_total", + latest.total_bytes_transferred, + "counter", + "Total bytes transferred", + None, + ), + ( + "outline_bytes_per_second", + stats.bytes_per_second, + "gauge", + "Average bytes per second", + None, + ), + ( + "outline_active_keys_total", + len(stats.active_keys), + "gauge", + "Number of active keys", + None, + ), + ( + "outline_snapshots_total", + len(self._history), + "counter", + "Total snapshots collected", + None, + ), + ] + + return self._prometheus_exporter.format_metrics_batch( + summary_metrics, + cache_key=cache_key, + ) + + def clear_history(self) -> None: + """Clear collected metrics history and caches.""" + self._history.clear() + self._stats_cache = None + self._prometheus_exporter.clear_cache() + _log_if_enabled(logging.INFO, "Metrics history and caches cleared") + + @property + def is_running(self) -> bool: + """Check if collector is running. + + :return: True if running + """ + return self._running + + @property + def snapshots_count(self) -> int: + """Get number of collected snapshots. + + :return: Snapshot count + """ + return len(self._history) + + @property + def uptime(self) -> float: + """Get collector uptime in seconds. + + :return: Uptime in seconds + """ + if not self._running or self._start_time == 0: + return 0.0 + return time.monotonic() - self._start_time + + async def __aenter__(self) -> Self: + """Context manager entry. + + :return: Self + """ + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object | None, + ) -> None: + """Context manager exit. + + :param exc_type: Exception type + :param exc_val: Exception value + :param exc_tb: Exception traceback + """ + await self.stop() + + +__all__ = [ + "MetricsCollector", + "MetricsSnapshot", + "UsageStats", +] diff --git a/pyoutlineapi/models.py b/pyoutlineapi/models.py index 308a856..f6a1f1b 100644 --- a/pyoutlineapi/models.py +++ b/pyoutlineapi/models.py @@ -1,5 +1,4 @@ -""" -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. Copyright (c) 2025 Denis Rozhnovskiy All rights reserved. @@ -12,248 +11,687 @@ https://github.com/orenlab/pyoutlineapi """ -from typing import Optional +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Any, Final, cast + +from pydantic import Field, field_validator + +from .common_types import ( + BaseValidatedModel, + Bytes, + BytesPerUserDict, + ChecksDict, + Constants, + Port, + TimestampMs, + TimestampSec, + Validators, +) + +if TYPE_CHECKING: + from typing_extensions import Self + +# Constants for unit conversions (immutable, typed) +_BYTES_IN_KB: Final[int] = 1024 +_BYTES_IN_MB: Final[int] = 1024**2 +_BYTES_IN_GB: Final[int] = 1024**3 +_MS_IN_SEC: Final[float] = 1000.0 +_SEC_IN_MIN: Final[float] = 60.0 +_SEC_IN_HOUR: Final[float] = 3600.0 + + +# ===== Unit Conversion Mixins ===== + + +class ByteConversionMixin: + """Mixin for byte conversion utilities with optimized calculations.""" + + bytes: int + + @property + def kilobytes(self) -> float: + """Get value in kilobytes. + + :return: Value in KB + """ + return self.bytes / _BYTES_IN_KB + + @property + def megabytes(self) -> float: + """Get value in megabytes. + + :return: Value in MB + """ + return self.bytes / _BYTES_IN_MB + + @property + def gigabytes(self) -> float: + """Get value in gigabytes. + + :return: Value in GB + """ + return self.bytes / _BYTES_IN_GB + + +class TimeConversionMixin: + """Mixin for time conversion utilities with optimized calculations.""" -from pydantic import BaseModel, Field, field_validator + seconds: int + @property + def minutes(self) -> float: + """Get time in minutes. -class DataLimit(BaseModel): - """Data transfer limit configuration.""" + :return: Time in minutes + """ + return self.seconds / _SEC_IN_MIN - bytes: int = Field(ge=0, description="Data limit in bytes") + @property + def hours(self) -> float: + """Get time in hours. + + :return: Time in hours + """ + return self.seconds / _SEC_IN_HOUR + + +# ===== Core Models ===== + + +class DataLimit(BaseValidatedModel, ByteConversionMixin): + """Data transfer limit in bytes with unit conversions.""" + + bytes: Bytes @classmethod - @field_validator("bytes") - def validate_bytes(cls, v: int) -> int: - if v < 0: - raise ValueError("bytes must be non-negative") - return v - - -class AccessKey(BaseModel): - """Access key details.""" - - id: str = Field(description="Access key identifier") - name: Optional[str] = Field(None, description="Access key name") - password: str = Field(description="Access key password") - port: int = Field(gt=0, lt=65536, description="Port number") - method: str = Field(description="Encryption method") - access_url: str = Field(alias="accessUrl", description="Complete access URL") - data_limit: Optional[DataLimit] = Field( - None, alias="dataLimit", description="Data limit for this key" - ) + def from_kilobytes(cls, kb: float) -> Self: + """Create DataLimit from kilobytes. + :param kb: Size in kilobytes + :return: DataLimit instance + """ + return cls(bytes=int(kb * _BYTES_IN_KB)) -class AccessKeyList(BaseModel): - """List of access keys.""" + @classmethod + def from_megabytes(cls, mb: float) -> Self: + """Create DataLimit from megabytes. - access_keys: list[AccessKey] = Field(alias="accessKeys") + :param mb: Size in megabytes + :return: DataLimit instance + """ + return cls(bytes=int(mb * _BYTES_IN_MB)) + + @classmethod + def from_gigabytes(cls, gb: float) -> Self: + """Create DataLimit from gigabytes. + + :param gb: Size in gigabytes + :return: DataLimit instance + """ + return cls(bytes=int(gb * _BYTES_IN_GB)) -class ServerMetrics(BaseModel): +class AccessKey(BaseValidatedModel): + """Access key model matching API schema with optimized properties. + + SCHEMA: Based on OpenAPI /access-keys endpoint """ - Server metrics data for data transferred per access key. - Per OpenAPI: /metrics/transfer endpoint + + id: str + name: str | None = None + password: str + port: Port + method: str + access_url: str = Field(alias="accessUrl") + data_limit: DataLimit | None = Field(None, alias="dataLimit") + + @field_validator("name", mode="before") + @classmethod + def validate_name(cls, v: str | None) -> str | None: + """Validate and normalize name from API.""" + if v is None: + return None + + if isinstance(v, str): + stripped = v.strip() + if not stripped: + return None + + if len(stripped) > Constants.MAX_NAME_LENGTH: + raise ValueError( + f"Name too long: {len(stripped)} (max {Constants.MAX_NAME_LENGTH})" + ) + return stripped + + @field_validator("id") + @classmethod + def validate_id(cls, v: str) -> str: + """Validate key ID. + + :param v: Key ID + :return: Validated key ID + :raises ValueError: If ID is invalid + """ + return Validators.validate_key_id(v) + + @property + def has_data_limit(self) -> bool: + """Check if key has data limit (optimized None check). + + :return: True if data limit exists + """ + return self.data_limit is not None + + @property + def display_name(self) -> str: + """Get display name with optimized conditional. + + :return: Display name + """ + return self.name if self.name else f"Key-{self.id}" + + +class AccessKeyList(BaseValidatedModel): + """List of access keys with optimized utility methods. + + SCHEMA: Based on GET /access-keys response """ - bytes_transferred_by_user_id: dict[str, int] = Field( - alias="bytesTransferredByUserId", - description="Data transferred by each access key ID", - ) + access_keys: list[AccessKey] = Field(alias="accessKeys") + @cached_property + def count(self) -> int: + """Get number of access keys (cached). -class TunnelTime(BaseModel): - """Tunnel time data structure.""" + NOTE: Cached because list is immutable after creation - seconds: int = Field(description="Time in seconds") + :return: Key count + """ + return len(self.access_keys) + @property + def is_empty(self) -> bool: + """Check if list is empty (uses cached count). -class DataTransferred(BaseModel): - """Data transfer information.""" + :return: True if no keys + """ + return self.count == 0 - bytes: int = Field(description="Bytes transferred") + def get_by_id(self, key_id: str) -> AccessKey | None: + """Get key by ID with early return optimization. + :param key_id: Access key ID + :return: Access key or None if not found + """ + for key in self.access_keys: + if key.id == key_id: + return key + return None -class BandwidthData(BaseModel): - """Bandwidth measurement data.""" + def get_by_name(self, name: str) -> list[AccessKey]: + """Get keys by name with optimized list comprehension. - data: dict[str, int] = Field(description="Bandwidth data with bytes field") - timestamp: Optional[int] = Field(None, description="Unix timestamp") + :param name: Key name + :return: List of matching keys (may be multiple) + """ + return [key for key in self.access_keys if key.name == name] + def filter_with_limits(self) -> list[AccessKey]: + """Get keys with data limits (optimized comprehension). -class BandwidthInfo(BaseModel): - """Current and peak bandwidth information.""" + :return: List of keys with limits + """ + return [key for key in self.access_keys if key.has_data_limit] - current: BandwidthData = Field(description="Current bandwidth") - peak: BandwidthData = Field(description="Peak bandwidth") + def filter_without_limits(self) -> list[AccessKey]: + """Get keys without data limits (optimized comprehension). + :return: List of keys without limits + """ + return [key for key in self.access_keys if not key.has_data_limit] -class LocationMetric(BaseModel): - """Location metric model.""" - location: str = Field(..., description="Location identifier") - asn: Optional[int] = Field(None, description="ASN number") - as_org: Optional[str] = Field(None, alias="asOrg", description="AS organization") - tunnel_time: "TunnelTime" = Field(..., alias="tunnelTime") - data_transferred: "DataTransferred" = Field(..., alias="dataTransferred") - @classmethod - @field_validator('asn', mode='before') - def validate_asn(cls, v): - """Convert 0 to None for ASN.""" - if v == 0: - return None - return v +class Server(BaseValidatedModel): + """Server information model with optimized properties. + SCHEMA: Based on GET /server response + """ + + name: str | None = None + server_id: str = Field(alias="serverId") + metrics_enabled: bool = Field(alias="metricsEnabled") + created_timestamp_ms: TimestampMs = Field(alias="createdTimestampMs") + port_for_new_access_keys: Port = Field(alias="portForNewAccessKeys") + hostname_for_access_keys: str | None = Field(None, alias="hostnameForAccessKeys") + access_key_data_limit: DataLimit | None = Field(None, alias="accessKeyDataLimit") + version: str | None = None + + @field_validator("name", mode="before") @classmethod - @field_validator('as_org', mode='before') - def validate_as_org(cls, v): - """Convert empty string to None for AS organization.""" - if v == "" or v == 0: - return None - return v + def validate_name(cls, v: str) -> str: + """Validate server name. + :param v: Server name + :return: Validated name + :raises ValueError: If name is empty + """ + validated = Validators.validate_name(v) + if validated is None: + raise ValueError("Server name cannot be empty") + return validated -class PeakDeviceCount(BaseModel): - """Peak device count information.""" + @property + def has_global_limit(self) -> bool: + """Check if server has global data limit (optimized). - data: int = Field(description="Peak device count") - timestamp: int = Field(description="Unix timestamp") + :return: True if global limit exists + """ + return self.access_key_data_limit is not None + @cached_property + def created_timestamp_seconds(self) -> float: + """Get creation timestamp in seconds (cached). -class ConnectionInfo(BaseModel): - """Connection information for access keys.""" + NOTE: Cached because timestamp is immutable - last_traffic_seen: int = Field( - alias="lastTrafficSeen", description="Last traffic timestamp" + :return: Timestamp in seconds + """ + return self.created_timestamp_ms / _MS_IN_SEC + + +# ===== Metrics Models ===== + + +class ServerMetrics(BaseValidatedModel): + """Transfer metrics with optimized aggregations. + + SCHEMA: Based on GET /metrics/transfer response + """ + + bytes_transferred_by_user_id: BytesPerUserDict = Field( + alias="bytesTransferredByUserId" ) - peak_device_count: PeakDeviceCount = Field(alias="peakDeviceCount") + + @cached_property + def total_bytes(self) -> int: + """Calculate total bytes with caching. + + :return: Total bytes transferred + """ + return sum(self.bytes_transferred_by_user_id.values()) + + @cached_property + def total_gigabytes(self) -> float: + """Get total in gigabytes (uses cached total_bytes). + + :return: Total GB transferred + """ + return self.total_bytes / _BYTES_IN_GB + + @cached_property + def user_count(self) -> int: + """Get number of users (cached). + + :return: Number of users + """ + return len(self.bytes_transferred_by_user_id) + + def get_user_bytes(self, user_id: str) -> int: + """Get bytes for specific user (O(1) dict lookup). + + :param user_id: User/key ID + :return: Bytes transferred or 0 if not found + """ + return self.bytes_transferred_by_user_id.get(user_id, 0) + + def top_users(self, limit: int = 10) -> list[tuple[str, int]]: + """Get top users by bytes transferred (optimized sorting). + + :param limit: Number of top users to return + :return: List of (user_id, bytes) tuples + """ + return sorted( + self.bytes_transferred_by_user_id.items(), + key=lambda x: x[1], + reverse=True, + )[:limit] -class AccessKeyMetric(BaseModel): - """Access key metrics data.""" +class TunnelTime(BaseValidatedModel, TimeConversionMixin): + """Tunnel time metric with time conversions. - access_key_id: int = Field(alias="accessKeyId") + SCHEMA: Based on experimental metrics tunnelTime object + """ + + seconds: int = Field(ge=0) + + +class DataTransferred(BaseValidatedModel, ByteConversionMixin): + """Data transfer metric with byte conversions. + + SCHEMA: Based on experimental metrics dataTransferred object + """ + + bytes: Bytes + + +class BandwidthDataValue(BaseValidatedModel): + """Bandwidth data value. + + SCHEMA: Based on experimental metrics bandwidth data object + """ + + bytes: int + + +class BandwidthData(BaseValidatedModel): + """Bandwidth measurement data. + + SCHEMA: Based on experimental metrics bandwidth current/peak object + """ + + data: BandwidthDataValue + timestamp: TimestampSec | None = None + + +class BandwidthInfo(BaseValidatedModel): + """Current and peak bandwidth information. + + SCHEMA: Based on experimental metrics bandwidth object + """ + + current: BandwidthData + peak: BandwidthData + + +class LocationMetric(BaseValidatedModel): + """Location-based usage metric. + + SCHEMA: Based on experimental metrics locations array item + """ + + location: str + asn: int | None = None + as_org: str | None = Field(None, alias="asOrg") tunnel_time: TunnelTime = Field(alias="tunnelTime") data_transferred: DataTransferred = Field(alias="dataTransferred") - connection: ConnectionInfo = Field(description="Connection metrics") -class ServerExperimentalMetric(BaseModel): - """Server-level experimental metrics.""" +class PeakDeviceCount(BaseValidatedModel): + """Peak device count with timestamp. + + SCHEMA: Based on experimental metrics connection peakDeviceCount object + """ + + data: int + timestamp: TimestampSec + + +class ConnectionInfo(BaseValidatedModel): + """Connection information and statistics. + + SCHEMA: Based on experimental metrics connection object + """ + + last_traffic_seen: TimestampSec = Field(alias="lastTrafficSeen") + peak_device_count: PeakDeviceCount = Field(alias="peakDeviceCount") + + +class AccessKeyMetric(BaseValidatedModel): + """Per-key experimental metrics. + + SCHEMA: Based on experimental metrics accessKeys array item + """ + access_key_id: str = Field(alias="accessKeyId") tunnel_time: TunnelTime = Field(alias="tunnelTime") data_transferred: DataTransferred = Field(alias="dataTransferred") - bandwidth: BandwidthInfo = Field(description="Bandwidth information") - locations: list[LocationMetric] = Field(description="Location-based metrics") + connection: ConnectionInfo -class ExperimentalMetrics(BaseModel): +class ServerExperimentalMetric(BaseValidatedModel): + """Server-level experimental metrics. + + SCHEMA: Based on experimental metrics server object """ - Experimental metrics data structure. - Per OpenAPI: /experimental/server/metrics endpoint + + tunnel_time: TunnelTime = Field(alias="tunnelTime") + data_transferred: DataTransferred = Field(alias="dataTransferred") + bandwidth: BandwidthInfo + locations: list[LocationMetric] + + +class ExperimentalMetrics(BaseValidatedModel): + """Experimental metrics with optimized lookup. + + SCHEMA: Based on GET /experimental/server/metrics response """ - server: ServerExperimentalMetric = Field(description="Server metrics") - access_keys: list[AccessKeyMetric] = Field( - alias="accessKeys", description="Access key metrics" - ) + server: ServerExperimentalMetric + access_keys: list[AccessKeyMetric] = Field(alias="accessKeys") + + def get_key_metric(self, key_id: str) -> AccessKeyMetric | None: + """Get metrics for specific key with early return. + + :param key_id: Access key ID + :return: Key metrics or None if not found + """ + for metric in self.access_keys: + if metric.access_key_id == key_id: + return metric # Early return + return None + + +# ===== Request Models ===== -class Server(BaseModel): +class AccessKeyCreateRequest(BaseValidatedModel): + """Request model for creating access keys. + + SCHEMA: Based on POST /access-keys request body """ - Server information. - Per OpenAPI: /server endpoint schema + + name: str | None = Field(default=None, min_length=1, max_length=255) + method: str | None = None + password: str | None = None + port: Port | None = None + limit: DataLimit | None = None + + +class ServerNameRequest(BaseValidatedModel): + """Request model for renaming server. + + SCHEMA: Based on PUT /name request body """ - name: str = Field(description="Server name") - server_id: str = Field(alias="serverId", description="Unique server identifier") - metrics_enabled: bool = Field( - alias="metricsEnabled", description="Metrics sharing status" - ) - created_timestamp_ms: int = Field( - alias="createdTimestampMs", description="Creation timestamp in milliseconds" - ) - version: str = Field(description="Server version") - port_for_new_access_keys: int = Field( - alias="portForNewAccessKeys", - gt=0, - lt=65536, - description="Default port for new keys", - ) - hostname_for_access_keys: Optional[str] = Field( - None, alias="hostnameForAccessKeys", description="Hostname for access keys" - ) - access_key_data_limit: Optional[DataLimit] = Field( - None, - alias="accessKeyDataLimit", - description="Global data limit for access keys", - ) + name: str = Field(min_length=1, max_length=255) + +class HostnameRequest(BaseValidatedModel): + """Request model for setting hostname. -class AccessKeyCreateRequest(BaseModel): + SCHEMA: Based on PUT /server/hostname-for-access-keys request body """ - Request parameters for creating an access key. - Per OpenAPI: /access-keys POST request body + + hostname: str = Field(min_length=1) + + +class PortRequest(BaseValidatedModel): + """Request model for setting default port. + + SCHEMA: Based on PUT /server/port-for-new-access-keys request body """ - name: Optional[str] = Field(None, description="Access key name") - method: Optional[str] = Field(None, description="Encryption method") - password: Optional[str] = Field(None, description="Access key password") - port: Optional[int] = Field(None, gt=0, lt=65536, description="Port number") - limit: Optional[DataLimit] = Field(None, description="Data limit for this key") + port: Port -class ServerNameRequest(BaseModel): - """Request for renaming server.""" +class AccessKeyNameRequest(BaseValidatedModel): + """Request model for renaming access key. - name: str = Field(description="New server name") + SCHEMA: Based on PUT /access-keys/{id}/name request body + """ + name: str = Field(min_length=1, max_length=255) -class HostnameRequest(BaseModel): - """Request for changing hostname.""" - hostname: str = Field(description="New hostname or IP address") +class DataLimitRequest(BaseValidatedModel): + """Request model for setting data limit. + Note: + The API expects the DataLimit object directly. + Use to_payload() to produce the correct request body. + """ -class PortRequest(BaseModel): - """Request for changing default port.""" + limit: DataLimit - port: int = Field(gt=0, lt=65536, description="New default port") + def to_payload(self) -> dict[str, dict[str, int]]: + """Convert to API request payload. + :return: Payload dict with limit object + """ + return {"limit": cast(dict[str, int], self.limit.model_dump(by_alias=True))} -class AccessKeyNameRequest(BaseModel): - """Request for renaming access key.""" - name: str = Field(description="New access key name") +class MetricsEnabledRequest(BaseValidatedModel): + """Request model for enabling/disabling metrics. + SCHEMA: Based on PUT /metrics/enabled request body + """ -class DataLimitRequest(BaseModel): - """Request for setting data limit.""" + metrics_enabled: bool = Field(alias="metricsEnabled") - limit: DataLimit = Field(description="Data limit configuration") +class MetricsStatusResponse(BaseValidatedModel): + """Response model for metrics status. -class MetricsEnabledRequest(BaseModel): - """Request for enabling/disabling metrics.""" + Returns current metrics sharing status. + SCHEMA: Based on GET /metrics/enabled response + """ - metrics_enabled: bool = Field( - alias="metricsEnabled", description="Enable or disable metrics" - ) + metrics_enabled: bool = Field(alias="metricsEnabled") -class MetricsStatusResponse(BaseModel): - """Response for /metrics/enabled endpoint.""" +# ===== Response Models ===== - metrics_enabled: bool = Field( - alias="metricsEnabled", description="Current metrics status" - ) +class ErrorResponse(BaseValidatedModel): + """Error response with optimized string formatting. -class ErrorResponse(BaseModel): - """ - Error response structure. - Per OpenAPI: 404 and 400 responses + SCHEMA: Based on API error response format """ - code: str = Field(description="Error code") - message: str = Field(description="Error message") + code: str + message: str + + def __str__(self) -> str: + """Format error as string (optimized f-string). + + :return: Formatted error message + """ + return f"{self.code}: {self.message}" + + +# ===== Utility Models ===== + + +class HealthCheckResult(BaseValidatedModel): + """Health check result with optimized diagnostics.""" + + healthy: bool + timestamp: float + checks: ChecksDict + + @cached_property + def failed_checks(self) -> list[str]: + """Get failed checks (cached for repeated access). + + :return: List of failed check names + """ + return [ + name + for name, result in self.checks.items() + if result.get("status") != "healthy" + ] + + @property + def success_rate(self) -> float: + """Calculate success rate (uses cached failed_checks). + + :return: Success rate (0.0 to 1.0) + """ + if not self.checks: + return 1.0 # Early return + + total = len(self.checks) + passed = total - len(self.failed_checks) # Uses cached property + return passed / total + + +class ServerSummary(BaseValidatedModel): + """Server summary with optimized aggregations.""" + + server: dict[str, Any] + access_keys_count: int + healthy: bool + transfer_metrics: BytesPerUserDict | None = None + experimental_metrics: dict[str, Any] | None = None + error: str | None = None + + @property + def total_bytes_transferred(self) -> int: + """Get total bytes with early return optimization. + + :return: Total bytes or 0 if no metrics + """ + if not self.transfer_metrics: + return 0 # Early return + return sum(self.transfer_metrics.values()) + + @property + def total_gigabytes_transferred(self) -> float: + """Get total GB (uses total_bytes_transferred). + + :return: Total GB or 0.0 if no metrics + """ + return self.total_bytes_transferred / _BYTES_IN_GB + + @property + def has_errors(self) -> bool: + """Check if summary has errors (optimized None check). + + :return: True if errors present + """ + return self.error is not None + + +__all__ = [ + "AccessKey", + "AccessKeyCreateRequest", + "AccessKeyList", + "AccessKeyMetric", + "AccessKeyNameRequest", + "BandwidthData", + "BandwidthDataValue", + "BandwidthInfo", + "ConnectionInfo", + "DataLimit", + "DataLimitRequest", + "DataTransferred", + "ErrorResponse", + "ExperimentalMetrics", + "HealthCheckResult", + "HostnameRequest", + "LocationMetric", + "MetricsEnabledRequest", + "MetricsStatusResponse", + "PeakDeviceCount", + "PortRequest", + "Server", + "ServerExperimentalMetric", + "ServerMetrics", + "ServerNameRequest", + "ServerSummary", + "TunnelTime", +] diff --git a/pyoutlineapi/response_parser.py b/pyoutlineapi/response_parser.py new file mode 100644 index 0000000..bbb525f --- /dev/null +++ b/pyoutlineapi/response_parser.py @@ -0,0 +1,307 @@ +"""PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. + +Copyright (c) 2025 Denis Rozhnovskiy +All rights reserved. + +This software is licensed under the MIT License. +You can find the full license text at: + https://opensource.org/licenses/MIT + +Source code repository: + https://github.com/orenlab/pyoutlineapi +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Final, Literal, TypeVar, cast, overload + +from pydantic import BaseModel, ValidationError + +from .common_types import Constants, JsonDict, JsonValue +from .exceptions import ValidationError as OutlineValidationError + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + +logger = logging.getLogger(__name__) + +# Type aliases +T = TypeVar("T", bound=BaseModel) + +# Constants for optimization +_MAX_LOGGED_ERRORS: Final[int] = 10 +_ERROR_FIELDS: Final[tuple[str, ...]] = ("error", "message", "error_message", "msg") + + +class ResponseParser: + """High-performance utility class for parsing and validating API responses.""" + + __slots__ = () # Stateless class - zero memory overhead + + @staticmethod + @overload + def parse( + data: dict[str, JsonValue], + model: type[T], + *, + as_json: Literal[True] = True, + ) -> JsonDict: ... # pragma: no cover + + @staticmethod + @overload + def parse( + data: dict[str, JsonValue], + model: type[T], + *, + as_json: Literal[False] = False, + ) -> T: ... # pragma: no cover + + @staticmethod + @overload + def parse( + data: dict[str, JsonValue], + model: type[T], + *, + as_json: bool, + ) -> T | JsonDict: ... # pragma: no cover + + @staticmethod + def parse( + data: dict[str, JsonValue], + model: type[T], + *, + as_json: bool = False, + ) -> T | JsonDict: + """Parse and validate response data with comprehensive error handling. + + Type-safe overloads ensure correct return type based on as_json parameter. + + :param data: Raw response data from API + :param model: Pydantic model class for validation + :param as_json: Return raw JSON dict instead of model instance + :return: Validated model instance or JSON dict + :raises ValidationError: If validation fails with detailed error info + + Example: + >>> data = {"name": "test", "id": 123} + >>> # Type-safe: returns MyModel instance + >>> result = ResponseParser.parse(data, MyModel, as_json=False) + >>> # Type-safe: returns dict + >>> json_result = ResponseParser.parse(data, MyModel, as_json=True) + """ + if not isinstance(data, dict): + raise OutlineValidationError( + f"Expected dict, got {type(data).__name__}", + model=model.__name__, + ) + + if not data and logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG): + logger.debug("Parsing empty dict for model %s", model.__name__) + + try: + validated = model.model_validate(data) + + if as_json: + return cast( # type: ignore[redundant-cast, unused-ignore] + JsonDict, validated.model_dump(by_alias=True) + ) + return cast(T, validated) # type: ignore[redundant-cast, unused-ignore] + + except ValidationError as e: + errors = e.errors() + + if not errors: + raise OutlineValidationError( + "Validation failed with no error details", + model=model.__name__, + ) from e + + first_error = errors[0] + field = ".".join(str(loc) for loc in first_error.get("loc", ())) + message = first_error.get("msg", "Validation failed") + + error_count = len(errors) + if error_count > 1: + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "Multiple validation errors for %s: %d error(s)", + model.__name__, + error_count, + ) + + if logger.isEnabledFor(Constants.LOG_LEVEL_DEBUG): + logger.debug("Validation error details:") + logged_count = min(error_count, _MAX_LOGGED_ERRORS) + + for i, error in enumerate(errors[:logged_count], 1): + error_field = ".".join(str(loc) for loc in error.get("loc", ())) + error_msg = error.get("msg", "Unknown error") + logger.debug(" %d. %s: %s", i, error_field, error_msg) + + if error_count > _MAX_LOGGED_ERRORS: + remaining = error_count - _MAX_LOGGED_ERRORS + logger.debug(" ... and %d more error(s)", remaining) + + raise OutlineValidationError( + message, + field=field, + model=model.__name__, + ) from e + + except Exception as e: + # Catch any other unexpected errors during validation + if logger.isEnabledFor(Constants.LOG_LEVEL_ERROR): + logger.error( + "Unexpected error during validation: %s", + e, + exc_info=True, + ) + raise OutlineValidationError( + f"Unexpected error during validation: {e}", + model=model.__name__, + ) from e + + @staticmethod + def parse_simple(data: Mapping[str, JsonValue] | object) -> bool: + """Parse simple success/error responses efficiently. + + Handles various response formats with minimal overhead: + - {"success": true/false} + - {"error": "..."} → False + - {"message": "..."} → False + - Empty dict → True (assumed success) + + :param data: Response data + :return: True if successful, False otherwise + + Example: + >>> ResponseParser.parse_simple({"success": True}) + True + >>> ResponseParser.parse_simple({"error": "Something failed"}) + False + >>> ResponseParser.parse_simple({}) + True + """ + if not isinstance(data, dict): + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "Expected dict in parse_simple, got %s", + type(data).__name__, + ) + return False + + if "success" in data: + success = data["success"] + if not isinstance(success, bool): + if logger.isEnabledFor(Constants.LOG_LEVEL_WARNING): + logger.warning( + "success field is not bool: %s, coercing to bool", + type(success).__name__, + ) + return bool(success) + return success + + return "error" not in data and "message" not in data + + @staticmethod + def validate_response_structure( + data: Mapping[str, JsonValue] | object, + required_fields: Sequence[str] | None = None, + ) -> bool: + """Validate response structure without full parsing. + + Lightweight validation before expensive Pydantic validation. + Useful for early rejection of malformed responses. + + :param data: Response data to validate + :param required_fields: Sequence of required field names + :return: True if structure is valid + + Example: + >>> data = {"id": 1, "name": "test"} + >>> ResponseParser.validate_response_structure(data, ["id", "name"]) + True + >>> ResponseParser.validate_response_structure(data, ["id", "missing"]) + False + """ + if not isinstance(data, dict): + return False + + if not data and not required_fields: + return True + + if not required_fields: + return True + + return all(field in data for field in required_fields) + + @staticmethod + def extract_error_message(data: Mapping[str, JsonValue] | object) -> str | None: + """Extract error message from response data efficiently. + + Checks common error field names in order of preference. + Uses pre-computed tuple for fast iteration. + + :param data: Response data + :return: Error message or None if not found + + Example: + >>> ResponseParser.extract_error_message({"error": "Not found"}) + 'Not found' + >>> ResponseParser.extract_error_message({"message": "Failed"}) + 'Failed' + >>> ResponseParser.extract_error_message({"success": True}) + None + """ + if not isinstance(data, dict): + return None + + for field in _ERROR_FIELDS: + if field in data: + value = data[field] + # Fast path: already a string + if isinstance(value, str): + return value + # Convert non-string to string (None → None) + return str(value) if value is not None else None + + return None + + @staticmethod + def is_error_response(data: Mapping[str, object] | object) -> bool: + """Check if response indicates an error efficiently. + + Fast boolean check for error indicators in response. + + :param data: Response data + :return: True if response indicates an error + + Example: + >>> ResponseParser.is_error_response({"error": "Failed"}) + True + >>> ResponseParser.is_error_response({"success": False}) + True + >>> ResponseParser.is_error_response({"success": True}) + False + >>> ResponseParser.is_error_response({}) + False + """ + if not isinstance(data, dict): + return False + + if "error" in data or "error_message" in data: + return True + + if "success" in data: + success = data["success"] + return success is False + + # No error indicators found + return False + + +__all__ = [ + "JsonDict", + "ResponseParser", +] diff --git a/pyproject.toml b/pyproject.toml index 6a74d46..1d009b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,67 +1,166 @@ -[tool.poetry] +[project] name = "pyoutlineapi" -version = "0.3.0" -description = "A modern, async-first Python client for the Outline VPN Server API with comprehensive data validation through Pydantic models." -authors = ["Denis Rozhnovskiy "] +version = "0.4.0" +description = "Production-ready async Python client for Outline VPN Server API with enterprise-grade features and full type safety." readme = "README.md" -license = "MIT" -packages = [{ include = "pyoutlineapi" }] -keywords = ["outline", "vpn", "api", "manager", "wrapper", "asyncio"] -documentation = "https://orenlab.github.io/pyoutlineapi/" +license = { file = "LICENSE" } +requires-python = ">=3.10,<4.0" + +authors = [ + { name = "Denis Rozhnovskiy", email = "pytelemonbot@mail.ru" } +] + +keywords = [ + "outline", + "vpn", + "api", + "async", + "asyncio", + "aiohttp", + "pydantic", + "client", + "manager", + "production", + "enterprise", +] + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Intended Audience :: Developers", - "Topic :: Internet :: WWW/HTTP :: HTTP Servers", - "Topic :: Security", - "Topic :: Internet :: Proxy Servers", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", - "Typing :: Typed", - "Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Framework :: aiohttp", "Framework :: Pydantic", + "Framework :: Pydantic :: 2", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Internet :: Proxy Servers", + "Topic :: Security", + "Topic :: System :: Networking", + "Operating System :: OS Independent", + "Typing :: Typed", +] + +dependencies = [ + "aiohttp>=3.13.3", + "pydantic>=2.12.5", + "pydantic-settings>=2.12.0", +] + +[project.urls] +Homepage = "https://github.com/orenlab/pyoutlineapi" +Repository = "https://github.com/orenlab/pyoutlineapi" +Documentation = "https://orenlab.github.io/pyoutlineapi/" +Changelog = "https://github.com/orenlab/pyoutlineapi/blob/main/CHANGELOG.md" +"Bug Tracker" = "https://github.com/orenlab/pyoutlineapi/issues" +Discussions = "https://github.com/orenlab/pyoutlineapi/discussions" + + +[dependency-groups] +dev = [ + "pytest>=8.4.2", + "pytest-asyncio>=0.25", + "pytest-cov>=6.0", + "pytest-timeout>=2.3", + "pytest-xdist>=3.6", + "aioresponses>=0.7.8", + "ruff>=0.8", + "mypy>=1.13", + "types-aiofiles>=24.1", + "pdoc>=15.0", + "rich>=14.2", + "bandit>=1.8", + "codeclone>=1.4.0", + "build>=1.2.0", + "twine>=5.0.0", ] -[tool.poetry.dependencies] -python = ">=3.10,<4.0" -pydantic = "^2.11.5" -aiohttp = "^3.12.11" - -[tool.poetry.group.dev.dependencies] -aioresponses = "^0.7.8" -pytest = "^8.3.4" -pytest-asyncio = "^0.25.2" -pytest-cov = "^5.0.0" -black = "^24.10.0" -mypy = "^1.0.0" -ruff = "^0.8.0" -pdoc = "^15.0.1" +# ================== Documentation ================== + +[tool.pdoc] +output_directory = "docs" +math = true +search = true +show_source = true + +[tool.pdoc.pdoc] +docformat = "google" +include_undocumented = false +show_inherited_members = true +members_order = "alphabetical" + + +# ================== Pytest ================== [tool.pytest.ini_options] asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" testpaths = ["tests"] -python_files = ["test_*.py"] -addopts = "-v --cov=pyoutlineapi --cov-report=html --cov-report=xml --cov-report=term-missing" -[tool.black] -line-length = 88 -target-version = ["py310", "py311", "py312", "py313"] -include = '\.pyi?$' +addopts = [ + "-v", + "--strict-markers", + "--strict-config", + "--tb=short", + "--maxfail=5", + "--cov=pyoutlineapi", + "--cov-branch", + "--cov-report=html", + "--cov-report=xml", + "--cov-report=term-missing:skip-covered", + "--cov-fail-under=80", +] -[tool.mypy] -python_version = "3.10" -strict = true -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -show_error_codes = true -pretty = true +markers = [ + "unit", + "integration", + "slow", + "network", +] + +filterwarnings = [ + "error", + "ignore::DeprecationWarning", + "ignore::PendingDeprecationWarning", +] + +timeout = 300 +timeout_method = "thread" + + +# ================== Coverage ================== + +[tool.coverage.run] +source = ["pyoutlineapi"] +branch = true +omit = [ + "tests/*", + "**/__pycache__/*", + "**/site-packages/*", +] + +[tool.coverage.report] +precision = 2 +show_missing = true +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "@abstractmethod", +] + +[tool.coverage.html] +directory = "htmlcov" + + +# ================== Ruff ================== [tool.ruff] line-length = 88 @@ -69,36 +168,77 @@ target-version = "py310" [tool.ruff.lint] select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade - "ARG", # flake8-unused-arguments - "SIM", # flake8-simplify - "TCH", # flake8-type-checking - "PTH", # flake8-use-pathlib - "ASYNC", # flake8-async + "F", "E", "W", "I", + "UP", "B", "C4", "SIM", + "RUF", "D", "ANN", + "S", "ASYNC", "PT", ] ignore = [ - "E501", # line too long, handled by black + "E501", + "PLR0912", + "PLR0913", + "PLR0915", + "PLR2004", + "E402", ] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["ARG", "S101"] # Allow unused arguments and assert statements in tests +"tests/*" = ["S101", "ANN", "D", "PLR2004"] +"__init__.py" = ["F401", "D104"] +"examples/*" = ["T201"] [tool.ruff.lint.isort] known-first-party = ["pyoutlineapi"] +combine-as-imports = true -[tool.poetry.urls] -homepage = "https://github.com/orenlab/pyoutlineapi" -repository = "https://github.com/orenlab/pyoutlineapi" -documentation = "https://orenlab.github.io/pyoutlineapi/" -changelog = "https://github.com/orenlab/pyoutlineapi/blob/main/CHANGELOG.md" -"Bug Tracker" = "https://github.com/orenlab/pyoutlineapi/issues" +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +quote-style = "double" + + +# ================== MyPy ================== + +[tool.mypy] +python_version = "3.10" +files = ["pyoutlineapi"] + +warn_return_any = true +warn_unused_configs = true +warn_unused_ignores = true +warn_unreachable = true +show_error_codes = true +pretty = true + +strict_optional = true +implicit_reexport = true + +[[tool.mypy.overrides]] +module = ["aiohttp.*", "aioresponses.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["pydantic", "pydantic.*", "pydantic_settings"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "tests.*" +check_untyped_defs = false + + +# ================== Bandit ================== + +[tool.bandit] +exclude_dirs = ["tests", "docs", "examples"] +skips = ["B101"] + + +# ================== Build ================== [build-system] -requires = ["poetry-core>=1.8.0"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["pyoutlineapi"] \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6669a01 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from typing import Any + +import pytest + + +class DummyAuditLogger: + def __init__(self) -> None: + self.logged: list[tuple[str, str, dict[str, Any] | None, str | None]] = [] + self.alogged: list[tuple[str, str, dict[str, Any] | None, str | None]] = [] + + def log_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + self.logged.append((action, resource, details, correlation_id)) + + async def alog_action( + self, + action: str, + resource: str, + *, + user: str | None = None, + details: dict[str, Any] | None = None, + correlation_id: str | None = None, + ) -> None: + self.alogged.append((action, resource, details, correlation_id)) + + +class DummyMetrics: + def __init__(self) -> None: + self.counters: list[tuple[str, dict[str, str] | None]] = [] + self.timings: list[tuple[str, float, dict[str, str] | None]] = [] + + def increment(self, name: str, tags: dict[str, str] | None = None) -> None: + self.counters.append((name, tags)) + + def timing( + self, + name: str, + value: float, + tags: dict[str, str] | None = None, + ) -> None: + self.timings.append((name, value, tags)) + + +class _ChunkedContent: + def __init__(self, data: bytes) -> None: + self._data = data + + async def iter_chunked(self, size: int): + for i in range(0, len(self._data), size): + yield self._data[i : i + size] + + +class DummyResponse: + def __init__( + self, + status: int, + body: bytes, + *, + headers: dict[str, str] | None = None, + reason: str | None = None, + json_data: dict[str, Any] | None = None, + json_error: Exception | None = None, + ) -> None: + self.status = status + self.headers = headers or {"Content-Type": "application/json"} + self.reason = reason or "OK" + self._body = body + self._json_data = json_data + self._json_error = json_error + self.content = _ChunkedContent(body) + + async def json(self) -> dict[str, Any]: + if self._json_error is not None: + raise self._json_error + if self._json_data is not None: + return self._json_data + return {} + + +class DummyRequestContext: + def __init__(self, response: DummyResponse) -> None: + self._response = response + + async def __aenter__(self) -> DummyResponse: + return self._response + + async def __aexit__(self, exc_type, exc, tb) -> None: + return None + + +class DummySession: + def __init__(self, responder: Callable[..., DummyResponse]) -> None: + self._responder = responder + self.closed = False + + def request(self, *args: Any, **kwargs: Any) -> DummyRequestContext: + return DummyRequestContext(self._responder(*args, **kwargs)) + + async def close(self) -> None: + self.closed = True + + +@pytest.fixture +def access_key_dict() -> dict[str, Any]: + return { + "id": "key-1", + "name": "Alice", + "password": "secret", + "port": 12345, + "method": "aes-256-gcm", + "accessUrl": "ss://example", + "dataLimit": {"bytes": 1024}, + } + + +@pytest.fixture +def server_dict() -> dict[str, Any]: + return { + "name": "My Server", + "serverId": "srv-1", + "metricsEnabled": True, + "createdTimestampMs": 1000, + "portForNewAccessKeys": 23456, + "hostnameForAccessKeys": "example.com", + "accessKeyDataLimit": {"bytes": 2048}, + "version": "1.0.0", + } + + +@pytest.fixture +def access_keys_list(access_key_dict: dict[str, Any]) -> dict[str, Any]: + return {"accessKeys": [access_key_dict]} + + +@pytest.fixture +def server_metrics_dict() -> dict[str, Any]: + return {"bytesTransferredByUserId": {"user-1": 100, "user-2": 200}} + + +@pytest.fixture +def experimental_metrics_dict() -> dict[str, Any]: + return { + "server": { + "tunnelTime": {"seconds": 10}, + "dataTransferred": {"bytes": 1234}, + "bandwidth": { + "current": {"data": {"bytes": 1}, "timestamp": 1}, + "peak": {"data": {"bytes": 2}, "timestamp": 2}, + }, + "locations": [], + }, + "accessKeys": [ + { + "accessKeyId": "key-1", + "tunnelTime": {"seconds": 5}, + "dataTransferred": {"bytes": 100}, + "connection": { + "lastTrafficSeen": 1, + "peakDeviceCount": {"data": 1, "timestamp": 1}, + }, + } + ], + } + + +@pytest.fixture +def event_loop_policy() -> asyncio.AbstractEventLoopPolicy: + return asyncio.get_event_loop_policy() diff --git a/tests/test_api_mixins.py b/tests/test_api_mixins.py new file mode 100644 index 0000000..ee9c7b0 --- /dev/null +++ b/tests/test_api_mixins.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from typing import cast + +import pytest + +from pyoutlineapi.api_mixins import ( + AccessKeyMixin, + DataLimitMixin, + MetricsMixin, + ServerMixin, +) +from pyoutlineapi.common_types import JsonDict, Validators +from pyoutlineapi.models import ( + AccessKey, + AccessKeyList, + DataLimit, + ExperimentalMetrics, + MetricsStatusResponse, + Server, + ServerMetrics, +) + + +class FakeClient(ServerMixin, AccessKeyMixin, DataLimitMixin, MetricsMixin): + def __init__(self, data): + self._data = data + self._default_json_format = False + self._audit_logger_instance = None + + async def _request(self, method: str, endpoint: str, *, json=None, params=None): + key = (method, endpoint) + if key in self._data: + return self._data[key] + # Support access-key specific routes + if endpoint.startswith("access-keys/"): + if endpoint.endswith("/name") or endpoint.endswith("/data-limit"): + return {"success": True} + return self._data.get(("GET", "access-keys/{id}"), {}) + return {} + + +@pytest.mark.asyncio +async def test_access_keys_and_server(access_key_dict, access_keys_list, server_dict): + data = { + ("GET", "access-keys"): access_keys_list, + ("GET", "access-keys/{id}"): access_key_dict, + ("POST", "access-keys"): access_key_dict, + ("GET", "server"): server_dict, + ("PUT", "name"): {"success": True}, + ("PUT", "server/hostname-for-access-keys"): {"success": True}, + ("PUT", "server/port-for-new-access-keys"): {"success": True}, + } + client = FakeClient(data) + + server = cast(Server, await client.get_server_info()) + assert server.server_id == "srv-1" + + key = cast(AccessKey, await client.create_access_key(name="Alice", port=12345)) + assert key.id == "key-1" + + key2 = cast(AccessKey, await client.create_access_key_with_id("key-2", name="Bob")) + assert key2.id == "key-1" + + keys = cast(AccessKeyList, await client.get_access_keys()) + assert keys.count == 1 + + assert await client.rename_access_key("key-1", "New") is True + assert await client.rename_server("Server") is True + assert await client.set_hostname("example.com") is True + assert await client.set_default_port(23456) is True + assert await client.delete_access_key("key-1") is True + assert await client.set_access_key_data_limit("key-1", DataLimit(bytes=1)) is True + assert await client.remove_access_key_data_limit("key-1") is True + key_single = cast(AccessKey, await client.get_access_key("key-1")) + assert key_single.id == "key-1" + + server_json = cast(JsonDict, await client.get_server_info(as_json=True)) + assert server_json["serverId"] == "srv-1" + + # AuditableMixin fallback path (remove instance logger to force fallback) + del client.__dict__["_audit_logger_instance"] + assert client._audit_logger is not None + + +@pytest.mark.asyncio +async def test_set_hostname_validation_error(): + client = FakeClient({}) + with pytest.raises(ValueError, match=r".*"): + await client.set_hostname(" ") + + +@pytest.mark.asyncio +async def test_limits_and_metrics(server_metrics_dict, experimental_metrics_dict): + data = { + ("GET", "metrics/transfer"): server_metrics_dict, + ("GET", "metrics/enabled"): {"metricsEnabled": True}, + ("PUT", "metrics/enabled"): {"metricsEnabled": True}, + ("GET", "experimental/server/metrics"): experimental_metrics_dict, + } + client = FakeClient(data) + + limit = DataLimit(bytes=1024) + assert await client.set_global_data_limit(limit) is True + assert await client.remove_global_data_limit() is True + + status = cast(MetricsStatusResponse, await client.get_metrics_status()) + assert status.metrics_enabled is True + + status_json = cast(JsonDict, await client.get_metrics_status(as_json=True)) + assert status_json["metricsEnabled"] is True + + status_set = await client.set_metrics_status(True) + assert status_set is True + + metrics = cast(ServerMetrics, await client.get_transfer_metrics()) + assert metrics.total_bytes == 300 + + exp = cast(ExperimentalMetrics, await client.get_experimental_metrics("1h")) + assert exp.get_key_metric("key-1") is not None + + assert Validators.validate_since("1h") == "1h" diff --git a/tests/test_audit.py b/tests/test_audit.py new file mode 100644 index 0000000..2050d19 --- /dev/null +++ b/tests/test_audit.py @@ -0,0 +1,712 @@ +from __future__ import annotations + +import asyncio +import logging +from contextlib import suppress + +import pytest + +from pyoutlineapi.audit import ( + AuditContext, + DefaultAuditLogger, + NoOpAuditLogger, + _sanitize_details, + audited, + get_audit_logger, + get_or_create_audit_logger, + set_audit_logger, +) + +REDACTED_VALUE = "***REDACTED***" + + +async def _run_process_queue_flush_scenario( + monkeypatch: pytest.MonkeyPatch, + *, + batch_size: int, + batch_timeout: float, +) -> list[int]: + logger_instance = DefaultAuditLogger( + batch_size=batch_size, + batch_timeout=batch_timeout, + ) + flushed: list[int] = [] + + def fake_write_batch(self, batch): + flushed.append(len(batch)) + + monkeypatch.setattr(DefaultAuditLogger, "_write_batch", fake_write_batch) + await logger_instance._queue.put({"action": "a", "resource": "r"}) + + task = asyncio.create_task(logger_instance._process_queue()) + await asyncio.sleep(0.01) + logger_instance._shutdown_event.set() + task.cancel() + with suppress(asyncio.CancelledError): + await task + + return flushed + + +class DummyLogger: + def __init__(self) -> None: + self.logged: list[tuple[str, str]] = [] + self.alogged: list[tuple[str, str]] = [] + + def log_action(self, action: str, resource: str, **kwargs: object) -> None: + self.logged.append((action, resource)) + + async def alog_action( + self, + action: str, + resource: str, + **kwargs: object, + ) -> None: + self.alogged.append((action, resource)) + + async def shutdown(self) -> None: + return None + + +class DummyResult: + def __init__(self, value: str) -> None: + self.id = value + + +class SyncExample: + def __init__(self, logger: DummyLogger) -> None: + self._audit_logger_instance = logger + + @property + def _audit_logger(self) -> DummyLogger: + return self._audit_logger_instance + + @audited() + def do(self, name: str) -> DummyResult: + return DummyResult(name) + + @audited(log_success=False, log_failure=True) + def fail(self, name: str) -> DummyResult: + raise RuntimeError(f"bad {name}") + + +class AsyncExample: + def __init__(self, logger: DummyLogger) -> None: + self._audit_logger_instance = logger + + @property + def _audit_logger(self) -> DummyLogger: + return self._audit_logger_instance + + @audited() + async def do(self, name: str) -> DummyResult: + return DummyResult(name) + + @audited(log_success=False, log_failure=True) + async def fail(self, name: str) -> DummyResult: + raise RuntimeError(f"bad {name}") + + +@pytest.mark.asyncio +async def test_audited_async_logs_success(): + logger = DummyLogger() + obj = AsyncExample(logger) + result = await obj.do("res") + assert result.id == "res" + await asyncio.sleep(0) + assert ("do", "res") in logger.alogged + + +def test_audited_sync_logs_success(): + logger = DummyLogger() + obj = SyncExample(logger) + result = obj.do("res") + assert result.id == "res" + assert ("do", "res") in logger.logged + + +def test_audit_logger_context(): + logger = DummyLogger() + set_audit_logger(logger) + assert get_audit_logger() is logger + + +def test_sanitize_details_masks(): + details = {"password": "x", "nested": {"token": "y"}} + sanitized = _sanitize_details(details) + assert sanitized["password"] == REDACTED_VALUE + assert sanitized["nested"]["token"] == REDACTED_VALUE + + +def test_sanitize_details_no_change_returns_same(): + details = {"count": 1, "nested": {"ok": "x"}} + sanitized = _sanitize_details(details) + assert sanitized is details + + +def test_sanitize_details_empty(): + assert _sanitize_details({}) == {} + + +def test_audit_context_resource_extraction(): + def sample(key_id: str): + return key_id + + ctx = AuditContext.from_call( + sample, None, args=("id-1",), kwargs={}, result=None, exception=None + ) + assert ctx.resource == "id-1" + + +def test_audit_context_resource_patterns(): + def func(key_id: str): + return None + + class Obj: + id = "obj-1" + + ctx = AuditContext.from_call(func, None, (), {"key_id": "k1"}, result=None) + assert ctx.resource == "k1" + + ctx = AuditContext.from_call(func, None, (), {}, result={"id": "k2"}) + assert ctx.resource == "k2" + + ctx = AuditContext.from_call(func, None, (Obj(),), {}, result=Obj()) + assert ctx.resource == "obj-1" + + def server_action(): + return None + + ctx = AuditContext.from_call(server_action, None, (), {}, result=None) + assert ctx.resource == "server" + + +def test_audit_context_resource_from_result_dict(): + def func(): + return None + + resource = AuditContext._extract_resource( + func, args=(), kwargs={}, result={"id": "r1"}, success=True + ) + assert resource == "r1" + + +@pytest.mark.asyncio +async def test_audit_logger_queue_full_fallback( + monkeypatch: pytest.MonkeyPatch, +) -> None: + logger_instance = DefaultAuditLogger(queue_size=1) + entries: list[dict[str, object]] = [] + + def fake_write_log(self, entry): + entries.append(entry) + + monkeypatch.setattr(DefaultAuditLogger, "_write_log", fake_write_log) + + def fake_put_nowait(_entry): + raise asyncio.QueueFull + + monkeypatch.setattr(logger_instance._queue, "put_nowait", fake_put_nowait) + + await logger_instance.alog_action("act", "res") + assert entries + + +@pytest.mark.asyncio +async def test_audit_logger_process_queue_timeout_flush(monkeypatch): + flushed = await _run_process_queue_flush_scenario( + monkeypatch, + batch_size=10, + batch_timeout=0.001, + ) + assert flushed + + +@pytest.mark.asyncio +async def test_audit_logger_process_queue_cancel_flush( + monkeypatch: pytest.MonkeyPatch, +) -> None: + logger_instance = DefaultAuditLogger(batch_size=10, batch_timeout=1.0) + flushed: list[int] = [] + + def fake_write_batch(self, batch): + flushed.append(len(batch)) + + monkeypatch.setattr(DefaultAuditLogger, "_write_batch", fake_write_batch) + await logger_instance._queue.put({"action": "a", "resource": "r"}) + + task = asyncio.create_task(logger_instance._process_queue()) + await asyncio.sleep(0.01) + task.cancel() + with suppress(asyncio.CancelledError): + await task + + assert flushed + + +@pytest.mark.asyncio +async def test_audit_logger_shutdown_timeout_logs_warning(caplog): + logger_instance = DefaultAuditLogger() + + async def slow_join(): + await asyncio.sleep(0.01) + + monkeypatch = pytest.MonkeyPatch() + monkeypatch.setattr(logger_instance._queue, "join", slow_join) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.audit"): + await logger_instance.shutdown(timeout=0.0001) + monkeypatch.undo() + assert any("Queue did not drain" in r.message for r in caplog.records) + + +def test_audit_context_resource_unknown(): + def op(): + return None + + ctx = AuditContext.from_call( + op, None, args=(), kwargs={}, result=None, exception=None + ) + assert ctx.resource == "unknown" + + +def test_audit_context_details_extraction(): + def op(name: str, limit: int = 10): + return name + + ctx = AuditContext.from_call( + op, None, args=(), kwargs={"name": "x"}, result=None, exception=None + ) + assert ctx.details.get("name") == "x" + + +def test_audit_context_details_for_list(): + def op(items: list[int]): + return items + + ctx = AuditContext.from_call( + op, None, args=(), kwargs={"items": [1, 2, 3]}, result=None, exception=None + ) + assert ctx.details.get("items") == 3 + + +def test_audit_context_details_for_dict(): + def op(payload: dict[str, object]): + return payload + + ctx = AuditContext.from_call( + op, None, args=(), kwargs={"payload": {"x": 1}}, result=None, exception=None + ) + assert ctx.details["payload"]["x"] == 1 + + +def test_audit_context_details_model_dump(): + class DummyModel: + def model_dump(self, **kwargs): + return {"x": 1} + + def op(model: DummyModel): + return model + + ctx = AuditContext.from_call( + op, None, args=(), kwargs={"model": DummyModel()}, result=None, exception=None + ) + assert ctx.details["model"]["x"] == 1 + + +def test_default_audit_logger_is_singleton(): + logger1 = get_or_create_audit_logger() + logger2 = get_or_create_audit_logger() + assert logger1 is logger2 + + default_logger = DefaultAuditLogger() + default_logger.log_action("act", "res") + + +def test_get_or_create_audit_logger_cache(): + logger1 = get_or_create_audit_logger(instance_id=1) + logger2 = get_or_create_audit_logger(instance_id=1) + assert logger1 is logger2 + + +def test_get_or_create_audit_logger_context_override(): + logger = DummyLogger() + set_audit_logger(logger) + assert get_or_create_audit_logger(instance_id=2) is logger + + +@pytest.mark.asyncio +async def test_noop_audit_logger(): + logger = NoOpAuditLogger() + logger.log_action("a", "r") + await logger.alog_action("a", "r") + + +@pytest.mark.asyncio +async def test_default_audit_logger_async_queue(monkeypatch): + logger = DefaultAuditLogger(queue_size=10, batch_size=1, batch_timeout=0.01) + + # Ensure background task runs and processes the item + await logger.alog_action("act", "res", details={"password": "x"}) + await asyncio.sleep(0.02) + await logger.shutdown() + + +@pytest.mark.asyncio +async def test_audited_failure_paths(): + logger = DummyLogger() + obj = AsyncExample(logger) + with pytest.raises(RuntimeError): + await obj.fail(name="res") + await asyncio.sleep(0) + assert ("fail", "res") in logger.alogged + + sync_obj = SyncExample(logger) + with pytest.raises(RuntimeError): + sync_obj.fail(name="res") + assert ("fail", "res") in logger.logged + + +@pytest.mark.asyncio +async def test_default_audit_logger_queue_full( + monkeypatch: pytest.MonkeyPatch, +) -> None: + logger = DefaultAuditLogger(queue_size=1, batch_size=10, batch_timeout=1.0) + entries: list[dict[str, object]] = [] + + def capture(self, entry): + entries.append(entry) + + monkeypatch.setattr(DefaultAuditLogger, "_write_log", capture) + logger._queue.put_nowait({"action": "a", "resource": "r"}) + await logger.alog_action("act", "res") + assert any(e.get("action") == "act" for e in entries) + + +@pytest.mark.asyncio +async def test_default_audit_logger_fallback_on_shutdown( + monkeypatch: pytest.MonkeyPatch, +) -> None: + logger = DefaultAuditLogger(queue_size=1, batch_size=1, batch_timeout=0.01) + entries: list[dict[str, object]] = [] + + def capture(self, entry): + entries.append(entry) + + monkeypatch.setattr(DefaultAuditLogger, "_write_log", capture) + logger._shutdown_event.set() + await logger.alog_action("act", "res", user="u", correlation_id="cid") + assert any(e.get("user") == "u" for e in entries) + + +def test_format_message_with_user_and_correlation(): + entry = { + "action": "act", + "resource": "res", + "user": "u", + "correlation_id": "cid", + } + msg = DefaultAuditLogger._format_message(entry) + assert "[AUDIT]" in msg + assert "u" in msg + assert "cid" in msg + + +@pytest.mark.asyncio +async def test_default_audit_logger_ensure_task_running_fast_path(): + logger = DefaultAuditLogger(queue_size=10, batch_size=10, batch_timeout=0.01) + await logger._ensure_task_running() + task = logger._task + assert task is not None + await logger._ensure_task_running() + await logger.shutdown() + + +def test_audited_sync_without_logger(): + class NoLogger: + @audited() + def do(self): + return "ok" + + obj = NoLogger() + assert obj.do() == "ok" + + +@pytest.mark.asyncio +async def test_default_audit_logger_shutdown_debug(caplog): + logger = DefaultAuditLogger(queue_size=10, batch_size=1, batch_timeout=0.01) + await logger.alog_action("act", "res") + await asyncio.sleep(0.02) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.audit"): + await logger.shutdown() + + +@pytest.mark.asyncio +async def test_default_audit_logger_cancel_flush( + monkeypatch: pytest.MonkeyPatch, +) -> None: + logger = DefaultAuditLogger(queue_size=10, batch_size=10, batch_timeout=0.1) + entries: list[dict[str, object]] = [] + + def capture(self, entry): + entries.append(entry) + + monkeypatch.setattr(DefaultAuditLogger, "_write_log", capture) + await logger._ensure_task_running() + logger._queue.put_nowait(logger._build_entry("act", "res", None, None, None)) + await asyncio.sleep(0.02) + task = logger._task + assert task is not None + task.cancel() + with suppress(asyncio.CancelledError): + await task + assert entries + + +@pytest.mark.asyncio +async def test_default_audit_logger_shutdown_timeout(monkeypatch): + logger = DefaultAuditLogger(queue_size=10, batch_size=100, batch_timeout=1.0) + logger._queue.put_nowait({"action": "a", "resource": "r"}) + + async def fake_join(): + await asyncio.sleep(0) + raise asyncio.TimeoutError() + + monkeypatch.setattr(logger._queue, "join", fake_join) + await logger.shutdown(timeout=0.01) + + +@pytest.mark.asyncio +async def test_audit_logger_shutdown_already_set(): + logger = DefaultAuditLogger() + logger._shutdown_event.set() + await logger.shutdown() + + +@pytest.mark.asyncio +async def test_audit_logger_queue_full_logs_warning(caplog, monkeypatch): + logger_instance = DefaultAuditLogger(queue_size=1) + logging.getLogger("pyoutlineapi.audit").setLevel(logging.WARNING) + + def fake_put_nowait(_entry): + raise asyncio.QueueFull + + monkeypatch.setattr(logger_instance._queue, "put_nowait", fake_put_nowait) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.audit"): + await logger_instance.alog_action("act", "res") + assert any("Queue full" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_audit_logger_queue_full_no_warning(monkeypatch): + logger_instance = DefaultAuditLogger(queue_size=1) + logging.getLogger("pyoutlineapi.audit").setLevel(logging.ERROR) + + def fake_put_nowait(_entry): + raise asyncio.QueueFull + + monkeypatch.setattr(logger_instance._queue, "put_nowait", fake_put_nowait) + await logger_instance.alog_action("act", "res") + + +@pytest.mark.asyncio +async def test_audit_logger_ensure_task_running_restarts(): + logger_instance = DefaultAuditLogger() + await logger_instance._ensure_task_running() + task = logger_instance._task + assert task is not None + task.cancel() + with suppress(asyncio.CancelledError): + await task + await logger_instance._ensure_task_running() + await logger_instance.shutdown() + + +@pytest.mark.asyncio +async def test_audit_logger_process_queue_batch_size(monkeypatch): + flushed = await _run_process_queue_flush_scenario( + monkeypatch, + batch_size=1, + batch_timeout=1.0, + ) + assert flushed + + +@pytest.mark.asyncio +async def test_audit_logger_process_queue_timeout_flushes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + logger_instance = DefaultAuditLogger(batch_size=10, batch_timeout=0.001) + flushed: list[int] = [] + + def fake_write_batch(self, batch): + flushed.append(len(batch)) + + monkeypatch.setattr(DefaultAuditLogger, "_write_batch", fake_write_batch) + await logger_instance._queue.put({"action": "a", "resource": "r"}) + task = asyncio.create_task(logger_instance._process_queue()) + await asyncio.sleep(0.01) + logger_instance._shutdown_event.set() + await task + assert flushed + + +@pytest.mark.asyncio +async def test_audit_logger_process_queue_empty_flush( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + logger_instance = DefaultAuditLogger(batch_size=10, batch_timeout=1.0) + logging.getLogger("pyoutlineapi.audit").setLevel(logging.DEBUG) + flushed: list[int] = [] + + def fake_write_batch(self, batch): + flushed.append(len(batch)) + + monkeypatch.setattr(DefaultAuditLogger, "_write_batch", fake_write_batch) + await logger_instance._queue.put({"action": "a", "resource": "r"}) + + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.audit"): + task = asyncio.create_task(logger_instance._process_queue()) + await asyncio.sleep(0.01) + logger_instance._shutdown_event.set() + await task + assert flushed + + +@pytest.mark.asyncio +async def test_audit_logger_shutdown_timeout_cancels_task(monkeypatch): + logger_instance = DefaultAuditLogger() + logging.getLogger("pyoutlineapi.audit").setLevel(logging.WARNING) + + async def fake_join(): + raise asyncio.TimeoutError() + + monkeypatch.setattr(logger_instance._queue, "join", fake_join) + await logger_instance._ensure_task_running() + await logger_instance.shutdown(timeout=0.001) + + +@pytest.mark.asyncio +async def test_audit_logger_shutdown_timeout_no_warning(monkeypatch): + logger_instance = DefaultAuditLogger() + logging.getLogger("pyoutlineapi.audit").setLevel(logging.ERROR) + + async def fake_join(): + raise asyncio.TimeoutError() + + monkeypatch.setattr(logger_instance._queue, "join", fake_join) + await logger_instance.shutdown(timeout=0.001) + + +@pytest.mark.asyncio +async def test_audited_async_without_logger(): + class NoLogger: + @audited() + async def do(self): + return "ok" + + obj = NoLogger() + assert await obj.do() == "ok" + + +def test_audited_sync_no_success_logging(): + logger = DummyLogger() + + class Example: + def __init__(self, logger): + self._audit_logger_instance = logger + + @property + def _audit_logger(self): + return self._audit_logger_instance + + @audited(log_success=False) + def do(self): + return "ok" + + obj = Example(logger) + assert obj.do() == "ok" + assert logger.logged == [] + + +@pytest.mark.asyncio +async def test_audited_async_no_success_logging(): + logger = DummyLogger() + + class Example: + def __init__(self, logger): + self._audit_logger_instance = logger + + @property + def _audit_logger(self): + return self._audit_logger_instance + + @audited(log_success=False) + async def do(self): + return "ok" + + obj = Example(logger) + assert await obj.do() == "ok" + assert logger.alogged == [] + + +def test_sanitize_details_nested_redaction(): + details = {"token": "x", "nested": {"password": "y"}} + sanitized = _sanitize_details(details) + assert sanitized["token"] == REDACTED_VALUE + assert sanitized["nested"]["password"] == REDACTED_VALUE + + +def test_get_or_create_audit_logger_cache_paths(): + logger1 = get_or_create_audit_logger(instance_id=123) + logger2 = get_or_create_audit_logger(instance_id=123) + assert logger1 is logger2 + + +def test_get_or_create_audit_logger_creates_and_caches(): + logger_instance = get_or_create_audit_logger(instance_id=999) + assert get_or_create_audit_logger(instance_id=999) is logger_instance + + +@pytest.mark.asyncio +async def test_audited_async_failure_logs(): + logger = DummyLogger() + + class Example: + def __init__(self, logger): + self._audit_logger_instance = logger + + @property + def _audit_logger(self): + return self._audit_logger_instance + + @audited() + async def fail(self): + raise RuntimeError("boom") + + obj = Example(logger) + with pytest.raises(RuntimeError): + await obj.fail() + await asyncio.sleep(0) + assert logger.alogged + + +def test_audited_sync_failure_logs(): + logger = DummyLogger() + + class Example: + def __init__(self, logger): + self._audit_logger_instance = logger + + @property + def _audit_logger(self): + return self._audit_logger_instance + + @audited() + def fail(self): + raise RuntimeError("boom") + + obj = Example(logger) + with pytest.raises(RuntimeError): + obj.fail() + assert logger.logged diff --git a/tests/test_base_client.py b/tests/test_base_client.py new file mode 100644 index 0000000..eebd065 --- /dev/null +++ b/tests/test_base_client.py @@ -0,0 +1,922 @@ +from __future__ import annotations + +import asyncio +import json +import logging +from collections.abc import Callable +from types import SimpleNamespace +from typing import cast + +import aiohttp +import pytest +from pydantic import SecretStr + +from pyoutlineapi.base_client import ( + BaseHTTPClient, + NoOpMetrics, + RateLimiter, + RetryHelper, + SSLFingerprintValidator, + TokenBucketRateLimiter, +) +from pyoutlineapi.circuit_breaker import CircuitBreaker, CircuitConfig +from pyoutlineapi.common_types import ( + Constants, + JsonDict, + JsonList, + JsonValue, + SSRFProtection, +) +from pyoutlineapi.exceptions import APIError, CircuitOpenError + + +class _ChunkedContent: + def __init__(self, data: bytes) -> None: + self._data = data + + async def iter_chunked(self, size: int): + for i in range(0, len(self._data), size): + yield self._data[i : i + size] + + +class DummyResponse: + @staticmethod + def _resolve_headers(headers: dict[str, str] | None) -> dict[str, str]: + if headers is None: + return {"Content-Type": "application/json"} + return headers + + @staticmethod + def _resolve_reason(reason: str | None) -> str: + if reason is None: + return "OK" + return reason + + def __init__( + self, + status: int, + body: bytes, + *, + headers: dict[str, str] | None = None, + reason: str | None = None, + json_data: dict[str, object] | None = None, + json_error: Exception | None = None, + ) -> None: + self.status, self._body = status, body + self.headers = self._resolve_headers(headers) + self.reason = self._resolve_reason(reason) + self._json_data, self._json_error = json_data, json_error + self.content = _ChunkedContent(self._body) + + async def json(self) -> dict[str, object]: + if self._json_error is not None: + raise self._json_error + if self._json_data is not None: + return self._json_data + return {} + + +class DummyRequestContext: + def __init__(self, response: DummyResponse) -> None: + self._response = response + + async def __aenter__(self) -> DummyResponse: + return self._response + + async def __aexit__(self, exc_type, exc, tb) -> None: + return None + + +class DummySession: + def __init__(self, responder: Callable[..., DummyResponse]) -> None: + self._responder = responder + self.closed = False + + def request(self, *args: object, **kwargs: object) -> DummyRequestContext: + return DummyRequestContext(self._responder(*args, **kwargs)) + + async def close(self) -> None: + self.closed = True + + +@pytest.mark.asyncio +async def test_request_rechecks_ssrf(monkeypatch): + client = BaseHTTPClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + allow_private_networks=False, + resolve_dns_for_ssrf=True, + ) + + monkeypatch.setattr( + SSRFProtection, "is_blocked_hostname_uncached", lambda _host: True + ) + + with pytest.raises(ValueError, match=r".*"): + await client._request("GET", "server") + + +@pytest.mark.asyncio +async def test_request_ssrf_passes_and_returns(monkeypatch): + class DummyClient(BaseHTTPClient): + async def _ensure_session(self) -> None: + return None + + async def _make_request_inner( + self, + method: str, + endpoint: str, + *, + json: JsonDict | JsonList | None = None, + params: dict[str, str | int | float | bool] | None = None, + correlation_id: str, + ) -> dict[str, JsonValue]: + _ = (method, endpoint, json, params, correlation_id) + return {} + + client = DummyClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + allow_private_networks=False, + resolve_dns_for_ssrf=True, + ) + + monkeypatch.setattr( + SSRFProtection, "is_blocked_hostname_uncached", lambda _host: False + ) + + result = await client._request("GET", "server") + assert result == {} + + +class _TestClient(BaseHTTPClient): + async def _ensure_session(self) -> None: # override to prevent real session + return None + + +@pytest.mark.asyncio +async def test_build_url_and_parse_response(access_key_dict): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + assert client._build_url("/server") == "https://example.com/secret/server" + + body = json.dumps(access_key_dict).encode("utf-8") + response = DummyResponse(status=200, body=body) + + data = await client._parse_response_safe( + cast(aiohttp.ClientResponse, response), "/server" + ) + assert data["id"] == "key-1" + + +@pytest.mark.asyncio +async def test_parse_response_size_limit(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + big = b"a" * (Constants.MAX_RESPONSE_SIZE + 1) + response = DummyResponse(status=200, body=big) + + with pytest.raises(APIError): + await client._parse_response_safe( + cast(aiohttp.ClientResponse, response), "/server" + ) + + +@pytest.mark.asyncio +async def test_parse_response_content_length_header_limit(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + response = DummyResponse( + status=200, + body=b"{}", + headers={ + "Content-Type": "application/json", + "Content-Length": str(Constants.MAX_RESPONSE_SIZE + 1), + }, + ) + with pytest.raises(APIError): + await client._parse_response_safe( + cast(aiohttp.ClientResponse, response), "/server" + ) + + +@pytest.mark.asyncio +async def test_parse_response_content_length_invalid_and_list_json(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + response = DummyResponse( + status=200, + body=b"[]", + headers={"Content-Type": "text/plain", "Content-Length": "bad"}, + ) + data = await client._parse_response_safe( + cast(aiohttp.ClientResponse, response), "/server" + ) + assert data["success"] is True + + +@pytest.mark.asyncio +async def test_handle_error_json(): + response = DummyResponse( + status=500, + body=b"{}", + json_data={"message": "fail"}, + ) + with pytest.raises(APIError) as exc: + await BaseHTTPClient._handle_error( + cast(aiohttp.ClientResponse, response), "/bad" + ) + assert "fail" in str(exc.value) + + +@pytest.mark.asyncio +async def test_handle_error_non_json(): + response = DummyResponse( + status=400, + body=b"not-json", + json_error=ValueError("bad"), + reason="Bad Request", + ) + with pytest.raises(APIError) as exc: + await BaseHTTPClient._handle_error( + cast(aiohttp.ClientResponse, response), "/bad" + ) + assert "Bad Request" in str(exc.value) + + +@pytest.mark.asyncio +async def test_make_request_inner_success_and_204(monkeypatch): + def responder(method: str, url: str, **kwargs: object) -> DummyResponse: + if method == "GET": + return DummyResponse(status=200, body=b'{"success": true}') + return DummyResponse(status=204, body=b"") + + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + client._session = cast(aiohttp.ClientSession, DummySession(responder)) + + result = await client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + assert result["success"] is True + + result = await client._make_request_inner( + "DELETE", "server", json=None, params=None, correlation_id="cid" + ) + assert result["success"] is True + + +@pytest.mark.asyncio +async def test_make_request_inner_debug_logging(caplog): + def responder(method: str, url: str, **kwargs: object) -> DummyResponse: + return DummyResponse(status=200, body=b'{"success": true}') + + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + client._enable_logging = True + client._session = cast(aiohttp.ClientSession, DummySession(responder)) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.base_client"): + await client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + assert any("GET" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_parse_response_invalid_json_returns_success(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + response = DummyResponse(status=200, body=b"{invalid") + data = await client._parse_response_safe( + cast(aiohttp.ClientResponse, response), "/server" + ) + assert data["success"] is True + + +@pytest.mark.asyncio +async def test_parse_response_invalid_json_error_status(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + response = DummyResponse(status=500, body=b"{invalid") + with pytest.raises(APIError): + await client._parse_response_safe( + cast(aiohttp.ClientResponse, response), "/server" + ) + + +@pytest.mark.asyncio +async def test_parse_response_non_dict_error_status(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + response = DummyResponse(status=400, body=b"[]") + with pytest.raises(APIError): + await client._parse_response_safe( + cast(aiohttp.ClientResponse, response), "/server" + ) + + +@pytest.mark.asyncio +async def test_shutdown_closes_session(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + def responder(method: str, url: str, **kwargs: object) -> DummyResponse: + return DummyResponse(204, b"") + + client._session = cast(aiohttp.ClientSession, DummySession(responder)) + await client.shutdown() + assert client._session is None + + +@pytest.mark.asyncio +async def test_shutdown_cancels_active_requests(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + async def sleeper() -> dict[str, JsonValue]: + await asyncio.sleep(1) + return {"ok": True} + + task = asyncio.create_task(sleeper()) + async with client._active_requests_lock: + client._active_requests.add(task) + + class DummySessionClose: + closed = False + + async def close(self) -> None: + self.closed = True + + client._session = cast(aiohttp.ClientSession, DummySessionClose()) + await client.shutdown(timeout=0.01) + assert task.cancelled() or task.done() + + +@pytest.mark.asyncio +async def test_rate_limiter_and_noop_metrics(monkeypatch): + limiter = TokenBucketRateLimiter(rate=1.0, capacity=1) + + async def fake_sleep(_): + return None + + monkeypatch.setattr("asyncio.sleep", fake_sleep) + + await limiter.acquire() + await limiter.acquire() + + metrics = NoOpMetrics() + metrics.increment("a") + metrics.timing("b", 1.0) + metrics.gauge("c", 2.0) + + +def test_token_bucket_invalid_params(): + with pytest.raises(ValueError, match=r".*"): + TokenBucketRateLimiter(rate=0.0, capacity=1) + with pytest.raises(ValueError, match=r".*"): + TokenBucketRateLimiter(rate=1.0, capacity=0) + + +@pytest.mark.asyncio +async def test_base_client_context_manager(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + async with client: + assert client is not None + + +def test_get_circuit_metrics_none(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + assert client.get_circuit_metrics() is None + + +def test_init_circuit_breaker_adjusts_timeout(): + config = CircuitConfig( + call_timeout=0.1, recovery_timeout=1.0, failure_threshold=1, success_threshold=1 + ) + client = BaseHTTPClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=5, + retry_attempts=2, + max_connections=1, + rate_limit=10, + circuit_config=config, + ) + assert client.get_circuit_metrics() is not None + + +@pytest.mark.asyncio +async def test_rate_limit_properties(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + assert client.rate_limit == 10 + assert client.active_requests == 0 + assert client.available_slots == 10 + stats = client.get_rate_limiter_stats() + assert "limit" in stats + await client.set_rate_limit(5) + assert client.rate_limit == 5 + + +@pytest.mark.asyncio +async def test_make_request_inner_connection_error(): + class ErrorSession: + def request( + self, + *args: object, + **kwargs: object, + ) -> DummyRequestContext: + raise aiohttp.ClientConnectionError("boom") + + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + client._session = cast(aiohttp.ClientSession, ErrorSession()) + with pytest.raises(APIError): + await client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + + +@pytest.mark.asyncio +async def test_request_with_circuit_open(monkeypatch): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + class DummyBreaker: + async def call(self, *args: object, **kwargs: object) -> object: + raise CircuitOpenError("open") + + client._circuit_breaker = cast(CircuitBreaker, DummyBreaker()) + with pytest.raises(CircuitOpenError): + await client._request("GET", "server") + + +@pytest.mark.asyncio +async def test_request_with_circuit_open_logs(caplog): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + class DummyBreaker: + async def call(self, *args: object, **kwargs: object) -> object: + raise CircuitOpenError("open") + + client._circuit_breaker = cast(CircuitBreaker, DummyBreaker()) + with ( + caplog.at_level( + logging.ERROR, + logger="pyoutlineapi.base_client", + ), + pytest.raises(CircuitOpenError), + ): + await client._request("GET", "server") + + +@pytest.mark.asyncio +async def test_request_success_path(monkeypatch): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + async def fake_inner(*args: object, **kwargs: object) -> dict[str, object]: + return {"ok": True} + + monkeypatch.setattr(client, "_make_request_inner", fake_inner) + data = await client._request("GET", "server") + assert data["ok"] is True + + +@pytest.mark.asyncio +async def test_make_request_inner_http_error(): + def responder(method: str, url: str, **kwargs: object) -> DummyResponse: + return DummyResponse( + status=500, + body=b'{"message": "fail"}', + json_data={"message": "fail"}, + ) + + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + client._session = cast(aiohttp.ClientSession, DummySession(responder)) + with pytest.raises(APIError): + await client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + + +@pytest.mark.asyncio +async def test_make_request_inner_no_session(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + client._session = None + with pytest.raises(APIError): + await client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + + +@pytest.mark.asyncio +async def test_make_request_inner_timeout_error(monkeypatch): + class TimeoutSession: + def request( + self, + *args: object, + **kwargs: object, + ) -> DummyRequestContext: + raise asyncio.TimeoutError() + + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + client._session = cast(aiohttp.ClientSession, TimeoutSession()) + with pytest.raises(APIError): + await client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + + +@pytest.mark.asyncio +async def test_make_request_inner_client_error(): + class ClientErrorSession: + def request( + self, + *args: object, + **kwargs: object, + ) -> DummyRequestContext: + raise aiohttp.ClientError("oops") + + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + client._session = cast(aiohttp.ClientSession, ClientErrorSession()) + with pytest.raises(APIError): + await client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + + +@pytest.mark.asyncio +async def test_ensure_session_uses_aiohttp(monkeypatch, caplog): + created = {"session": False} + + class DummyTraceConfig: + def __init__(self) -> None: + self.on_connection_create_end: list[object] = [] + self.on_connection_reuseconn: list[object] = [] + + class DummyConnector: + def __init__(self, **kwargs: object) -> None: + self.kwargs = kwargs + + class DummySession: + def __init__(self, **kwargs: object) -> None: + created["session"] = True + self.closed = False + + async def close(self) -> None: + self.closed = True + + monkeypatch.setattr(aiohttp, "TraceConfig", DummyTraceConfig) + monkeypatch.setattr(aiohttp, "TCPConnector", DummyConnector) + monkeypatch.setattr(aiohttp, "ClientSession", DummySession) + + client = BaseHTTPClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.base_client"): + await client._ensure_session() + assert created["session"] is True + + +@pytest.mark.asyncio +async def test_ensure_session_fast_path(): + client = BaseHTTPClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + + class DummySessionFast: + closed = False + + client._session = cast(aiohttp.ClientSession, DummySessionFast()) + await client._ensure_session() + + +@pytest.mark.asyncio +async def test_rate_limiter_context_and_set_limit(): + limiter = RateLimiter(limit=1) + async with limiter: + assert limiter.active == 1 + assert limiter.active == 0 + + await limiter.set_limit(2) + assert limiter.limit == 2 + await limiter.set_limit(2) + with pytest.raises(ValueError, match=r".*"): + await limiter.set_limit(0) + + +def test_rate_limiter_available_edge_cases(): + limiter = RateLimiter(limit=1) + + class DummySemaphore: + _value = "bad" + + limiter._semaphore = cast(asyncio.Semaphore, DummySemaphore()) + assert limiter.available == 0 + + class BrokenSemaphore: + def __getattr__(self, _name: str) -> object: + raise TypeError("boom") + + limiter._semaphore = cast(asyncio.Semaphore, BrokenSemaphore()) + assert limiter.available == 0 + + +def test_rate_limiter_invalid_limit(): + with pytest.raises(ValueError, match=r".*"): + RateLimiter(limit=0) + + +def test_rate_limiter_available_logs_warning(caplog): + limiter = RateLimiter(limit=1) + + class BrokenSemaphore: + def __getattr__(self, _name: str) -> object: + raise TypeError("missing") + + limiter._semaphore = cast(asyncio.Semaphore, BrokenSemaphore()) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.base_client"): + assert limiter.available == 0 + assert any("Cannot access semaphore value" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_rate_limiter_set_limit_logs(caplog): + limiter = RateLimiter(limit=1) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.base_client"): + await limiter.set_limit(2) + assert any("Rate limit changed" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_retry_helper_success(monkeypatch): + helper = RetryHelper() + calls = {"count": 0} + + async def func() -> dict[str, JsonValue]: + calls["count"] += 1 + if calls["count"] < 2: + raise APIError("fail") + return {"ok": True} + + async def fake_sleep(_): + return None + + monkeypatch.setattr("asyncio.sleep", fake_sleep) + result = await helper.execute_with_retry(func, "/endpoint", 2, NoOpMetrics()) + assert result["ok"] is True + + +@pytest.mark.asyncio +async def test_retry_helper_non_retryable(): + helper = RetryHelper() + + async def func() -> dict[str, JsonValue]: + raise APIError("fail", status_code=400) + + with pytest.raises(APIError): + await helper.execute_with_retry(func, "/endpoint", 2, NoOpMetrics()) + + +def test_ssl_fingerprint_validator_verify(): + cert_bytes = b"cert" + import hashlib + + expected = hashlib.sha256(cert_bytes).hexdigest() + validator = SSLFingerprintValidator(SecretStr(expected)) + validator._verify_cert_fingerprint(cert_bytes) + with pytest.raises(ValueError, match=r".*"): + validator._verify_cert_fingerprint(b"other") + + validator.__exit__(None, None, None) + with pytest.raises(RuntimeError): + _ = validator.ssl_context + + +@pytest.mark.asyncio +async def test_ssl_fingerprint_validator_verify_connection(): + cert_bytes = b"cert" + import hashlib + + expected = hashlib.sha256(cert_bytes).hexdigest() + validator = SSLFingerprintValidator(SecretStr(expected)) + + class DummySSL: + def getpeercert(self, *, binary_form: bool = False) -> bytes | None: + return cert_bytes if binary_form else None + + class DummyTransport: + def get_extra_info(self, name: str) -> object: + if name == "ssl_object": + return DummySSL() + return None + + class DummyParams: + transport = DummyTransport() + + await validator.verify_connection( + cast(aiohttp.ClientSession, None), + SimpleNamespace(), + cast(aiohttp.TraceConnectionCreateEndParams, DummyParams()), + ) + + +@pytest.mark.asyncio +async def test_ssl_fingerprint_validator_verify_connection_no_transport(): + validator = SSLFingerprintValidator(SecretStr("a" * 64)) + + class DummyParams: + transport = None + + await validator.verify_connection( + cast(aiohttp.ClientSession, None), + SimpleNamespace(), + cast(aiohttp.TraceConnectionCreateEndParams, DummyParams()), + ) + + +def test_validate_numeric_params_errors(): + with pytest.raises(ValueError, match=r".*"): + BaseHTTPClient._validate_numeric_params(0, 0, 1) + with pytest.raises(ValueError, match=r".*"): + BaseHTTPClient._validate_numeric_params(1, -1, 1) + with pytest.raises(ValueError, match=r".*"): + BaseHTTPClient._validate_numeric_params(1, 0, 0) + + +def test_api_url_and_reset_circuit_breaker(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + assert "https://example.com" in client.api_url + assert client.circuit_state is None + + +@pytest.mark.asyncio +async def test_reset_circuit_breaker_noop(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=10, + ) + assert await client.reset_circuit_breaker() is False diff --git a/tests/test_base_client_extra.py b/tests/test_base_client_extra.py new file mode 100644 index 0000000..1ebae9b --- /dev/null +++ b/tests/test_base_client_extra.py @@ -0,0 +1,340 @@ +from __future__ import annotations + +import asyncio +import logging +from types import SimpleNamespace +from typing import cast + +import aiohttp +import pytest +from pydantic import SecretStr + +from pyoutlineapi.base_client import ( + BaseHTTPClient, + NoOpMetrics, + RateLimiter, + RetryHelper, + SSLFingerprintValidator, +) +from pyoutlineapi.circuit_breaker import CircuitBreaker +from pyoutlineapi.common_types import JsonValue +from pyoutlineapi.exceptions import APIError, CircuitOpenError + + +class DummyResponse: + def __init__(self, status: int, reason: str = "Bad") -> None: + self.status = status + self.reason = reason + + async def json(self) -> dict[str, object]: + raise TypeError("bad") + + +class _ChunkedContent: + def __init__(self, data: bytes) -> None: + self._data = data + + async def iter_chunked(self, size: int): + for i in range(0, len(self._data), size): + yield self._data[i : i + size] + + +class SimpleResponse: + def __init__(self, status: int = 200, body: bytes = b"{}") -> None: + self.status = status + self.reason = "OK" + self.headers = {"Content-Type": "application/json"} + self._body = body + self.content = _ChunkedContent(body) + + async def json(self) -> dict[str, object]: + return {} + + +class _TestClient(BaseHTTPClient): + async def _ensure_session(self) -> None: # override to prevent real session + return None + + +class DummySession: + def __init__(self, response: object) -> None: + self._response = response + self.closed = False + + def request(self, *args: object, **kwargs: object) -> object: + return self._response + + async def close(self) -> None: + self.closed = True + + +def test_rate_limiter_available_attribute_error(caplog): + limiter = RateLimiter(limit=1) + logging.getLogger("pyoutlineapi.base_client").setLevel(logging.WARNING) + + class BrokenSemaphore: + def __getattribute__(self, _name: str) -> object: + raise AttributeError("missing") + + limiter._semaphore = cast(asyncio.Semaphore, BrokenSemaphore()) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.base_client"): + assert limiter.available == 0 + + +def test_ssl_fingerprint_validator_expected_secret_missing(): + validator = SSLFingerprintValidator(SecretStr("a" * 64)) + validator.__exit__(None, None, None) + with pytest.raises(RuntimeError): + validator._verify_cert_fingerprint(b"cert") + + +@pytest.mark.asyncio +async def test_ssl_fingerprint_validator_verify_connection_no_ssl_object(): + validator = SSLFingerprintValidator(SecretStr("a" * 64)) + + class DummyTransport: + def get_extra_info(self, name: str) -> object: + return None + + class DummyParams: + transport = DummyTransport() + + await validator.verify_connection( + cast(aiohttp.ClientSession, None), + SimpleNamespace(), + cast(aiohttp.TraceConnectionCreateEndParams, DummyParams()), + ) + + +@pytest.mark.asyncio +async def test_ssl_fingerprint_validator_verify_connection_empty_cert(): + validator = SSLFingerprintValidator(SecretStr("a" * 64)) + + class DummySSL: + def getpeercert(self, *, binary_form: bool = False) -> bytes | None: + return b"" + + class DummyTransport: + def get_extra_info(self, name: str) -> object: + if name == "ssl_object": + return DummySSL() + return None + + class DummyParams: + transport = DummyTransport() + + await validator.verify_connection( + cast(aiohttp.ClientSession, None), + SimpleNamespace(), + cast(aiohttp.TraceConnectionCreateEndParams, DummyParams()), + ) + + +@pytest.mark.asyncio +async def test_handle_error_type_error(): + with pytest.raises(APIError): + await BaseHTTPClient._handle_error( + cast(aiohttp.ClientResponse, DummyResponse(500)), + "/bad", + ) + + +@pytest.mark.asyncio +async def test_shutdown_already_in_progress(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=1, + ) + client._shutdown_event.set() + await client.shutdown() + + +@pytest.mark.asyncio +async def test_shutdown_logs_and_cancels(caplog): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=1, + ) + + async def sleeper() -> dict[str, JsonValue]: + await asyncio.sleep(1) + return {"ok": True} + + task = asyncio.create_task(sleeper()) + async with client._active_requests_lock: + client._active_requests.add(task) + + class DummySession: + closed = False + + async def close(self) -> None: + self.closed = True + + client._session = cast(aiohttp.ClientSession, DummySession()) + + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.base_client"): + await client.shutdown(timeout=0.01) + assert any("Shutdown timeout" in r.message for r in caplog.records) + assert any("HTTP client shutdown complete" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_request_circuit_open_logs_error(caplog): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=1, + ) + + class DummyBreaker: + async def call(self, *args: object, **kwargs: object) -> object: + raise CircuitOpenError("open") + + client._circuit_breaker = cast(CircuitBreaker, DummyBreaker()) + logging.getLogger("pyoutlineapi.base_client").setLevel(logging.ERROR) + with ( + caplog.at_level( + logging.ERROR, + logger="pyoutlineapi.base_client", + ), + pytest.raises(CircuitOpenError), + ): + await client._request("GET", "server") + + +@pytest.mark.asyncio +async def test_retry_helper_logs_warning(caplog): + helper = RetryHelper() + logging.getLogger("pyoutlineapi.base_client").setLevel(logging.WARNING) + + async def boom() -> dict[str, JsonValue]: + raise APIError("fail", status_code=500) + + with ( + caplog.at_level( + logging.WARNING, + logger="pyoutlineapi.base_client", + ), + pytest.raises(APIError), + ): + await helper.execute_with_retry(boom, "/endpoint", 0, NoOpMetrics()) + assert any("Request to" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_retry_helper_no_warning_when_logger_disabled(): + helper = RetryHelper() + logging.getLogger("pyoutlineapi.base_client").setLevel(logging.ERROR) + + async def boom() -> dict[str, JsonValue]: + raise APIError("fail", status_code=500) + + with pytest.raises(APIError): + await helper.execute_with_retry(boom, "/endpoint", 0, NoOpMetrics()) + + +@pytest.mark.asyncio +async def test_ensure_session_double_check_returns(monkeypatch): + client = BaseHTTPClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=1, + ) + + class DummySessionFast: + closed = False + + class DummyLock: + async def __aenter__(self) -> None: + client._session = cast(aiohttp.ClientSession, DummySessionFast()) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: object | None, + ) -> None: + return None + + client._session = None + client._session_lock = cast(asyncio.Lock, DummyLock()) + await client._ensure_session() + assert client._session is not None + + +@pytest.mark.asyncio +async def test_active_requests_tracking(): + entered = asyncio.Event() + continue_event = asyncio.Event() + + class DummyContext: + async def __aenter__(self) -> SimpleResponse: + entered.set() + await continue_event.wait() + return SimpleResponse() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: object | None, + ) -> None: + return None + + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=1, + ) + client._session = cast(aiohttp.ClientSession, DummySession(DummyContext())) + + task = asyncio.create_task( + client._make_request_inner( + "GET", "server", json=None, params=None, correlation_id="cid" + ) + ) + await entered.wait() + assert client.active_requests == 1 + continue_event.set() + await task + assert client.active_requests == 0 + + +@pytest.mark.asyncio +async def test_reset_circuit_breaker_configured(): + client = _TestClient( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + timeout=1, + retry_attempts=0, + max_connections=1, + rate_limit=1, + ) + + class DummyBreaker: + async def reset(self) -> None: + return None + + @property + def metrics(self) -> object: + return None + + client._circuit_breaker = cast(CircuitBreaker, DummyBreaker()) + assert await client.reset_circuit_breaker() is True diff --git a/tests/test_batch_operations.py b/tests/test_batch_operations.py new file mode 100644 index 0000000..363052b --- /dev/null +++ b/tests/test_batch_operations.py @@ -0,0 +1,319 @@ +from __future__ import annotations + +import asyncio +import logging +from typing import cast + +import pytest + +from pyoutlineapi.batch_operations import ( + BatchOperations, + BatchProcessor, + BatchResult, + ValidationHelper, +) +from pyoutlineapi.client import AsyncOutlineClient +from pyoutlineapi.models import AccessKey, DataLimit + +PLACEHOLDER_CREDENTIAL = "pwd" + + +class DummyClient: + async def create_access_key(self, **kwargs: object) -> AccessKey: + name = cast(str | None, kwargs.get("name")) + return AccessKey( + id="key-1", + name=name, + password=PLACEHOLDER_CREDENTIAL, + port=12345, + method="aes-256-gcm", + accessUrl="ss://example", + dataLimit=None, + ) + + async def delete_access_key(self, key_id: str) -> bool: + return True + + async def rename_access_key(self, key_id: str, name: str) -> bool: + return True + + async def set_access_key_data_limit(self, key_id: str, limit: DataLimit) -> bool: + return True + + async def get_access_key(self, key_id: str) -> AccessKey: + return AccessKey( + id=key_id, + name="Name", + password=PLACEHOLDER_CREDENTIAL, + port=12345, + method="aes-256-gcm", + accessUrl="ss://example", + dataLimit=None, + ) + + +def _as_client(client: object) -> AsyncOutlineClient: + return cast(AsyncOutlineClient, client) + + +@pytest.mark.asyncio +async def test_batch_processor_success_and_fail() -> None: + processor: BatchProcessor[int, int] = BatchProcessor(max_concurrent=2) + + async def double(x: int) -> int: + return x * 2 + + results = await processor.process([1, 2, 3], double) + assert results == [2, 4, 6] + + async def fail(x: int) -> int: + raise RuntimeError(f"bad {x}") + + results = await processor.process([1], fail, fail_fast=False) + assert isinstance(results[0], Exception) + + assert await processor.process([], double) == [] + + +@pytest.mark.asyncio +async def test_batch_processor_cancels_pending_tasks() -> None: + processor: BatchProcessor[int, int] = BatchProcessor(max_concurrent=2) + started = asyncio.Event() + cancelled = asyncio.Event() + + async def worker(value: int) -> int: + if value == 1: + started.set() + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + cancelled.set() + raise + return value + await started.wait() + raise RuntimeError("boom") + + with pytest.raises(RuntimeError): + await processor.process([1, 2], worker, fail_fast=True) + + await asyncio.wait_for(cancelled.wait(), timeout=0.2) + + +def test_validation_helper_config(): + helper = ValidationHelper() + config = {"name": " test ", "port": 12345} + validated = helper.validate_config_dict(config, 0, fail_fast=True) + assert validated is not None + assert validated["name"] == "test" + assert helper.validate_config_dict("bad", 0, fail_fast=False) is None + with pytest.raises(ValueError, match=r".*"): + helper.validate_config_dict("bad", 0, fail_fast=True) + assert helper.validate_config_dict({"name": " "}, 0, fail_fast=False) is None + with pytest.raises(ValueError, match=r".*"): + helper.validate_config_dict({"name": " "}, 0, fail_fast=True) + validated = helper.validate_config_dict({"port": 12345}, 0, fail_fast=True) + assert validated is not None + + +def test_validation_helper_tuple_pair(): + helper = ValidationHelper() + assert helper.validate_tuple_pair(("a", "b"), 0, (str, str), False) == ("a", "b") + assert helper.validate_tuple_pair(("a", 1), 0, (str, str), False) is None + with pytest.raises(ValueError, match=r".*"): + helper.validate_tuple_pair(("a",), 0, (str, str), True) + with pytest.raises(ValueError, match=r".*"): + helper.validate_tuple_pair(("a", 1), 0, (str, str), True) + + +def test_validation_helper_key_id(): + helper = ValidationHelper() + assert helper.validate_key_id("key-1", 0, False) == "key-1" + assert helper.validate_key_id(123, 0, False) is None + with pytest.raises(ValueError, match=r".*"): + helper.validate_key_id(123, 0, True) + assert helper.validate_key_id("bad id", 0, False) is None + with pytest.raises(ValueError, match=r".*"): + helper.validate_key_id("bad id", 0, True) + + +def test_batch_result_properties() -> None: + result = BatchResult( + total=2, + successful=1, + failed=1, + results=(1, Exception("x")), + errors=("x",), + validation_errors=(), + ) + assert result.success_rate == 0.5 + assert result.has_errors is True + assert result.has_validation_errors is False + assert result.get_successful_results() == [1] + assert len(result.get_failures()) == 1 + data = result.to_dict() + assert data["total"] == 2 + empty: BatchResult[int] = BatchResult( + total=0, + successful=0, + failed=0, + results=(), + errors=(), + validation_errors=(), + ) + assert empty.success_rate == 1.0 + + +@pytest.mark.asyncio +async def test_batch_operations_create_and_fetch(): + ops = BatchOperations(_as_client(DummyClient())) + result = await ops.create_multiple_keys([{"name": "Alice"}], fail_fast=False) + assert result.total == 1 + assert result.successful == 1 + + fetched = await ops.fetch_multiple_keys(["key-1"], fail_fast=False) + assert fetched.total == 1 + assert fetched.successful == 1 + + empty = await ops.fetch_multiple_keys([]) + assert empty.total == 0 + empty_create = await ops.create_multiple_keys([]) + assert empty_create.total == 0 + + +@pytest.mark.asyncio +async def test_batch_operations_other_actions(): + ops = BatchOperations(_as_client(DummyClient())) + + delete_result = await ops.delete_multiple_keys(["key-1"], fail_fast=False) + assert delete_result.successful == 1 + + rename_result = await ops.rename_multiple_keys([("key-1", "New")]) + assert rename_result.successful == 1 + + limit_result = await ops.set_multiple_data_limits([("key-1", 100)]) + assert limit_result.successful == 1 + + empty = await ops.delete_multiple_keys([]) + assert empty.total == 0 + empty_rename = await ops.rename_multiple_keys([]) + assert empty_rename.total == 0 + empty_limits = await ops.set_multiple_data_limits([]) + assert empty_limits.total == 0 + + +@pytest.mark.asyncio +async def test_batch_fail_fast_and_custom_ops() -> None: + ops = BatchOperations(_as_client(DummyClient()), max_concurrent=1) + + async def bad(_: int) -> int: + raise RuntimeError("fail") + + processor: BatchProcessor[int, int] = BatchProcessor(max_concurrent=1) + with pytest.raises(RuntimeError): + await processor.process([1], bad, fail_fast=True) + + async def ok(): + return 1 + + custom = await ops.execute_custom_operations([ok]) + assert custom.successful == 1 + + async def fail_op(): + raise RuntimeError("boom") + + result = await ops.execute_custom_operations([fail_op], fail_fast=False) + assert result.failed == 1 + + +@pytest.mark.asyncio +async def test_batch_operations_validation_errors(): + ops = BatchOperations(_as_client(DummyClient())) + create_result = await ops.create_multiple_keys([{"name": " "}], fail_fast=False) + assert create_result.failed == 1 + assert create_result.has_validation_errors is True + + rename_result = await ops.rename_multiple_keys([("bad id", "")], fail_fast=False) + assert rename_result.has_errors is True + + limit_result = await ops.set_multiple_data_limits([("bad", -1)], fail_fast=False) + assert limit_result.failed >= 1 + + bad_rename = await ops.rename_multiple_keys( + cast(list[tuple[str, str]], [("bad",)]), + fail_fast=False, + ) + assert bad_rename.failed >= 1 + + bad_limits = await ops.set_multiple_data_limits( + cast(list[tuple[str, int]], [("key-1", "bad")]), + fail_fast=False, + ) + assert bad_limits.failed >= 1 + with pytest.raises(ValueError, match=r".*"): + await ops.rename_multiple_keys([("bad id", "")], fail_fast=True) + with pytest.raises(ValueError, match=r".*"): + await ops.set_multiple_data_limits([("bad", -1)], fail_fast=True) + + +@pytest.mark.asyncio +async def test_batch_operations_invalid_tuple_types(monkeypatch): + ops = BatchOperations(_as_client(DummyClient())) + + def bad_validate(*_args: object, **_kwargs: object) -> tuple[int, str]: + return (123, "name") + + monkeypatch.setattr( + ValidationHelper, "validate_tuple_pair", staticmethod(bad_validate) + ) + result = await ops.rename_multiple_keys([("key-1", "new")], fail_fast=False) + assert result.validation_errors + + def bad_validate_limits(*_args: object, **_kwargs: object) -> tuple[str, str]: + return ("key-1", "bad") + + monkeypatch.setattr( + ValidationHelper, "validate_tuple_pair", staticmethod(bad_validate_limits) + ) + result = await ops.set_multiple_data_limits([("key-1", 1)], fail_fast=False) + assert result.validation_errors + + +@pytest.mark.asyncio +async def test_batch_concurrency_and_custom_ops_empty(): + ops = BatchOperations(_as_client(DummyClient())) + await ops.set_concurrency(2) + empty = await ops.execute_custom_operations([]) + assert empty.total == 0 + + +@pytest.mark.asyncio +async def test_batch_operations_invalid_concurrency(): + with pytest.raises(ValueError, match=r".*"): + BatchOperations(_as_client(DummyClient()), max_concurrent=0) + + +def test_batch_processor_invalid_concurrency(): + with pytest.raises(ValueError, match=r".*"): + BatchProcessor(max_concurrent=0) + + +@pytest.mark.asyncio +async def test_batch_processor_set_concurrency_logs( + caplog: pytest.LogCaptureFixture, +) -> None: + processor: BatchProcessor[int, int] = BatchProcessor(max_concurrent=1) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.batch_operations"): + await processor.set_concurrency(2) + assert any("concurrency changed" in r.message for r in caplog.records) + await processor.set_concurrency(2) + with pytest.raises(ValueError, match=r".*"): + await processor.set_concurrency(0) + + +@pytest.mark.asyncio +async def test_batch_operations_invalid_ids(): + ops = BatchOperations(_as_client(DummyClient())) + delete_result = await ops.delete_multiple_keys(["bad id"], fail_fast=False) + assert delete_result.failed >= 1 + fetch_result = await ops.fetch_multiple_keys(["bad id"], fail_fast=False) + assert fetch_result.failed >= 1 diff --git a/tests/test_circuit_breaker.py b/tests/test_circuit_breaker.py new file mode 100644 index 0000000..254174b --- /dev/null +++ b/tests/test_circuit_breaker.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +import asyncio +import logging + +import pytest + +from pyoutlineapi.circuit_breaker import CircuitBreaker, CircuitConfig, CircuitState +from pyoutlineapi.exceptions import CircuitOpenError, OutlineTimeoutError + + +@pytest.mark.asyncio +async def test_circuit_breaker_transitions(monkeypatch): + config = CircuitConfig( + failure_threshold=1, + recovery_timeout=1.0, + success_threshold=1, + call_timeout=0.2, + ) + breaker = CircuitBreaker("test", config) + + async def fail(): + raise ValueError("boom") + + async def ok(): + return "ok" + + with pytest.raises(ValueError, match=r".*"): + await breaker.call(fail) + + with pytest.raises(CircuitOpenError): + await breaker.call(ok) + + # Simulate recovery timeout elapsed + t = 100.0 + monkeypatch.setattr("pyoutlineapi.circuit_breaker.time.monotonic", lambda: t) + breaker._last_failure_time = t - 2.0 + + result = await breaker.call(ok) + assert result == "ok" + assert breaker.state in (CircuitState.HALF_OPEN, CircuitState.CLOSED) + + +def test_circuit_metrics_snapshot(): + breaker = CircuitBreaker("name") + snapshot = breaker.get_metrics_snapshot() + assert "success_rate" in snapshot + + +def test_circuit_config_validation(): + with pytest.raises(ValueError, match=r".*"): + CircuitConfig(failure_threshold=0) + with pytest.raises(ValueError, match=r".*"): + CircuitConfig(recovery_timeout=0.0) + with pytest.raises(ValueError, match=r".*"): + CircuitConfig(success_threshold=0) + with pytest.raises(ValueError, match=r".*"): + CircuitConfig(call_timeout=0.0) + + +def test_circuit_metrics_rates(): + metrics = CircuitBreaker("m").metrics + assert metrics.success_rate == 1.0 + metrics.total_calls = 10 + metrics.successful_calls = 7 + assert metrics.failure_rate == pytest.approx(0.3) + assert "failure_rate" in metrics.to_dict() + + +def test_circuit_breaker_name_validation(): + with pytest.raises(ValueError, match=r".*"): + CircuitBreaker("") + + +def test_circuit_breaker_properties(): + breaker = CircuitBreaker("props") + assert breaker.name == "props" + assert breaker.config is not None + + +@pytest.mark.asyncio +async def test_circuit_breaker_record_success_fast_path(): + breaker = CircuitBreaker("fast") + await breaker._record_success(0.01) + assert breaker.metrics.successful_calls == 1 + + +@pytest.mark.asyncio +async def test_circuit_breaker_reset(): + breaker = CircuitBreaker("reset", CircuitConfig(failure_threshold=1)) + + async def fail(): + raise RuntimeError("fail") + + with pytest.raises(RuntimeError): + await breaker.call(fail) + await breaker.reset() + assert breaker.state == CircuitState.CLOSED + + +@pytest.mark.asyncio +async def test_circuit_breaker_reset_logs_info(caplog): + breaker = CircuitBreaker("reset-log") + with caplog.at_level(logging.INFO, logger="pyoutlineapi.circuit_breaker"): + await breaker.reset() + assert any("manual reset" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_circuit_breaker_open_state_rejects(): + breaker = CircuitBreaker("open", CircuitConfig(recovery_timeout=10.0)) + breaker._state = CircuitState.OPEN + breaker._last_failure_time = 100.0 + with pytest.MonkeyPatch.context() as mp: + mp.setattr("pyoutlineapi.circuit_breaker.time.monotonic", lambda: 100.1) + with pytest.raises(CircuitOpenError) as exc: + await breaker.call(asyncio.sleep, 0) + assert exc.value.retry_after >= 0.0 + + +@pytest.mark.asyncio +async def test_transition_to_open_logs_info(caplog): + breaker = CircuitBreaker("transition-open") + with caplog.at_level(logging.INFO, logger="pyoutlineapi.circuit_breaker"): + await breaker._transition_to(CircuitState.OPEN) + assert breaker.state == CircuitState.OPEN + + +@pytest.mark.asyncio +async def test_circuit_breaker_timeout(): + breaker = CircuitBreaker("timeout", CircuitConfig(call_timeout=0.1)) + + async def slow(): + await asyncio.sleep(0.2) + return "ok" + + with pytest.raises(OutlineTimeoutError): + await breaker.call(slow) + + +@pytest.mark.asyncio +async def test_circuit_breaker_timeout_logs_warning(caplog): + breaker = CircuitBreaker("timeout-log", CircuitConfig(call_timeout=0.1)) + logger = logging.getLogger("pyoutlineapi.circuit_breaker") + logger.setLevel(logging.WARNING) + + async def slow(): + await asyncio.sleep(0.2) + + with ( + caplog.at_level( + logging.WARNING, + logger="pyoutlineapi.circuit_breaker", + ), + pytest.raises(OutlineTimeoutError), + ): + await breaker.call(slow) + assert any("timeout after" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_circuit_breaker_state_helpers(): + breaker = CircuitBreaker("helpers") + assert breaker.is_closed() is True + assert breaker.is_open() is False + assert breaker.is_half_open() is False + assert breaker.get_time_since_last_state_change() >= 0 + + +@pytest.mark.asyncio +async def test_circuit_breaker_check_state_transitions(caplog, monkeypatch): + breaker = CircuitBreaker( + "check", CircuitConfig(failure_threshold=1, recovery_timeout=1.0) + ) + breaker._failure_count = 1 + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.circuit_breaker"): + await breaker._check_state() + assert breaker.state == CircuitState.OPEN + + now = 100.0 + monkeypatch.setattr("pyoutlineapi.circuit_breaker.time.monotonic", lambda: now) + breaker._last_failure_time = now - 2.0 + with caplog.at_level(logging.INFO, logger="pyoutlineapi.circuit_breaker"): + await breaker._check_state() + assert breaker.state in {CircuitState.HALF_OPEN, CircuitState.OPEN} + + +@pytest.mark.asyncio +async def test_circuit_breaker_check_state_opens_with_warning(caplog): + breaker = CircuitBreaker("warn", CircuitConfig(failure_threshold=1)) + breaker._failure_count = 1 + logging.getLogger("pyoutlineapi.circuit_breaker").setLevel(logging.WARNING) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.circuit_breaker"): + await breaker._check_state() + assert breaker.state == CircuitState.OPEN + + +@pytest.mark.asyncio +async def test_circuit_breaker_check_state_half_open_noop(): + breaker = CircuitBreaker("check-half") + breaker._state = CircuitState.HALF_OPEN + await breaker._check_state() + assert breaker.state == CircuitState.HALF_OPEN + + +@pytest.mark.asyncio +async def test_circuit_breaker_record_success_and_failure(caplog): + breaker = CircuitBreaker("record", CircuitConfig(success_threshold=1)) + breaker._failure_count = 1 + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.circuit_breaker"): + await breaker._record_success(0.1) + assert breaker.metrics.successful_calls == 1 + + breaker._state = CircuitState.HALF_OPEN + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.circuit_breaker"): + await breaker._record_failure(0.1, RuntimeError("fail")) + assert breaker.state == CircuitState.OPEN + + +@pytest.mark.asyncio +async def test_circuit_breaker_record_success_resets_failures(caplog): + breaker = CircuitBreaker("record-reset") + breaker._failure_count = 2 + logger = logging.getLogger("pyoutlineapi.circuit_breaker") + logger.setLevel(logging.DEBUG) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.circuit_breaker"): + await breaker._record_success(0.1) + assert breaker.metrics.successful_calls == 1 + assert breaker._failure_count == 0 + + +@pytest.mark.asyncio +async def test_circuit_breaker_record_failure_debug(caplog): + breaker = CircuitBreaker("record-fail") + logger = logging.getLogger("pyoutlineapi.circuit_breaker") + logger.setLevel(logging.DEBUG) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.circuit_breaker"): + await breaker._record_failure(0.1, RuntimeError("boom")) + assert any("failure #1" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_circuit_breaker_half_open_success_closes(caplog): + breaker = CircuitBreaker("half-success", CircuitConfig(success_threshold=1)) + breaker._state = CircuitState.HALF_OPEN + logger = logging.getLogger("pyoutlineapi.circuit_breaker") + logger.setLevel(logging.INFO) + with caplog.at_level(logging.INFO, logger="pyoutlineapi.circuit_breaker"): + await breaker._record_success(0.1) + assert breaker.state == CircuitState.CLOSED + + +@pytest.mark.asyncio +async def test_circuit_breaker_half_open_success_logs_info(caplog): + breaker = CircuitBreaker("half-info", CircuitConfig(success_threshold=1)) + breaker._state = CircuitState.HALF_OPEN + logging.getLogger("pyoutlineapi.circuit_breaker").setLevel(logging.INFO) + with caplog.at_level(logging.INFO, logger="pyoutlineapi.circuit_breaker"): + await breaker._record_success(0.1) + assert any("closing after" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_circuit_breaker_half_open_failure_logs_warning(caplog): + breaker = CircuitBreaker("half-fail-log") + breaker._state = CircuitState.HALF_OPEN + logging.getLogger("pyoutlineapi.circuit_breaker").setLevel(logging.WARNING) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.circuit_breaker"): + await breaker._record_failure(0.1, RuntimeError("boom")) + assert any("reopening" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_circuit_breaker_transition_to_same_state(caplog): + breaker = CircuitBreaker("transition") + with caplog.at_level(logging.INFO, logger="pyoutlineapi.circuit_breaker"): + await breaker._transition_to(CircuitState.CLOSED) + assert breaker.state == CircuitState.CLOSED diff --git a/tests/test_client.py b/tests/test_client.py index 7a456f5..7b232ab 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,686 +1,930 @@ -""" -Tests for PyOutlineAPI client module. - -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. - -Copyright (c) 2025 Denis Rozhnovskiy -All rights reserved. - -This software is licensed under the MIT License. -You can find the full license text at: - https://opensource.org/licenses/MIT - -Source code repository: - https://github.com/orenlab/pyoutlineapi -""" +from __future__ import annotations +import asyncio import logging -import time +from contextlib import suppress +from typing import cast import aiohttp import pytest -from aioresponses import aioresponses - -# Import the client and related classes -from pyoutlineapi.client import AsyncOutlineClient -from pyoutlineapi.exceptions import APIError -from pyoutlineapi.models import ( - AccessKey, - AccessKeyList, - DataLimit, - MetricsStatusResponse, - Server, - ServerMetrics, + +from pyoutlineapi.audit import AuditLogger +from pyoutlineapi.client import ( + AsyncOutlineClient, + MultiServerManager, + create_client, + create_multi_server_manager, ) +from pyoutlineapi.config import OutlineClientConfig +from pyoutlineapi.exceptions import ConfigurationError + +PLACEHOLDER_CREDENTIAL = "pwd" + + +@pytest.mark.asyncio +async def test_resolve_configuration_errors(): + with pytest.raises(ConfigurationError): + AsyncOutlineClient._resolve_configuration(None, None, "x", {}) + with pytest.raises(ConfigurationError): + AsyncOutlineClient._resolve_configuration(None, "url", None, {}) + with pytest.raises(ConfigurationError): + AsyncOutlineClient._resolve_configuration( + OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ), + "url", + "cert", + {}, + ) + with pytest.raises(ConfigurationError): + AsyncOutlineClient._resolve_configuration(None, None, None, {}) + with pytest.raises(ConfigurationError): + AsyncOutlineClient._resolve_configuration(None, 123, 456, {}) + + +def test_client_init_with_config(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + json_format=True, + ) + client = AsyncOutlineClient(config=config) + assert client.config.api_url.startswith("https://example.com") + assert client.get_sanitized_config["cert_sha256"] == "***MASKED***" + assert client.json_format is True + + +def test_client_init_logs_info(caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + enable_logging=True, + ) + with caplog.at_level(logging.INFO, logger="pyoutlineapi.client"): + AsyncOutlineClient(config=config) + assert any("Client initialized" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_get_server_summary(monkeypatch, access_keys_list, server_dict): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fake_get_server_info(*args, **kwargs): + return server_dict + + async def fake_get_access_keys(*args, **kwargs): + return access_keys_list + + async def fake_get_metrics_status(*args, **kwargs): + return {"metricsEnabled": False} + + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", fake_get_metrics_status) + + summary = await client.get_server_summary() + assert summary["access_keys_count"] == 1 + assert summary["server"]["serverId"] == "srv-1" + + +@pytest.mark.asyncio +async def test_get_server_summary_error_branches(monkeypatch, server_dict): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fail_server(*args, **kwargs): + raise RuntimeError("server fail") + + async def bad_keys(*args, **kwargs): + return {"accessKeys": "bad"} + + async def metrics_enabled(*args, **kwargs): + return {"metricsEnabled": True} + + async def transfer_fail(*args, **kwargs): + raise RuntimeError("transfer fail") + + monkeypatch.setattr(client, "get_server_info", fail_server) + monkeypatch.setattr(client, "get_access_keys", bad_keys) + monkeypatch.setattr(client, "get_metrics_status", metrics_enabled) + monkeypatch.setattr(client, "get_transfer_metrics", transfer_fail) + + summary = await client.get_server_summary() + assert summary["healthy"] is False + assert summary["access_keys_count"] == 0 + assert summary["metrics_enabled"] is True + assert summary["errors"] + + +@pytest.mark.asyncio +async def test_get_server_summary_debug_logging(monkeypatch, caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fail_server(*args, **kwargs): + raise RuntimeError("server fail") + + async def fail_keys(*args, **kwargs): + raise RuntimeError("keys fail") + + async def fail_metrics(*args, **kwargs): + raise RuntimeError("metrics fail") + monkeypatch.setattr(client, "get_server_info", fail_server) + monkeypatch.setattr(client, "get_access_keys", fail_keys) + monkeypatch.setattr(client, "get_metrics_status", fail_metrics) -# Test data fixtures -@pytest.fixture -def valid_api_url(): - """Valid API URL for testing.""" - return "https://example.com:1234/secret" - - -@pytest.fixture -def valid_cert_sha256(): - """Valid SHA-256 certificate fingerprint.""" - return "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" - - -@pytest.fixture -def invalid_cert_sha256(): - """Invalid SHA-256 certificate fingerprint.""" - return "invalid_cert" - - -@pytest.fixture -def server_response(): - """Mock server information response.""" - return { - "name": "Test Server", - "serverId": "12345", - "metricsEnabled": True, - "createdTimestampMs": 1640995200000, - "version": "1.0.0", - "accessKeyDataLimit": {"bytes": 1073741824}, - "portForNewAccessKeys": 8388, - "hostnameForAccessKeys": "example.com" - } - - -@pytest.fixture -def access_key_response(): - """Mock access key response.""" - return { - "id": "1", - "name": "Test Key", - "password": "test_password", - "port": 8388, - "method": "chacha20-ietf-poly1305", - "accessUrl": "ss://test_url", - "dataLimit": {"bytes": 1073741824} - } - - -@pytest.fixture -def access_keys_list_response(): - """Mock access keys list response.""" - return { - "accessKeys": [ - { - "id": "1", - "name": "Key 1", - "password": "pass1", - "port": 8388, - "method": "chacha20-ietf-poly1305", - "accessUrl": "ss://url1" - }, - { - "id": "2", - "name": "Key 2", - "password": "pass2", - "port": 8389, - "method": "chacha20-ietf-poly1305", - "accessUrl": "ss://url2", - "dataLimit": {"bytes": 2147483648} - } - ] - } - - -@pytest.fixture -def metrics_status_response(): - """Mock metrics status response.""" - return {"metricsEnabled": True} - - -@pytest.fixture -def server_metrics_response(): - """Mock server metrics response.""" - return { - "bytesTransferredByUserId": { - "1": 1024000, - "2": 2048000 - } - } - - -@pytest.fixture -def experimental_metrics_response(): - """Mock experimental metrics response.""" - return { - "server": { - "tunnelTime": {"seconds": 3600}, - "dataTransferred": {"bytes": 1073741824} - }, - "accessKeys": [ - { - "id": "1", - "tunnelTime": {"seconds": 1800}, - "dataTransferred": {"bytes": 536870912} - } - ] - } - - -class TestAsyncOutlineClientInitialization: - """Test client initialization and validation.""" - - def test_valid_initialization(self, valid_api_url, valid_cert_sha256): - """Test successful client initialization.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256) - - assert client._api_url == valid_api_url - assert client._cert_sha256 == valid_cert_sha256 - assert client._json_format is False - assert client._retry_attempts == 3 - assert client._enable_logging is False - assert client._user_agent == "PyOutlineAPI/0.3.0" - assert client._max_connections == 10 - assert client._rate_limit_delay == 0.0 - - def test_initialization_with_custom_params(self, valid_api_url, valid_cert_sha256): - """Test initialization with custom parameters.""" - client = AsyncOutlineClient( - valid_api_url, - valid_cert_sha256, - json_format=True, - timeout=60, - retry_attempts=5, - enable_logging=True, - user_agent="Custom Agent", - max_connections=20, - rate_limit_delay=1.0 - ) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.client"): + summary = await client.get_server_summary() + assert summary["errors"] + assert any("Failed to fetch server info" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_get_server_summary_transfer_metrics_logging( + monkeypatch, caplog, server_dict +): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fake_get_server_info(*args, **kwargs): + return server_dict + + async def fake_get_access_keys(*args, **kwargs): + return {"accessKeys": []} + + async def metrics_enabled(*args, **kwargs): + return {"metricsEnabled": True} + + async def transfer_fail(*args, **kwargs): + raise RuntimeError("transfer fail") + + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", metrics_enabled) + monkeypatch.setattr(client, "get_transfer_metrics", transfer_fail) + + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.client"): + summary = await client.get_server_summary() + assert summary["metrics_enabled"] is True + assert any("Failed to fetch transfer metrics" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_get_server_summary_metrics_response_logging( + monkeypatch, caplog, server_dict +): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + from pyoutlineapi.models import MetricsStatusResponse + + async def fake_get_server_info(*args, **kwargs): + return server_dict + + async def fake_get_access_keys(*args, **kwargs): + return {"accessKeys": []} + + async def metrics_status(*args, **kwargs): + return MetricsStatusResponse(metricsEnabled=True) + + async def transfer_fail(*args, **kwargs): + raise RuntimeError("transfer fail") + + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", metrics_status) + monkeypatch.setattr(client, "get_transfer_metrics", transfer_fail) + + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.client"): + summary = await client.get_server_summary() + assert summary["metrics_enabled"] is True + assert any("Failed to fetch transfer metrics" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_get_server_summary_unexpected_metrics_status(monkeypatch, server_dict): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fake_get_server_info(*args, **kwargs): + return server_dict + + async def fake_get_access_keys(*args, **kwargs): + return {"accessKeys": []} + + async def metrics_status(*args, **kwargs): + return "bad" + + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", metrics_status) + + summary = await client.get_server_summary() + assert summary["metrics_enabled"] is False + + +@pytest.mark.asyncio +async def test_get_server_summary_unexpected_keys_result(monkeypatch, server_dict): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fake_get_server_info(*args, **kwargs): + return server_dict + + async def bad_keys(*args, **kwargs): + return "bad" + + async def metrics_status(*args, **kwargs): + return {"metricsEnabled": False} + + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", bad_keys) + monkeypatch.setattr(client, "get_metrics_status", metrics_status) + + summary = await client.get_server_summary() + assert summary["access_keys_count"] == 0 - assert client._json_format is True - assert client._timeout.total == 60 - assert client._retry_attempts == 5 - assert client._enable_logging is True - assert client._user_agent == "Custom Agent" - assert client._max_connections == 20 - assert client._rate_limit_delay == 1.0 - def test_empty_api_url_raises_error(self, valid_cert_sha256): - """Test that empty API URL raises ValueError.""" - with pytest.raises(ValueError, match="api_url cannot be empty"): - AsyncOutlineClient("", valid_cert_sha256) +@pytest.mark.asyncio +async def test_get_server_summary_access_key_list( + monkeypatch, access_key_dict, server_dict +): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) - def test_whitespace_api_url_raises_error(self, valid_cert_sha256): - """Test that whitespace-only API URL raises ValueError.""" - with pytest.raises(ValueError, match="api_url cannot be empty"): - AsyncOutlineClient(" ", valid_cert_sha256) + async def fake_get_server_info(*args, **kwargs): + return server_dict - def test_empty_cert_sha256_raises_error(self, valid_api_url): - """Test that empty certificate SHA256 raises ValueError.""" - with pytest.raises(ValueError, match="cert_sha256 cannot be empty"): - AsyncOutlineClient(valid_api_url, "") - - def test_invalid_cert_sha256_format_raises_error(self, valid_api_url): - """Test that invalid certificate format raises ValueError.""" - with pytest.raises(ValueError, match="cert_sha256 must contain only hexadecimal"): - AsyncOutlineClient(valid_api_url, "invalid_hex_string") - - def test_wrong_cert_sha256_length_raises_error(self, valid_api_url): - """Test that wrong certificate length raises ValueError.""" - with pytest.raises(ValueError, match="cert_sha256 must be exactly 64 hexadecimal"): - AsyncOutlineClient(valid_api_url, "abcdef123456") - - def test_api_url_trailing_slash_removal(self, valid_cert_sha256): - """Test that trailing slashes are removed from API URL.""" - client = AsyncOutlineClient("https://example.com/path/", valid_cert_sha256) - assert client._api_url == "https://example.com/path" + from pyoutlineapi.models import AccessKeyList + async def fake_get_access_keys(*args, **kwargs): + return AccessKeyList(accessKeys=[access_key_dict]) -class TestAsyncOutlineClientContextManager: - """Test async context manager behavior.""" + async def fake_get_metrics_status(*args, **kwargs): + return {"metricsEnabled": False} - @pytest.mark.asyncio - async def test_context_manager_entry_exit(self, valid_api_url, valid_cert_sha256): - """Test context manager entry and exit.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256) + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", fake_get_metrics_status) - async with client as c: - assert c is client - assert client._session is not None - assert not client._session.closed - - assert client._session is None + summary = await client.get_server_summary() + assert summary["access_keys_count"] == 1 - @pytest.mark.asyncio - async def test_create_factory_method(self, valid_api_url, valid_cert_sha256): - """Test the create factory method.""" - async with AsyncOutlineClient.create(valid_api_url, valid_cert_sha256) as client: - assert isinstance(client, AsyncOutlineClient) - assert client._session is not None - - @pytest.mark.asyncio - async def test_logging_setup_on_enter(self, valid_api_url, valid_cert_sha256, caplog): - """Test logging setup when entering context manager.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256, enable_logging=True) - - with caplog.at_level(logging.INFO): - async with client: - pass - assert "Initialized OutlineAPI client" in caplog.text - assert "OutlineAPI client session closed" in caplog.text +@pytest.mark.asyncio +async def test_get_server_summary_metrics_status_response( + monkeypatch, access_key_dict, server_dict +): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + async def fake_get_server_info(*args, **kwargs): + return server_dict -class TestAsyncOutlineClientRequests: - """Test HTTP request functionality.""" + async def fake_get_access_keys(*args, **kwargs): + return {"accessKeys": [access_key_dict]} - @pytest.mark.asyncio - async def test_ensure_context_decorator_without_session(self, valid_api_url, valid_cert_sha256): - """Test that methods fail without active session.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256) + from pyoutlineapi.models import MetricsStatusResponse - with pytest.raises(RuntimeError, match="Client session is not initialized"): - await client.get_server_info() + async def fake_get_metrics_status(*args, **kwargs): + return MetricsStatusResponse(metricsEnabled=True) - @pytest.mark.asyncio - async def test_build_url_with_valid_endpoint(self, valid_api_url, valid_cert_sha256): - """Test URL building with valid endpoint.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256) + async def fail_transfer(*args, **kwargs): + raise RuntimeError("transfer fail") - url = client._build_url("server") - assert url == f"{valid_api_url}/server" + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", fake_get_metrics_status) + monkeypatch.setattr(client, "get_transfer_metrics", fail_transfer) - url = client._build_url("/server") - assert url == f"{valid_api_url}/server" + summary = await client.get_server_summary() + assert summary["metrics_enabled"] is True + assert summary["errors"] - def test_build_url_with_invalid_endpoint(self, valid_api_url, valid_cert_sha256): - """Test URL building with invalid endpoint.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256) - with pytest.raises(ValueError, match="Endpoint must be a string"): - client._build_url(None) +@pytest.mark.asyncio +async def test_get_server_summary_with_metrics( + monkeypatch, access_keys_list, server_dict, server_metrics_dict +): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) - def test_get_ssl_context_success(self, valid_api_url, valid_cert_sha256): - """Test SSL context creation with valid certificate.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256) + async def fake_get_server_info(*args, **kwargs): + return server_dict - ssl_context = client._get_ssl_context() - assert ssl_context is not None + async def fake_get_access_keys(*args, **kwargs): + return access_keys_list + + async def fake_get_metrics_status(*args, **kwargs): + return {"metricsEnabled": True} + + async def fake_get_transfer_metrics(*args, **kwargs): + return server_metrics_dict + + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", fake_get_metrics_status) + monkeypatch.setattr(client, "get_transfer_metrics", fake_get_transfer_metrics) + + summary = await client.get_server_summary() + assert summary["metrics_enabled"] is True + assert "transfer_metrics" in summary + + +@pytest.mark.asyncio +async def test_health_check_error(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fail(*args, **kwargs): + raise RuntimeError("boom") + + monkeypatch.setattr(client, "get_server_info", fail) + data = await client.health_check() + assert data["healthy"] is False + + +@pytest.mark.asyncio +async def test_health_check_success(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def ok(*args, **kwargs): + return {"ok": True} + + monkeypatch.setattr(client, "get_server_info", ok) + data = await client.health_check() + assert data["healthy"] is True + + +@pytest.mark.asyncio +async def test_multi_server_manager_health(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + + async def fake_aenter(self): + return self + + async def fake_health_check(self): + return {"healthy": True} + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fake_aenter) + monkeypatch.setattr(AsyncOutlineClient, "health_check", fake_health_check) + + manager = MultiServerManager([config]) + async with manager: + results = await manager.health_check_all() + assert next(iter(results.values()))["healthy"] is True + + +@pytest.mark.asyncio +async def test_create_context_manager(monkeypatch): + async def fake_aenter(self): + return self + + async def fake_aexit(self, exc_type, exc, tb): + return None + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fake_aenter) + monkeypatch.setattr(AsyncOutlineClient, "__aexit__", fake_aexit) + + async with AsyncOutlineClient.create( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) as client: + assert isinstance(client, AsyncOutlineClient) + + +@pytest.mark.asyncio +async def test_create_context_manager_with_config(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + + async def fake_aenter(self): + return self + + async def fake_aexit(self, exc_type, exc, tb): + return None + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fake_aenter) + monkeypatch.setattr(AsyncOutlineClient, "__aexit__", fake_aexit) + + async with AsyncOutlineClient.create(config=config) as client: + assert isinstance(client, AsyncOutlineClient) + + +def test_create_client_function(): + client = create_client( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + assert isinstance(client, AsyncOutlineClient) + + +def test_from_env(tmp_path): + env_file = tmp_path / ".env" + env_file.write_text( + "OUTLINE_API_URL=https://example.com/secret\n" + "OUTLINE_CERT_SHA256=" + "a" * 64 + "\n", + encoding="utf-8", + ) + client = AsyncOutlineClient.from_env(env_file=env_file) + assert isinstance(client, AsyncOutlineClient) + + +@pytest.mark.asyncio +async def test_multi_server_manager_helpers(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + + async def fake_aenter(self): + return self + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fake_aenter) + + manager = create_multi_server_manager([config]) + async with manager: + assert manager.server_count == 1 + assert len(manager.get_server_names()) == 1 + client = manager.get_client(0) + assert isinstance(client, AsyncOutlineClient) + assert manager.get_all_clients() + assert "MultiServerManager" in repr(manager) + assert manager.get_status_summary()["total_servers"] == 1 + + # string identifier lookup + safe_name = manager.get_server_names()[0] + assert manager.get_client(safe_name) is client + + +@pytest.mark.asyncio +async def test_multi_server_manager_logging(monkeypatch, caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + + async def fake_aenter(self): + return self + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fake_aenter) + manager = MultiServerManager([config]) + with caplog.at_level(logging.INFO, logger="pyoutlineapi.client"): + async with manager: + pass + assert any("initialized" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_multi_server_manager_init_failure(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + + async def fail_aenter(self): + raise RuntimeError("boom") + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fail_aenter) + + manager = MultiServerManager([config]) + with pytest.raises(ConfigurationError): + async with manager: + pass + + +def test_multi_server_manager_init_errors(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + with pytest.raises(ConfigurationError): + MultiServerManager([]) + with pytest.raises(ConfigurationError): + MultiServerManager([config] * 51) + + +@pytest.mark.asyncio +async def test_health_check_all_error_path(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + manager = MultiServerManager([config]) + + class Dummy: + async def health_check(self) -> dict[str, object]: + raise RuntimeError("fail") + + manager._clients = {"srv": cast(AsyncOutlineClient, Dummy())} + results = await manager.health_check_all() + assert results["srv"]["healthy"] is False + + +@pytest.mark.asyncio +async def test_health_check_all_exception_result(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + manager = MultiServerManager([config]) + + async def boom(*_args: object, **_kwargs: object) -> dict[str, object]: + raise RuntimeError("boom") + + manager._clients = {"srv": cast(AsyncOutlineClient, object())} + monkeypatch.setattr(MultiServerManager, "_health_check_single", boom) + + results = await manager.health_check_all() + assert results["srv"]["error_type"] == "RuntimeError" + + +@pytest.mark.asyncio +async def test_health_check_single_timeout(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + manager = MultiServerManager([config]) + + class SlowClient: + async def health_check(self) -> dict[str, object]: + await asyncio.sleep(0.01) + return {"healthy": True} + + result = await manager._health_check_single( + "srv", + cast(AsyncOutlineClient, SlowClient()), + timeout=0.001, + ) + assert result["error_type"] == "TimeoutError" + + +@pytest.mark.asyncio +async def test_multi_server_manager_get_client_errors(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + manager = MultiServerManager([config]) + with pytest.raises(KeyError): + manager.get_client("missing") + with pytest.raises(IndexError): + manager.get_client(1) + + +@pytest.mark.asyncio +async def test_multi_server_manager_aexit_clears_clients(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + + async def fake_aenter(self): + return self + + async def fake_aexit(self, exc_type, exc, tb): + return None + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fake_aenter) + monkeypatch.setattr(AsyncOutlineClient, "__aexit__", fake_aexit) + + manager = MultiServerManager([config]) + await manager.__aenter__() + await manager.__aexit__(None, None, None) + assert manager.get_all_clients() == [] + + +@pytest.mark.asyncio +async def test_multi_server_manager_aexit_logs_warning(monkeypatch, caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + + async def fake_aenter(self): + return self + + async def bad_aexit(self, exc_type, exc, tb): + raise RuntimeError("boom") + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", fake_aenter) + monkeypatch.setattr(AsyncOutlineClient, "__aexit__", bad_aexit) + + manager = MultiServerManager([config]) + await manager.__aenter__() + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.client"): + await manager.__aexit__(None, None, None) + assert any("Shutdown completed with" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_client_status_and_repr(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + async def fake_get_server_info(*args, **kwargs): + return {"serverId": "s"} + + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + status = client.get_status() + assert "rate_limit" in status + assert "AsyncOutlineClient" in repr(client) + + task = asyncio.create_task(asyncio.sleep(0.01)) + client._active_requests.add(task) + try: + assert "requests=" in repr(client) + finally: + task.cancel() + with suppress(asyncio.CancelledError): + await task + client._active_requests.clear() + + +@pytest.mark.asyncio +async def test_client_aexit_handles_errors(monkeypatch, caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + class BadAudit: + async def shutdown(self) -> None: + raise RuntimeError("bad") + + async def bad_shutdown(timeout: float = 30.0) -> None: + raise RuntimeError("fail") + + class DummySession: + closed = False + + async def close(self) -> None: + self.closed = True + + client._audit_logger_instance = cast(AuditLogger, BadAudit()) + monkeypatch.setattr(client, "shutdown", bad_shutdown) + client._session = cast(aiohttp.ClientSession, DummySession()) + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.client"): + await client.__aexit__(None, None, None) + assert client._session.closed is True + + +@pytest.mark.asyncio +async def test_client_aexit_emergency_cleanup_error(caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + class BadAudit: + async def shutdown(self) -> None: + raise RuntimeError("shutdown fail") + + class BadSession: + closed = False + + async def close(self) -> None: + raise RuntimeError("close fail") + + client._audit_logger_instance = cast(AuditLogger, BadAudit()) + client._session = cast(aiohttp.ClientSession, BadSession()) + + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.client"): + await client.__aexit__(None, None, None) + assert any("Emergency cleanup error" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_get_healthy_servers_missing_client(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + manager = MultiServerManager([config]) + + async def fake_health_check_all(self, *args, **kwargs): + return {"missing": {"healthy": True}} + + monkeypatch.setattr(MultiServerManager, "health_check_all", fake_health_check_all) + healthy = await manager.get_healthy_servers() + assert healthy == [] + + +@pytest.mark.asyncio +async def test_get_healthy_servers_filters(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + manager = MultiServerManager([config]) + + class DummyClient: + is_connected = True + + manager._clients = {"srv": cast(AsyncOutlineClient, DummyClient())} + + async def fake_health_check_all(self, *args, **kwargs): + return {"srv": {"healthy": True}} + + monkeypatch.setattr(MultiServerManager, "health_check_all", fake_health_check_all) + healthy = await manager.get_healthy_servers() + assert healthy + + +@pytest.mark.asyncio +async def test_health_check_access_key_list_and_metrics(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + + from pyoutlineapi.models import AccessKey, AccessKeyList, MetricsStatusResponse + + async def fake_get_server_info(*args, **kwargs): + return {"serverId": "srv"} + + async def fake_get_access_keys(*args, **kwargs): + key = AccessKey( + id="key-1", + name="Name", + password=PLACEHOLDER_CREDENTIAL, + port=12345, + method="aes-256-gcm", + accessUrl="ss://example", + dataLimit=None, + ) + return AccessKeyList(accessKeys=[key]) - @pytest.mark.asyncio - async def test_rate_limiting_applied(self, valid_api_url, valid_cert_sha256): - """Test that rate limiting is applied correctly.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256, rate_limit_delay=0.1) + async def fake_get_metrics_status(*args, **kwargs): + return MetricsStatusResponse(metricsEnabled=True) - start_time = time.time() - await client._apply_rate_limiting() - client._last_request_time = time.time() - await client._apply_rate_limiting() - end_time = time.time() + async def fake_get_transfer_metrics(*args, **kwargs): + return {"bytesTransferredByUserId": {"key-1": 10}} - # Should have delayed at least 0.1 seconds - assert end_time - start_time >= 0.1 + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", fake_get_metrics_status) + monkeypatch.setattr(client, "get_transfer_metrics", fake_get_transfer_metrics) - @pytest.mark.asyncio - async def test_rate_limiting_no_delay(self, valid_api_url, valid_cert_sha256): - """Test that no rate limiting is applied when delay is 0.""" - client = AsyncOutlineClient(valid_api_url, valid_cert_sha256, rate_limit_delay=0.0) + summary = await client.get_server_summary() + assert summary["access_keys_count"] == 1 + assert summary["metrics_enabled"] is True + assert "transfer_metrics" in summary - start_time = time.time() - await client._apply_rate_limiting() - await client._apply_rate_limiting() - end_time = time.time() - # Should not have significant delay - assert end_time - start_time < 0.01 +@pytest.mark.asyncio +async def test_get_server_summary_dict_keys_and_metrics(monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) + async def fake_get_server_info(*args, **kwargs): + return {"serverId": "srv"} -class TestAsyncOutlineClientRetryLogic: - """Test retry logic functionality.""" + async def fake_get_access_keys(*args, **kwargs): + return {"accessKeys": [{"id": "k1"}]} - @pytest.mark.asyncio - async def test_retry_success_on_second_attempt(self): - """Test successful retry after initial failure.""" - call_count = 0 + async def fake_get_metrics_status(*args, **kwargs): + return {"metricsEnabled": False} - async def failing_request(): - nonlocal call_count - call_count += 1 - if call_count == 1: - raise aiohttp.ClientError("Temporary failure") - return "success" + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", fake_get_metrics_status) - result = await AsyncOutlineClient._retry_request(failing_request, attempts=3) - assert result == "success" - assert call_count == 2 + summary = await client.get_server_summary() + assert summary["access_keys_count"] == 1 + assert summary["metrics_enabled"] is False - @pytest.mark.asyncio - async def test_retry_exhausted_attempts(self): - """Test that retry stops after max attempts.""" - call_count = 0 - async def always_failing_request(): - nonlocal call_count - call_count += 1 - raise aiohttp.ClientError("Always failing") +@pytest.mark.asyncio +async def test_get_server_summary_metrics_status_error(monkeypatch, caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) - with pytest.raises(APIError, match="Request failed after 2 attempts"): - await AsyncOutlineClient._retry_request(always_failing_request, attempts=2) + async def fake_get_server_info(*args, **kwargs): + return {"serverId": "srv"} - assert call_count == 2 + async def fake_get_access_keys(*args, **kwargs): + return {"accessKeys": []} - @pytest.mark.asyncio - async def test_retry_non_retriable_error(self): - """Test that non-retriable errors are not retried.""" - call_count = 0 + async def fake_get_metrics_status(*args, **kwargs): + raise RuntimeError("metrics fail") - async def non_retriable_error(): - nonlocal call_count - call_count += 1 - raise APIError("Bad request", 400) + monkeypatch.setattr(client, "get_server_info", fake_get_server_info) + monkeypatch.setattr(client, "get_access_keys", fake_get_access_keys) + monkeypatch.setattr(client, "get_metrics_status", fake_get_metrics_status) - with pytest.raises(APIError, match="Bad request"): - await AsyncOutlineClient._retry_request(non_retriable_error, attempts=3) - - assert call_count == 1 + with caplog.at_level(logging.DEBUG, logger="pyoutlineapi.client"): + summary = await client.get_server_summary() + assert summary["errors"] -class TestAsyncOutlineClientServerMethods: - """Test server management methods.""" +@pytest.mark.asyncio +async def test_client_aexit_cleanup_logs_warning(caplog, monkeypatch): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + client = AsyncOutlineClient(config=config) - @pytest.mark.asyncio - async def test_get_server_info_success(self, valid_api_url, valid_cert_sha256, server_response): - """Test successful server info retrieval.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/server", payload=server_response) + class BadAudit: + async def shutdown(self) -> None: + raise RuntimeError("audit fail") - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.get_server_info() + async def bad_shutdown(timeout: float = 30.0) -> None: + raise RuntimeError("shutdown fail") - assert isinstance(result, Server) - assert result.name == "Test Server" - assert result.server_id == "12345" + class DummySession: + closed = False - @pytest.mark.asyncio - async def test_get_server_info_json_format(self, valid_api_url, valid_cert_sha256, server_response): - """Test server info retrieval in JSON format.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/server", payload=server_response) + async def close(self) -> None: + self.closed = True - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256, json_format=True) as client: - result = await client.get_server_info() + client._audit_logger_instance = cast(AuditLogger, BadAudit()) + monkeypatch.setattr(client, "shutdown", bad_shutdown) + client._session = cast(aiohttp.ClientSession, DummySession()) - assert isinstance(result, dict) - assert result["name"] == "Test Server" - - @pytest.mark.asyncio - async def test_rename_server_success(self, valid_api_url, valid_cert_sha256): - """Test successful server renaming.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/name", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.rename_server("New Server Name") - assert result is True - - @pytest.mark.asyncio - async def test_set_hostname_success(self, valid_api_url, valid_cert_sha256): - """Test successful hostname setting.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/server/hostname-for-access-keys", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.set_hostname("vpn.example.com") - assert result is True - - @pytest.mark.asyncio - async def test_set_default_port_success(self, valid_api_url, valid_cert_sha256): - """Test successful default port setting.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/server/port-for-new-access-keys", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.set_default_port(8388) - assert result is True - - @pytest.mark.asyncio - async def test_set_default_port_invalid_range(self, valid_api_url, valid_cert_sha256): - """Test port validation for invalid ranges.""" - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - with pytest.raises(ValueError, match="Privileged ports are not allowed"): - await client.set_default_port(80) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.client"): + await client.__aexit__(None, None, None) + assert any("Cleanup completed with" in r.message for r in caplog.records) - with pytest.raises(ValueError, match="Privileged ports are not allowed"): - await client.set_default_port(70000) - - -class TestAsyncOutlineClientMetricsMethods: - """Test metrics-related methods.""" - - @pytest.mark.asyncio - async def test_get_metrics_status_success(self, valid_api_url, valid_cert_sha256, metrics_status_response): - """Test successful metrics status retrieval.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/metrics/enabled", payload=metrics_status_response) - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.get_metrics_status() - - assert isinstance(result, MetricsStatusResponse) - assert result.metrics_enabled is True - - @pytest.mark.asyncio - async def test_set_metrics_status_success(self, valid_api_url, valid_cert_sha256): - """Test successful metrics status setting.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/metrics/enabled", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.set_metrics_status(True) - assert result is True - - @pytest.mark.asyncio - async def test_get_transfer_metrics_success(self, valid_api_url, valid_cert_sha256, server_metrics_response): - """Test successful transfer metrics retrieval.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/metrics/transfer", payload=server_metrics_response) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.get_transfer_metrics() - - assert isinstance(result, ServerMetrics) - assert "1" in result.bytes_transferred_by_user_id - - -class TestAsyncOutlineClientAccessKeyMethods: - """Test access key management methods.""" - - @pytest.mark.asyncio - async def test_create_access_key_success(self, valid_api_url, valid_cert_sha256, access_key_response): - """Test successful access key creation.""" - with aioresponses() as m: - m.post(f"{valid_api_url}/access-keys", payload=access_key_response) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.create_access_key(name="Test Key") - - assert isinstance(result, AccessKey) - assert result.name == "Test Key" - assert result.id == "1" - - @pytest.mark.asyncio - async def test_create_access_key_with_all_params(self, valid_api_url, valid_cert_sha256, access_key_response): - """Test access key creation with all parameters.""" - with aioresponses() as m: - m.post(f"{valid_api_url}/access-keys", payload=access_key_response) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - limit = DataLimit(bytes=1024 ** 3) - result = await client.create_access_key( - name="Full Key", - password="secret", - port=8388, - method="chacha20-ietf-poly1305", - limit=limit - ) - - assert isinstance(result, AccessKey) - - @pytest.mark.asyncio - async def test_create_access_key_with_id(self, valid_api_url, valid_cert_sha256, access_key_response): - """Test access key creation with specific ID.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/access-keys/custom-id", payload=access_key_response) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.create_access_key_with_id("custom-id", name="Custom Key") - - assert isinstance(result, AccessKey) - - @pytest.mark.asyncio - async def test_get_access_keys_success(self, valid_api_url, valid_cert_sha256, access_keys_list_response): - """Test successful access keys retrieval.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/access-keys", payload=access_keys_list_response) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.get_access_keys() - - assert isinstance(result, AccessKeyList) - assert len(result.access_keys) == 2 - assert result.access_keys[0].id == "1" - - @pytest.mark.asyncio - async def test_get_access_key_success(self, valid_api_url, valid_cert_sha256, access_key_response): - """Test successful single access key retrieval.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/access-keys/1", payload=access_key_response) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.get_access_key("1") - - assert isinstance(result, AccessKey) - assert result.id == "1" - - @pytest.mark.asyncio - async def test_rename_access_key_success(self, valid_api_url, valid_cert_sha256): - """Test successful access key renaming.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/access-keys/1/name", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.rename_access_key("1", "New Name") - assert result is True - - @pytest.mark.asyncio - async def test_delete_access_key_success(self, valid_api_url, valid_cert_sha256): - """Test successful access key deletion.""" - with aioresponses() as m: - m.delete(f"{valid_api_url}/access-keys/1", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.delete_access_key("1") - assert result is True - - @pytest.mark.asyncio - async def test_set_access_key_data_limit_success(self, valid_api_url, valid_cert_sha256): - """Test successful access key data limit setting.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/access-keys/1/data-limit", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.set_access_key_data_limit("1", 1024 ** 3) - assert result is True - - @pytest.mark.asyncio - async def test_remove_access_key_data_limit_success(self, valid_api_url, valid_cert_sha256): - """Test successful access key data limit removal.""" - with aioresponses() as m: - m.delete(f"{valid_api_url}/access-keys/1/data-limit", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.remove_access_key_data_limit("1") - assert result is True - - -class TestAsyncOutlineClientGlobalDataLimit: - """Test global data limit methods.""" - - @pytest.mark.asyncio - async def test_set_global_data_limit_success(self, valid_api_url, valid_cert_sha256): - """Test successful global data limit setting.""" - with aioresponses() as m: - m.put(f"{valid_api_url}/server/access-key-data-limit", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.set_global_data_limit(100 * 1024 ** 3) - assert result is True - - @pytest.mark.asyncio - async def test_remove_global_data_limit_success(self, valid_api_url, valid_cert_sha256): - """Test successful global data limit removal.""" - with aioresponses() as m: - m.delete(f"{valid_api_url}/server/access-key-data-limit", status=204) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.remove_global_data_limit() - assert result is True - - -class TestAsyncOutlineClientBatchOperations: - """Test batch operations.""" - - @pytest.mark.asyncio - async def test_batch_create_access_keys_success(self, valid_api_url, valid_cert_sha256, access_key_response): - """Test successful batch access key creation.""" - with aioresponses() as m: - # Mock multiple POST requests - for i in range(2): - response_copy = access_key_response.copy() - response_copy["id"] = str(i + 1) - response_copy["name"] = f"Key {i + 1}" - m.post(f"{valid_api_url}/access-keys", payload=response_copy) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - configs = [ - {"name": "Key 1"}, - {"name": "Key 2", "port": 8388} - ] - results = await client.batch_create_access_keys(configs) - - assert len(results) == 2 - assert all(isinstance(r, AccessKey) for r in results) - - @pytest.mark.asyncio - async def test_batch_create_access_keys_with_failure(self, valid_api_url, valid_cert_sha256, access_key_response): - """Test batch creation with some failures and fail_fast=False.""" - with aioresponses() as m: - # First request succeeds - m.post(f"{valid_api_url}/access-keys", payload=access_key_response) - # Second request fails - m.post(f"{valid_api_url}/access-keys", status=400, payload={"error": "Bad request"}) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - configs = [ - {"name": "Key 1"}, - {"name": "Key 2"} - ] - results = await client.batch_create_access_keys(configs, fail_fast=False) - - assert len(results) == 2 - assert isinstance(results[0], AccessKey) - assert isinstance(results[1], Exception) - - @pytest.mark.asyncio - async def test_batch_create_access_keys_fail_fast(self, valid_api_url, valid_cert_sha256): - """Test batch creation with fail_fast=True.""" - with aioresponses() as m: - m.post(f"{valid_api_url}/access-keys", status=400, payload={"error": "Bad request"}) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - configs = [{"name": "Key 1"}] - - with pytest.raises(APIError): - await client.batch_create_access_keys(configs, fail_fast=True) - - -class TestAsyncOutlineClientHealthCheck: - """Test health check functionality.""" - - @pytest.mark.asyncio - async def test_health_check_success(self, valid_api_url, valid_cert_sha256, server_response): - """Test successful health check.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/server", payload=server_response) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.health_check() - assert result is True - assert client.is_healthy is True - - @pytest.mark.asyncio - async def test_health_check_failure(self, valid_api_url, valid_cert_sha256): - """Test health check failure.""" - with aioresponses() as m: - m.get(f"{valid_api_url}/server", status=500) - - async with AsyncOutlineClient(valid_api_url, valid_cert_sha256) as client: - result = await client.health_check() - assert result is False - assert client.is_healthy is False +@pytest.mark.asyncio +async def test_multi_server_manager_aenter_logs_warning(monkeypatch, caplog): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + manager = MultiServerManager([config]) + + async def bad_enter(self) -> AsyncOutlineClient: + raise RuntimeError("fail") + + monkeypatch.setattr(AsyncOutlineClient, "__aenter__", bad_enter) + with ( + caplog.at_level( + logging.WARNING, + logger="pyoutlineapi.client", + ), + pytest.raises(ConfigurationError), + ): + async with manager: + pass + assert any("Failed to initialize server" in r.message for r in caplog.records) diff --git a/tests/test_common_types.py b/tests/test_common_types.py new file mode 100644 index 0000000..1b38240 --- /dev/null +++ b/tests/test_common_types.py @@ -0,0 +1,376 @@ +from __future__ import annotations + +import time +from datetime import datetime, timezone + +import pytest + +from pyoutlineapi import common_types +from pyoutlineapi.common_types import ( + Constants, + CredentialSanitizer, + SecureIDGenerator, + SSRFProtection, + Validators, + build_config_overrides, + is_json_serializable, + is_valid_bytes, + is_valid_port, + mask_sensitive_data, + merge_config_kwargs, + validate_snapshot_size, +) +from pyoutlineapi.models import DataLimit + +MASKED_VALUE = "***MASKED***" +SockAddr = tuple[str, int] | tuple[str, int, int, int] +AddrInfo = tuple[object, object, object, object, SockAddr] + + +def test_is_valid_port_and_bytes(): + assert is_valid_port(1) is True + assert is_valid_port(65535) is True + assert is_valid_port(0) is False + assert is_valid_bytes(0) is True + assert is_valid_bytes(-1) is False + + +def test_ssrf_protection_blocked_private_ip(): + assert SSRFProtection.is_blocked_ip("10.0.0.1") is True + assert SSRFProtection.is_blocked_ip("127.0.0.1") is False + assert SSRFProtection.is_blocked_ip("example.com") is False + + +def test_credential_sanitizer_patterns(): + text = "password=secret api_key=ABCDEFGH1234567890TOKEN" + sanitized = CredentialSanitizer.sanitize(text) + assert "***PASSWORD***" in sanitized + assert "***API_KEY***" in sanitized + assert CredentialSanitizer.sanitize("") == "" + + +def test_mask_sensitive_data_nested(): + data = {"token": "abc", "nested": {"password": "secret"}, "ok": 1} + masked = mask_sensitive_data(data) + assert masked["token"] == MASKED_VALUE + assert masked["nested"]["password"] == MASKED_VALUE + assert masked["ok"] == 1 + + +def test_mask_sensitive_data_list_branch(): + data = {"items": [{"token": "x"}, {"ok": 1}], "other": 1} + masked = mask_sensitive_data(data) + assert masked["items"][0]["token"] == MASKED_VALUE + + +def test_mask_sensitive_data_list_with_non_dict_items(): + data = {"items": [{"token": "x"}, "plain", 5]} + masked = mask_sensitive_data(data) + assert masked["items"][0]["token"] == MASKED_VALUE + assert masked["items"][1] == "plain" + assert masked["items"][2] == 5 + + +def test_validators_basic(): + assert Validators.validate_port(8080) == 8080 + with pytest.raises(ValueError, match=r".*"): + Validators.validate_port(0) + + assert Validators.validate_name(" Test ") == "Test" + with pytest.raises(ValueError, match=r".*"): + Validators.validate_name(" ") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_name("x" * (Constants.MAX_NAME_LENGTH + 1)) + + from pydantic import SecretStr + + secret = Validators.validate_cert_fingerprint( + SecretStr("a" * Constants.CERT_FINGERPRINT_LENGTH) + ) + assert secret.get_secret_value() == "a" * Constants.CERT_FINGERPRINT_LENGTH + + from pydantic import SecretStr + + with pytest.raises(ValueError, match=r".*"): + Validators.validate_cert_fingerprint(SecretStr("bad")) + with pytest.raises(ValueError, match=r".*"): + Validators.validate_cert_fingerprint(SecretStr("")) + + +def test_validate_url_private_networks(): + url = "https://10.0.0.1:1234/secret" + assert Validators.validate_url(url, allow_private_networks=True) == url + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url(url, allow_private_networks=False) + + +def test_validate_url_invalid_cases(): + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url("") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url("http://") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url("http://example.com/\x00") + long_url = "http://example.com/" + ("a" * (Constants.MAX_URL_LENGTH + 1)) + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url(long_url) + + +def test_validate_url_strict_ssrf_blocks_private(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + return [(None, None, None, None, ("10.0.0.5", 0))] + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url( + "https://example.com", + allow_private_networks=False, + resolve_dns=True, + ) + + +def test_validate_url_strict_ssrf_allows_public(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + return [(None, None, None, None, ("1.1.1.1", 0))] + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + url = Validators.validate_url( + "https://example.com", + allow_private_networks=False, + resolve_dns=True, + ) + assert url.startswith("https://example.com") + + +def test_validate_url_strict_ssrf_rebinding_guard(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + return [ + (None, None, None, None, ("1.1.1.1", 0)), + (None, None, None, None, ("10.0.0.9", 0)), + ] + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url( + "https://example.com", + allow_private_networks=False, + resolve_dns=True, + ) + + +def test_validate_url_strict_ssrf_blocks_private_ipv6(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + return [(None, None, None, None, ("fd00::1", 0, 0, 0))] + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url( + "https://example.com", + allow_private_networks=False, + resolve_dns=True, + ) + + +def test_validate_url_strict_ssrf_allows_public_ipv6(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + return [(None, None, None, None, ("2606:4700:4700::1111", 0, 0, 0))] + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + url = Validators.validate_url( + "https://example.com", + allow_private_networks=False, + resolve_dns=True, + ) + assert url.startswith("https://example.com") + + +def test_validate_url_strict_ssrf_blocks_mixed_ipv6(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + return [ + (None, None, None, None, ("2606:4700:4700::1111", 0, 0, 0)), + (None, None, None, None, ("fd00::2", 0, 0, 0)), + ] + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url( + "https://example.com", + allow_private_networks=False, + resolve_dns=True, + ) + + +def test_validate_url_strict_ssrf_resolution_error(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + raise common_types.socket.gaierror("boom") + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url( + "https://example.com", + allow_private_networks=False, + resolve_dns=True, + ) + + +def test_validate_url_blocks_localhost_when_private_disallowed(): + with pytest.raises(ValueError, match=r".*"): + Validators.validate_url( + "http://localhost:1234", + allow_private_networks=False, + resolve_dns=False, + ) + + +def test_is_blocked_hostname_uncached_blocks_private(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + return [(None, None, None, None, ("10.0.0.8", 0))] + + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + assert SSRFProtection.is_blocked_hostname_uncached("example.com") is True + + +def test_is_blocked_hostname_uncached_allows_localhost(): + assert SSRFProtection.is_blocked_hostname_uncached("localhost") is False + + +def test_resolve_hostname_uncached_resolution_error(monkeypatch): + def fake_getaddrinfo( + _host: str, *_args: object, **_kwargs: object + ) -> list[AddrInfo]: + raise common_types.socket.gaierror("boom") + + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + with pytest.raises(ValueError, match=r".*"): + SSRFProtection.is_blocked_hostname_uncached("example.com") + + +def test_validate_non_negative_and_since(): + assert Validators.validate_non_negative(10, "limit") == 10 + assert Validators.validate_non_negative(DataLimit(bytes=5), "limit") == 5 + + now_iso = datetime.now(timezone.utc).isoformat() + assert Validators.validate_since(now_iso) == now_iso + assert Validators.validate_since("1h") == "1h" + with pytest.raises(ValueError, match=r".*"): + Validators.validate_since("") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_since("not-a-time") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_non_negative(-1, "limit") + + +def test_build_config_overrides_and_json_serializable(): + overrides = build_config_overrides(timeout=10, enable_logging=True, foo=1) + assert overrides["timeout"] == 10 + assert overrides["enable_logging"] is True + assert "foo" not in overrides + + merged = merge_config_kwargs({"a": 1}, {"timeout": 5}) + assert merged["a"] == 1 + assert merged["timeout"] == 5 + + assert is_json_serializable({"a": 1, "b": [1, 2]}) is True + assert is_json_serializable({"t": time.time(), "f": object()}) is False + + +def test_validate_snapshot_size_ok(): + validate_snapshot_size({"a": 1, "b": {"c": 2}}) + + +def test_sanitize_url_and_endpoint(): + url = "https://example.com/secret/path" + assert Validators.sanitize_url_for_logging(url) == "https://example.com/***" + assert Validators.sanitize_endpoint_for_logging("") == "***EMPTY***" + assert Validators.sanitize_endpoint_for_logging("a" * 30) == "***" + + +def test_validate_snapshot_size_error(monkeypatch): + def fake_getsizeof(_: object) -> int: + return (Constants.MAX_SNAPSHOT_SIZE_MB * 1024 * 1024) + 1 + + monkeypatch.setattr("sys.getsizeof", fake_getsizeof) + with pytest.raises(ValueError, match=r".*"): + validate_snapshot_size({"data": "x"}) + + +def test_mask_sensitive_depth_limit() -> None: + nested: dict[str, object] = {} + current = nested + for _ in range(Constants.MAX_RECURSION_DEPTH + 2): + new: dict[str, object] = {} + current["child"] = new + current = new + masked = mask_sensitive_data(nested) + # Walk down to find the depth error marker + walker: object = masked + found_error = False + for _ in range(Constants.MAX_RECURSION_DEPTH + 3): + if isinstance(walker, dict) and "_error" in walker: + found_error = True + break + walker = walker.get("child", {}) if isinstance(walker, dict) else {} + assert found_error is True + + +def test_secure_id_generator(): + cid = SecureIDGenerator.generate_correlation_id() + assert isinstance(cid, str) + assert len(cid) >= 16 + + token = SecureIDGenerator.generate_request_id() + assert isinstance(token, str) + assert len(token) >= 16 + + +def test_validate_string_not_empty(): + assert Validators.validate_string_not_empty("x", "field") == "x" + with pytest.raises(ValueError, match=r".*"): + Validators.validate_string_not_empty("", "field") + + +def test_validate_key_id_and_sanitize_url_error(): + assert Validators.validate_key_id("key-1") == "key-1" + with pytest.raises(ValueError, match=r".*"): + Validators.validate_key_id("bad id") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_key_id("bad/../id") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_key_id("bad\x00id") + with pytest.raises(ValueError, match=r".*"): + Validators.validate_key_id("a" * (Constants.MAX_KEY_ID_LENGTH + 1)) + + assert Validators.sanitize_url_for_logging("::bad") == ":///***" + + +def test_sanitize_url_for_logging_exception(monkeypatch): + def boom(_url: str) -> None: + raise ValueError("boom") + + monkeypatch.setattr("pyoutlineapi.common_types.urlparse", boom) + assert ( + Validators.sanitize_url_for_logging("http://example.com") == "***INVALID_URL***" + ) diff --git a/tests/test_common_types_extra.py b/tests/test_common_types_extra.py new file mode 100644 index 0000000..a04c7e6 --- /dev/null +++ b/tests/test_common_types_extra.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from typing import cast + +import pytest + +from pyoutlineapi import common_types +from pyoutlineapi.common_types import ( + Constants, + SSRFProtection, + Validators, + mask_sensitive_data, + validate_snapshot_size, +) + +MASKED_VALUE = "***MASKED***" +AddrInfo = tuple[object, object, object, object, tuple[str, int]] + + +def test_validate_since_accepts_relative_and_iso(): + assert Validators.validate_since("24h") == "24h" + assert Validators.validate_since("2024-01-01T00:00:00Z") == "2024-01-01T00:00:00Z" + with pytest.raises(ValueError, match=r".*"): + Validators.validate_since("invalid") + + +def test_validate_key_id_invalid_characters(): + assert Validators.validate_key_id("key_123") == "key_123" + with pytest.raises(ValueError, match=r".*"): + Validators.validate_key_id("bad%2Fkey") + + +def test_sanitize_url_for_logging_invalid(monkeypatch): + def bad_parse(_url: str) -> object: + raise ValueError("bad") + + monkeypatch.setattr(common_types, "urlparse", bad_parse) + assert ( + Validators.sanitize_url_for_logging("http://example.com") == "***INVALID_URL***" + ) + + +def test_sanitize_endpoint_for_logging_empty(): + assert Validators.sanitize_endpoint_for_logging("") == "***EMPTY***" + + +def test_mask_sensitive_data_max_depth() -> None: + data: dict[str, object] = {} + current = data + for _ in range(Constants.MAX_RECURSION_DEPTH + 2): + current["next"] = {} + current = cast(dict[str, object], current["next"]) + + masked = mask_sensitive_data(data) + # Walk down until error marker appears + cursor: object = masked + found = False + while isinstance(cursor, dict) and "next" in cursor: + cursor = cursor["next"] + if ( + isinstance(cursor, dict) + and cursor.get("_error") == "Max recursion depth exceeded" + ): + found = True + break + assert found is True + + +def test_mask_sensitive_data_list_without_dicts(): + data = {"items": ["a", 1, None]} + masked = mask_sensitive_data(data) + assert masked["items"] == ["a", 1, None] + + +def test_mask_sensitive_data_list_modification(): + data = {"items": [{"token": "x"}], "plain": 1} + masked = mask_sensitive_data(data) + assert masked["items"][0]["token"] == MASKED_VALUE + + +def test_mask_sensitive_data_top_level_sensitive_key(): + data = {"password": "secret", "nested": {"ok": 1}} + masked = mask_sensitive_data(data) + assert masked["password"] == MASKED_VALUE + + +def test_mask_sensitive_data_nested_copy(): + data = {"nested": {"ok": 1}} + masked = mask_sensitive_data(data) + assert masked["nested"]["ok"] == 1 + + +def test_validate_snapshot_size_limit(monkeypatch): + monkeypatch.setattr(Constants, "MAX_SNAPSHOT_SIZE_MB", 0) + with pytest.raises(ValueError, match=r".*"): + validate_snapshot_size({"x": "y"}) + + +def test_resolve_hostname_invalid_entries(monkeypatch): + def fake_getaddrinfo( + _host: str, + *_args: object, + **_kwargs: object, + ) -> list[AddrInfo]: + return [(None, None, None, None, ("not-an-ip", 0))] + + SSRFProtection._resolve_hostname.cache_clear() + monkeypatch.setattr(common_types.socket, "getaddrinfo", fake_getaddrinfo) + with pytest.raises(ValueError, match=r".*"): + SSRFProtection._resolve_hostname("example.com") + + with pytest.raises(ValueError, match=r".*"): + SSRFProtection._resolve_hostname_uncached("example.com") + + +def test_is_blocked_hostname_allows_localhost(): + assert SSRFProtection.is_blocked_hostname("localhost") is False + assert SSRFProtection.is_blocked_hostname_uncached("localhost") is False + + +def test_is_blocked_hostname_blocks_private(monkeypatch): + import ipaddress + + def fake_resolve( + _host: str, + ) -> tuple[ipaddress.IPv4Address | ipaddress.IPv6Address, ...]: + return (ipaddress.ip_address("10.0.0.1"),) + + monkeypatch.setattr(SSRFProtection, "_resolve_hostname", fake_resolve) + assert SSRFProtection.is_blocked_hostname("example.com") is True + + +def test_is_blocked_hostname_uncached_blocks(monkeypatch): + import ipaddress + + def fake_resolve( + _host: str, + ) -> tuple[ipaddress.IPv4Address | ipaddress.IPv6Address, ...]: + return (ipaddress.ip_address("10.0.0.1"),) + + monkeypatch.setattr(SSRFProtection, "_resolve_hostname_uncached", fake_resolve) + assert SSRFProtection.is_blocked_hostname_uncached("example.com") is True + + +def test_is_blocked_hostname_uncached_allows_public(monkeypatch): + import ipaddress + + def fake_resolve( + _host: str, + ) -> tuple[ipaddress.IPv4Address | ipaddress.IPv6Address, ...]: + return (ipaddress.ip_address("1.1.1.1"),) + + monkeypatch.setattr(SSRFProtection, "_resolve_hostname_uncached", fake_resolve) + assert SSRFProtection.is_blocked_hostname_uncached("example.com") is False diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..01188b1 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import logging +from pathlib import Path + +import pytest +from pydantic import SecretStr + +from pyoutlineapi.config import ( + DevelopmentConfig, + OutlineClientConfig, + ProductionConfig, + create_env_template, + load_config, +) +from pyoutlineapi.exceptions import ConfigurationError + + +def _write_env(path: Path) -> None: + path.write_text( + """ +OUTLINE_API_URL=https://example.com/secret +OUTLINE_CERT_SHA256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +OUTLINE_TIMEOUT=15 +""".strip(), + encoding="utf-8", + ) + + +def test_from_env_and_sanitized(tmp_path: Path): + env_file = tmp_path / ".env" + _write_env(env_file) + config = OutlineClientConfig.from_env(env_file=env_file) + assert config.api_url.startswith("https://example.com") + assert config.timeout == 15 + sanitized = config.get_sanitized_config + assert sanitized["cert_sha256"] == "***MASKED***" + assert "secret" not in str(sanitized["api_url"]) + + +def test_from_env_str_path(tmp_path: Path): + env_file = tmp_path / ".env" + _write_env(env_file) + config = OutlineClientConfig.from_env(env_file=str(env_file)) + assert config.api_url.startswith("https://example.com") + + +def test_create_minimal(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + timeout=20, + ) + assert config.timeout == 20 + assert isinstance(config.cert_sha256, SecretStr) + + with pytest.raises(TypeError): + OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256=123, + ) + + config2 = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + ) + assert isinstance(config2.cert_sha256, SecretStr) + + +def test_load_config_variants(monkeypatch): + monkeypatch.setenv("DEV_OUTLINE_API_URL", "https://example.com/secret") + monkeypatch.setenv("DEV_OUTLINE_CERT_SHA256", "a" * 64) + monkeypatch.setenv("PROD_OUTLINE_API_URL", "https://example.com/secret") + monkeypatch.setenv("PROD_OUTLINE_CERT_SHA256", "a" * 64) + config = load_config( + "development", + timeout=11, + ) + assert isinstance(config, DevelopmentConfig) + config = load_config( + "production", + timeout=12, + ) + assert isinstance(config, ProductionConfig) + + with pytest.raises(ValueError, match=r".*"): + load_config("nope") + + monkeypatch.setenv("OUTLINE_API_URL", "https://example.com/secret") + monkeypatch.setenv("OUTLINE_CERT_SHA256", "a" * 64) + config = load_config("custom") + assert isinstance(config, OutlineClientConfig) + + +def test_load_config_unknown_env_branch(monkeypatch): + from pyoutlineapi import config as config_module + + monkeypatch.setenv("OUTLINE_API_URL", "https://example.com/secret") + monkeypatch.setenv("OUTLINE_CERT_SHA256", "a" * 64) + monkeypatch.setattr( + config_module, + "_VALID_ENVIRONMENTS", + frozenset({"development", "production", "custom", "staging"}), + ) + config = load_config("staging") + assert isinstance(config, OutlineClientConfig) + + +def test_create_env_template(tmp_path: Path): + target = tmp_path / "template.env" + create_env_template(target) + assert target.exists() + content = target.read_text(encoding="utf-8") + assert "OUTLINE_API_URL" in content + + target2 = tmp_path / "template2.env" + create_env_template(str(target2)) + assert target2.exists() + + +def test_create_env_template_invalid_path(): + with pytest.raises(TypeError): + create_env_template(123) + + +def test_model_copy_and_circuit_config(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + enable_circuit_breaker=True, + circuit_failure_threshold=2, + ) + copied = config.model_copy_immutable(timeout=12) + assert copied.timeout == 12 + assert copied.circuit_config is not None + + with pytest.raises(ValueError, match=r".*"): + config.model_copy_immutable(bad_key=1) + + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + enable_circuit_breaker=False, + ) + assert config.circuit_config is None + + +def test_cert_sha_assignment_guard(): + config = OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + ) + with pytest.raises(TypeError): + config.cert_sha256 = "bad" + config.cert_sha256 = SecretStr("a" * 64) + + +def test_user_agent_control_chars(): + with pytest.raises(ValueError, match=r".*"): + OutlineClientConfig.create_minimal( + api_url="https://example.com/secret", + cert_sha256="a" * 64, + user_agent="bad\u0001", + ) + + +def test_validate_config_http_warning_and_circuit_adjust(caplog): + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.config"): + config = OutlineClientConfig.create_minimal( + api_url="http://example.com/secret", + cert_sha256="a" * 64, + enable_circuit_breaker=True, + circuit_call_timeout=0.1, + ) + assert config.circuit_call_timeout >= config._get_max_request_time() + assert any("Using HTTP" in r.message for r in caplog.records) + + +def test_from_env_errors(tmp_path: Path, monkeypatch): + with pytest.raises(ConfigurationError): + OutlineClientConfig.from_env(env_file=tmp_path / "missing.env") + + with pytest.raises(TypeError): + OutlineClientConfig.from_env(env_file=123) # type: ignore[arg-type] + + # env_file None uses environment + monkeypatch.setenv("OUTLINE_API_URL", "https://example.com/secret") + monkeypatch.setenv("OUTLINE_CERT_SHA256", "a" * 64) + config = OutlineClientConfig.from_env() + assert isinstance(config, OutlineClientConfig) + + +def test_production_config_security(monkeypatch): + monkeypatch.setenv("PROD_OUTLINE_API_URL", "http://example.com/secret") + monkeypatch.setenv("PROD_OUTLINE_CERT_SHA256", "a" * 64) + with pytest.raises(ConfigurationError): + load_config("production") + + monkeypatch.setenv("PROD_OUTLINE_API_URL", "https://example.com/secret") + monkeypatch.setenv("PROD_OUTLINE_CERT_SHA256", "a" * 64) + config = ProductionConfig( + api_url="https://example.com/secret", + cert_sha256=SecretStr("a" * 64), + enable_circuit_breaker=False, + ) + assert config.enable_circuit_breaker is False + assert config.allow_private_networks is False + assert config.resolve_dns_for_ssrf is True diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 3b99c13..021dee1 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,531 +1,171 @@ -""" -Tests for PyOutlineAPI exceptions module. - -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. - -Copyright (c) 2025 Denis Rozhnovskiy -All rights reserved. - -This software is licensed under the MIT License. -You can find the full license text at: - https://opensource.org/licenses/MIT - -Source code repository: - https://github.com/orenlab/pyoutlineapi -""" +from __future__ import annotations import pytest -# Import the exceptions to test -from pyoutlineapi import OutlineError, APIError - - -class TestOutlineError: - """Test cases for OutlineError base exception class.""" - - def test_outline_error_is_exception(self): - """Test that OutlineError inherits from Exception.""" - assert issubclass(OutlineError, Exception) - - def test_outline_error_creation_without_message(self): - """Test creating OutlineError without message.""" - error = OutlineError() - assert isinstance(error, OutlineError) - assert isinstance(error, Exception) +from pyoutlineapi.exceptions import ( + APIError, + CircuitOpenError, + ConfigurationError, + OutlineConnectionError, + OutlineError, + OutlineTimeoutError, + ValidationError, + format_error_chain, + get_retry_delay, + get_safe_error_dict, + is_retryable, +) - def test_outline_error_creation_with_message(self): - """Test creating OutlineError with message.""" - message = "Test error message" - error = OutlineError(message) - assert str(error) == message - assert error.args == (message,) +PLACEHOLDER_VALUE = "x" +UPDATED_VALUE = "changed" - def test_outline_error_creation_with_empty_message(self): - """Test creating OutlineError with empty message.""" - error = OutlineError("") - assert str(error) == "" - assert error.args == ("",) - def test_outline_error_creation_with_none_message(self): - """Test creating OutlineError with None message.""" - error = OutlineError(None) - assert str(error) == "None" - assert error.args == (None,) - - def test_outline_error_multiple_args(self): - """Test creating OutlineError with multiple arguments.""" - arg1, arg2, arg3 = "arg1", 123, {"key": "value"} - error = OutlineError(arg1, arg2, arg3) - assert error.args == (arg1, arg2, arg3) +def test_outline_error_sanitizes_and_truncates_message(): + message = "password=supersecret " + ("a" * 2000) + err = OutlineError(message, safe_details={"field": "x"}) + text = str(err) + assert "***PASSWORD***" in text + assert "..." in text + assert "field=x" in text - def test_outline_error_inheritance_chain(self): - """Test that OutlineError maintains proper inheritance.""" - error = OutlineError("test") - assert isinstance(error, OutlineError) - assert isinstance(error, Exception) - assert isinstance(error, BaseException) - - def test_outline_error_can_be_raised(self): - """Test that OutlineError can be raised and caught.""" - with pytest.raises(OutlineError) as exc_info: - raise OutlineError("Test exception") - assert str(exc_info.value) == "Test exception" - assert isinstance(exc_info.value, OutlineError) - - def test_outline_error_can_be_caught_as_exception(self): - """Test that OutlineError can be caught as generic Exception.""" - with pytest.raises(Exception) as exc_info: - raise OutlineError("Test exception") - - assert isinstance(exc_info.value, OutlineError) - - -class TestAPIError: - """Test cases for APIError exception class.""" - - def test_api_error_is_outline_error(self): - """Test that APIError inherits from OutlineError.""" - assert issubclass(APIError, OutlineError) - assert issubclass(APIError, Exception) - - def test_api_error_creation_with_message_only(self): - """Test creating APIError with only message parameter.""" - message = "API request failed" - error = APIError(message) - - assert str(error) == message - assert error.args == (message,) - assert error.status_code is None - assert error.attempt is None - - def test_api_error_creation_with_all_parameters(self): - """Test creating APIError with all parameters.""" - message = "API request failed" - status_code = 404 - attempt = 3 - - error = APIError(message, status_code, attempt) - - assert error.args == (message,) - assert error.status_code == status_code - assert error.attempt == attempt - - def test_api_error_creation_with_status_code_only(self): - """Test creating APIError with message and status_code.""" - message = "Not found" - status_code = 404 - - error = APIError(message, status_code=status_code) - - assert str(error) == message - assert error.status_code == status_code - assert error.attempt is None - - def test_api_error_creation_with_attempt_only(self): - """Test creating APIError with message and attempt.""" - message = "Connection timeout" - attempt = 2 - - error = APIError(message, attempt=attempt) - - assert error.status_code is None - assert error.attempt == attempt - - def test_api_error_str_without_attempt(self): - """Test __str__ method when attempt is None.""" - message = "API error" - error = APIError(message, status_code=500) - - assert str(error) == message - - def test_api_error_str_with_attempt(self): - """Test __str__ method when attempt is provided.""" - message = "Connection failed" - attempt = 3 - error = APIError(message, attempt=attempt) - - expected = f"[Attempt {attempt}] {message}" - assert str(error) == expected - - def test_api_error_str_with_zero_attempt(self): - """Test __str__ method when attempt is 0.""" - message = "First attempt failed" - attempt = 0 - error = APIError(message, attempt=attempt) - - expected = f"[Attempt {attempt}] {message}" - assert str(error) == expected - - def test_api_error_str_with_negative_attempt(self): - """Test __str__ method when attempt is negative (edge case).""" - message = "Invalid attempt" - attempt = -1 - error = APIError(message, attempt=attempt) - - expected = f"[Attempt {attempt}] {message}" - assert str(error) == expected - - def test_api_error_parameters_are_optional(self): - """Test that status_code and attempt parameters are truly optional.""" - message = "Required message" - - # Test with explicit None values - error1 = APIError(message, None, None) - assert error1.status_code is None - assert error1.attempt is None - - # Test with keyword arguments as None - error2 = APIError(message, status_code=None, attempt=None) - assert error2.status_code is None - assert error2.attempt is None - - def test_api_error_with_different_status_codes(self): - """Test APIError with various HTTP status codes.""" - test_cases = [ - (200, "OK"), - (400, "Bad Request"), - (401, "Unauthorized"), - (403, "Forbidden"), - (404, "Not Found"), - (500, "Internal Server Error"), - (502, "Bad Gateway"), - (503, "Service Unavailable"), - ] - - for status_code, message in test_cases: - error = APIError(message, status_code=status_code) - assert error.status_code == status_code - assert str(error) == message - - def test_api_error_with_different_attempt_values(self): - """Test APIError with various attempt values.""" - message = "Retry attempt" - test_attempts = [1, 2, 5, 10, 100] - - for attempt in test_attempts: - error = APIError(message, attempt=attempt) - assert error.attempt == attempt - expected_str = f"[Attempt {attempt}] {message}" - assert str(error) == expected_str - - def test_api_error_inheritance_chain(self): - """Test that APIError maintains proper inheritance chain.""" - error = APIError("test") - assert isinstance(error, APIError) - assert isinstance(error, OutlineError) - assert isinstance(error, Exception) - assert isinstance(error, BaseException) - - def test_api_error_can_be_raised(self): - """Test that APIError can be raised and caught.""" - message = "API failure" - status_code = 500 - attempt = 2 - - with pytest.raises(APIError) as exc_info: - raise APIError(message, status_code, attempt) - - error = exc_info.value - assert str(error) == f"[Attempt {attempt}] {message}" - assert error.status_code == status_code - assert error.attempt == attempt - - def test_api_error_can_be_caught_as_outline_error(self): - """Test that APIError can be caught as OutlineError.""" - with pytest.raises(OutlineError) as exc_info: - raise APIError("Test API error") - - assert isinstance(exc_info.value, APIError) - - def test_api_error_can_be_caught_as_exception(self): - """Test that APIError can be caught as generic Exception.""" - with pytest.raises(Exception) as exc_info: - raise APIError("Test API error") - - assert isinstance(exc_info.value, APIError) - - def test_api_error_attributes_are_accessible(self): - """Test that all APIError attributes are accessible.""" - message = "Test message" - status_code = 418 # I'm a teapot - attempt = 7 - - error = APIError(message, status_code, attempt) - - # Test attribute access - assert hasattr(error, 'status_code') - assert hasattr(error, 'attempt') - assert hasattr(error, 'args') - - # Test attribute values - assert error.status_code == status_code - assert error.attempt == attempt - assert error.args == (message,) - - def test_api_error_with_empty_message(self): - """Test APIError with empty message.""" - error = APIError("", status_code=200, attempt=1) - assert str(error) == "[Attempt 1] " - assert error.status_code == 200 - - def test_api_error_with_complex_message(self): - """Test APIError with complex message containing special characters.""" - message = "API error: Connection failed!\nDetails: timeout after 30s\n→ Check network" - attempt = 3 - - error = APIError(message, attempt=attempt) - expected = f"[Attempt {attempt}] {message}" - assert str(error) == expected - - def test_api_error_super_call_behavior(self): - """Test that APIError properly calls parent __init__ and __str__.""" - message = "Super test" - error = APIError(message) - - # Verify that parent Exception.__init__ was called - assert error.args == (message,) - - # Verify that when attempt is None, parent __str__ is used - assert str(error) == message - - def test_api_error_type_annotations(self): - """Test that APIError accepts proper types according to annotations.""" - # Test with proper types - error1 = APIError("message", 200, 1) - assert isinstance(error1.status_code, int) - assert isinstance(error1.attempt, int) - - # Test with None values (Optional types) - error2 = APIError("message", None, None) - assert error2.status_code is None - assert error2.attempt is None - - -class TestExceptionInteraction: - """Test cases for exception interaction and edge cases.""" - - def test_exception_hierarchy_catching(self): - """Test catching exceptions at different levels of hierarchy.""" - - # Test catching APIError as OutlineError - try: - raise APIError("API failed", 500, 2) - except OutlineError as e: - assert isinstance(e, APIError) - assert e.status_code == 500 - assert e.attempt == 2 - - # Test catching OutlineError as Exception - try: - raise OutlineError("Outline failed") - except Exception as e: - assert isinstance(e, OutlineError) +def test_outline_error_non_string_message_and_details_copy(): + err = OutlineError( + 123, + details={"secret": PLACEHOLDER_VALUE}, + safe_details={"safe": "y"}, + ) + assert "123" in str(err) + details = err.details + details["secret"] = UPDATED_VALUE + assert err.details["secret"] == PLACEHOLDER_VALUE + safe = err.safe_details + safe["safe"] = "changed" + assert err.safe_details["safe"] == "y" - def test_multiple_exception_types_in_single_try_block(self): - """Test handling multiple exception types.""" - def raise_outline_error(): - raise OutlineError("Base error") +def test_outline_error_details_properties(): + err = OutlineError("oops") + assert err.details == {} + assert err.safe_details == {} - def raise_api_error(): - raise APIError("API error", 404, 1) - # Test catching specific types - with pytest.raises(OutlineError): - raise_outline_error() - - with pytest.raises(APIError): - raise_api_error() - - # Test catching both as OutlineError - for func in [raise_outline_error, raise_api_error]: - with pytest.raises(OutlineError): - func() - - def test_exception_chaining(self): - """Test exception chaining (raise from).""" - original_error = ValueError("Original error") - - with pytest.raises(APIError) as exc_info: - try: - raise original_error - except ValueError as e: - raise APIError("API wrapper error", 500, 1) from e +def test_outline_error_repr(): + err = OutlineError("oops") + assert repr(err) == "OutlineError('oops')" - api_error = exc_info.value - assert api_error.__cause__ is original_error - assert str(api_error) == "[Attempt 1] API wrapper error" - def test_exception_context_preservation(self): - """Test that exception context is preserved.""" +def test_api_error_properties_and_retryable(): + err = APIError("fail", status_code=503, endpoint="/server") + assert err.is_retryable is True + assert err.is_server_error is True + assert err.is_client_error is False + assert err.is_rate_limit_error is False - def inner_function(): - raise OutlineError("Inner error") + err_no_status = APIError("fail") + assert err_no_status.is_retryable is False - def outer_function(): - try: - inner_function() - except OutlineError: - raise APIError("Outer error", 500, 2) - with pytest.raises(APIError) as exc_info: - outer_function() +def test_circuit_open_error_validation_and_delay(): + err = CircuitOpenError("open", retry_after=12.3) + assert err.is_retryable is True + assert err.default_retry_delay == 12.3 + with pytest.raises(ValueError, match=r".*"): + CircuitOpenError("open", retry_after=-1) - api_error = exc_info.value - assert str(api_error) == "[Attempt 2] Outer error" - assert api_error.__context__ is not None - assert isinstance(api_error.__context__, OutlineError) +def test_configuration_and_validation_errors(): + config_err = ConfigurationError("bad", field="api_url", security_issue=True) + assert config_err.safe_details["field"] == "api_url" + assert config_err.safe_details["security_issue"] is True -class TestExceptionEdgeCases: - """Test edge cases and unusual scenarios.""" + val_err = ValidationError("bad", field="port", model="Server") + assert val_err.safe_details["field"] == "port" + assert val_err.safe_details["model"] == "Server" - def test_api_error_with_very_large_numbers(self): - """Test APIError with very large status codes and attempts.""" - large_status = 999999 - large_attempt = 1000000 + config_err2 = ConfigurationError("bad", field="only") + assert config_err2.safe_details["field"] == "only" + val_err2 = ValidationError("bad", model="OnlyModel") + assert val_err2.safe_details["model"] == "OnlyModel" - error = APIError("Large numbers", large_status, large_attempt) - assert error.status_code == large_status - assert error.attempt == large_attempt - assert str(error) == f"[Attempt {large_attempt}] Large numbers" - - def test_api_error_with_unicode_message(self): - """Test APIError with Unicode characters in message.""" - unicode_message = "API错误: 连接失败 🔥 → 请检查网络" - error = APIError(unicode_message, 500, 1) + config_err3 = ConfigurationError("bad", security_issue=True) + assert config_err3.safe_details["security_issue"] is True - assert str(error) == f"[Attempt 1] {unicode_message}" - assert error.args == (unicode_message,) + val_err3 = ValidationError("bad", field="field-only") + assert val_err3.safe_details["field"] == "field-only" - def test_exception_pickle_serialization(self): - """Test that exceptions can be pickled and unpickled.""" - import pickle - # Test OutlineError - outline_error = OutlineError("Pickle test") - pickled = pickle.dumps(outline_error) - unpickled = pickle.loads(pickled) +def test_connection_and_timeout_errors(): + conn_err = OutlineConnectionError("down", host="example.com", port=443) + assert conn_err.is_retryable is True + assert conn_err.safe_details["host"] == "example.com" + assert conn_err.safe_details["port"] == 443 - assert isinstance(unpickled, OutlineError) - assert str(unpickled) == "Pickle test" + conn_err2 = OutlineConnectionError("down") + assert conn_err2.safe_details == {} - # Test APIError - api_error = APIError("API pickle test", 404, 3) - pickled = pickle.dumps(api_error) - unpickled = pickle.loads(pickled) + conn_err3 = OutlineConnectionError("down", port=80) + assert conn_err3.safe_details["port"] == 80 - assert isinstance(unpickled, APIError) - assert str(unpickled) == "[Attempt 3] API pickle test" - assert unpickled.status_code == 404 - assert unpickled.attempt == 3 + timeout_err = OutlineTimeoutError("slow", timeout=1.23, operation="get") + assert timeout_err.is_retryable is True + assert timeout_err.safe_details["timeout"] == 1.23 + assert timeout_err.safe_details["operation"] == "get" - def test_exception_repr_methods(self): - """Test __repr__ methods of exceptions.""" + timeout_err2 = OutlineTimeoutError("slow") + assert timeout_err2.safe_details == {} - # OutlineError - outline_error = OutlineError("Test repr") - repr_str = repr(outline_error) - assert "OutlineError" in repr_str - assert "Test repr" in repr_str + timeout_err3 = OutlineTimeoutError("slow", operation="op") + assert timeout_err3.safe_details["operation"] == "op" - # APIError - api_error = APIError("API repr test", 500, 2) - repr_str = repr(api_error) - assert "APIError" in repr_str - assert "API repr test" in repr_str - - def test_exception_equality_and_hashing(self): - """Test exception equality and hashing behavior.""" - - # Test OutlineError equality - error1 = OutlineError("Same message") - error2 = OutlineError("Same message") - error3 = OutlineError("Different message") - - # Exceptions with same args should be equal - assert error1.args == error2.args - assert error1.args != error3.args - - # Test APIError equality - api1 = APIError("Same", 200, 1) - api2 = APIError("Same", 200, 1) - api3 = APIError("Same", 404, 1) - assert api1.args == api2.args - assert api1.status_code == api2.status_code - assert api1.attempt == api2.attempt +def test_retry_helpers_and_safe_error_dict(): + api_err = APIError("fail", status_code=400) + assert is_retryable(api_err) is False + assert get_retry_delay(api_err) is None - assert api1.status_code != api3.status_code + not_outline = ValueError("x") + assert is_retryable(not_outline) is False + assert get_retry_delay(not_outline) is None - def test_memory_efficiency(self): - """Test that exceptions don't consume excessive memory.""" + data = get_safe_error_dict(api_err) + assert data["type"] == "APIError" + assert data["status_code"] == 400 + assert data["is_client_error"] is True - # Create many exceptions and ensure they don't consume too much memory - exceptions = [] - for i in range(1000): - exceptions.append(APIError(f"Error {i}", i % 600, i % 10)) + data2 = get_safe_error_dict(APIError("fail")) + assert "is_client_error" not in data2 + assert "is_server_error" not in data2 - # Basic check that all exceptions were created - assert len(exceptions) == 1000 - assert all(isinstance(e, APIError) for e in exceptions) + data3 = get_safe_error_dict(OutlineConnectionError("down", host="h", port=1)) + assert data3["host"] == "h" + assert data3["port"] == 1 - # Check that they have expected properties - assert exceptions[500].status_code == 500 % 600 - assert exceptions[500].attempt == 500 % 10 + timeout_err = OutlineTimeoutError("slow") + assert get_retry_delay(timeout_err) == timeout_err.default_retry_delay + circuit_err = CircuitOpenError("open", retry_after=5.0) + data4 = get_safe_error_dict(circuit_err) + assert data4["retry_after"] == 5.0 -# Integration tests -class TestExceptionIntegration: - """Integration tests simulating real-world usage scenarios.""" - - def test_error_logging_scenario(self): - """Test simulating error logging with exceptions.""" - import logging - from io import StringIO - - # Setup logging capture - log_capture = StringIO() - handler = logging.StreamHandler(log_capture) - logger = logging.getLogger("test_logger") - logger.addHandler(handler) - logger.setLevel(logging.ERROR) + config_err = ConfigurationError("bad", security_issue=False) + data5 = get_safe_error_dict(config_err) + assert data5["security_issue"] is False - try: - raise APIError("Critical API failure", 500, 5) - except APIError as e: - logger.error("API Error occurred: %s (Status: %s, Attempt: %s)", - str(e), e.status_code, e.attempt) + validation_err = ValidationError("bad") + data6 = get_safe_error_dict(validation_err) + assert "field" not in data6 - log_output = log_capture.getvalue() - assert "Critical API failure" in log_output - assert "Status: 500" in log_output - assert "Attempt: 5" in log_output + data7 = get_safe_error_dict(OutlineTimeoutError("slow", timeout=2.0)) + assert data7["timeout"] == 2.0 - # Cleanup - logger.removeHandler(handler) - def test_exception_in_async_context(self): - """Test that exceptions work properly in async context simulation.""" - import asyncio - - async def async_api_call(): - raise APIError("Async API failed", 408, 1) - - async def test_async(): - with pytest.raises(APIError) as exc_info: - await async_api_call() - - error = exc_info.value - assert error.status_code == 408 - assert error.attempt == 1 - assert "Async API failed" in str(error) - - # Run the async test - asyncio.run(test_async()) - - -if __name__ == "__main__": - # Run tests if script is executed directly - pytest.main([__file__, "-v", "--tb=short"]) +def test_format_error_chain_uses_cause_or_context(): + try: + try: + raise KeyError("root") + except KeyError as err: + raise ValueError("child") from err + except Exception as exc: + chain = format_error_chain(exc) + assert len(chain) == 2 diff --git a/tests/test_health_monitoring.py b/tests/test_health_monitoring.py new file mode 100644 index 0000000..7613855 --- /dev/null +++ b/tests/test_health_monitoring.py @@ -0,0 +1,345 @@ +from __future__ import annotations + +import logging +import time +from collections.abc import Callable, Coroutine +from typing import cast + +import pytest + +from pyoutlineapi.client import AsyncOutlineClient +from pyoutlineapi.health_monitoring import ( + HealthCheckHelper, + HealthMonitor, + HealthStatus, + PerformanceMetrics, +) + + +def _as_client(client: object) -> AsyncOutlineClient: + return cast(AsyncOutlineClient, client) + + +class DummyClient: + async def get_server_info(self): + return {"name": "ok"} + + def get_circuit_metrics(self): + return None + + +class FailingServerClient(DummyClient): + async def get_server_info(self): + raise RuntimeError("fail") + + +@pytest.mark.asyncio +async def test_health_monitor_check_and_cache(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + result = await monitor.check() + assert result.healthy is True + cached = await monitor.check() + assert cached is result + + +@pytest.mark.asyncio +async def test_health_monitor_check_returns_cached_result(): + monitor = HealthMonitor(_as_client(FailingServerClient()), cache_ttl=10.0) + cached = HealthStatus(healthy=True, timestamp=1.0, checks={}, metrics={}) + monitor._cached_result = cached + monitor._last_check_time = time.monotonic() + assert await monitor.check() is cached + + +@pytest.mark.asyncio +async def test_health_monitor_quick_check_returns_cached(monkeypatch): + monitor = HealthMonitor(_as_client(FailingServerClient()), cache_ttl=10.0) + monitor._cached_result = HealthStatus( + healthy=True, timestamp=1.0, checks={}, metrics={} + ) + monitor._last_check_time = time.monotonic() + assert await monitor.quick_check() is True + + +@pytest.mark.asyncio +async def test_health_monitor_quick_check(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + assert await monitor.quick_check() is True + + +def test_health_monitor_invalid_cache_ttl(): + with pytest.raises(ValueError, match=r".*"): + HealthMonitor(_as_client(DummyClient()), cache_ttl=0.0) + + +class CircuitClient(DummyClient): + def get_circuit_metrics(self): + return {"state": "OPEN", "success_rate": 0.2} + + +@pytest.mark.asyncio +async def test_health_monitor_custom_check_and_circuit(): + monitor = HealthMonitor(_as_client(CircuitClient()), cache_ttl=1.0) + + async def custom_check(_client: AsyncOutlineClient) -> dict[str, object]: + return {"status": "ok"} + + monitor.add_custom_check("custom", custom_check) + result = await monitor.check(use_cache=False) + assert "custom" in result.checks + assert result.healthy is False + assert monitor.remove_custom_check("custom") is True + assert monitor.remove_custom_check("missing") is False + assert monitor.clear_custom_checks() == 0 + + +def test_health_status_properties(): + status = HealthStatus( + healthy=False, + timestamp=1.0, + checks={ + "c1": {"status": "healthy"}, + "c2": {"status": "warning"}, + "c3": {"status": "degraded"}, + "c4": {"status": "unhealthy"}, + }, + metrics={}, + ) + assert status.failed_checks == ["c4"] + assert status.warning_checks == ["c2"] + assert status.total_checks == 4 + assert status.passed_checks == 1 + assert status.is_degraded is True + data = status.to_dict() + assert data["failed_checks"] == ["c4"] + + +@pytest.mark.asyncio +async def test_wait_until_healthy_timeout(monkeypatch): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def always_false(*_args: object, **_kwargs: object) -> bool: + return False + + monkeypatch.setattr(HealthMonitor, "quick_check", always_false) + result = await monitor.wait_for_healthy(timeout=0.01, check_interval=0.005) + assert result is False + + +class FailingClient(DummyClient): + async def get_server_info(self): + raise RuntimeError("fail") + + +@pytest.mark.asyncio +async def test_health_monitor_failure_path(): + monitor = HealthMonitor(_as_client(FailingClient()), cache_ttl=1.0) + result = await monitor.check(use_cache=False) + assert result.healthy is False + + +@pytest.mark.asyncio +async def test_custom_check_error_path(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def bad_check(_client: AsyncOutlineClient) -> dict[str, object]: + raise RuntimeError("boom") + + monitor.add_custom_check("bad", bad_check) + result = await monitor.check(use_cache=False) + assert result.checks["bad"]["status"] == "error" + + +@pytest.mark.asyncio +async def test_quick_check_exception_path(): + monitor = HealthMonitor(_as_client(FailingClient()), cache_ttl=1.0) + assert await monitor.quick_check() is False + + +def test_record_request_and_metrics(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + monitor.record_request(True, 1.0) + monitor.record_request(False, 2.0) + metrics = monitor.get_metrics() + assert metrics["total_requests"] == 2 + monitor.reset_metrics() + assert monitor.get_metrics()["total_requests"] == 0 + + with pytest.raises(ValueError, match=r".*"): + monitor.record_request(True, -1.0) + + +def test_performance_metrics_and_helper(): + metrics = PerformanceMetrics() + assert metrics.success_rate == 1.0 + metrics.total_requests = 10 + metrics.successful_requests = 7 + assert metrics.failure_rate == pytest.approx(0.3) + + helper = HealthCheckHelper() + assert helper.determine_status_by_time(0.1) in {"healthy", "warning", "degraded"} + assert helper.determine_status_by_time(10.0) == "degraded" + assert helper.determine_circuit_status("OPEN", 0.1) == "unhealthy" + assert helper.determine_circuit_status("HALF_OPEN", 0.1) == "warning" + assert helper.determine_circuit_status("CLOSED", 0.8) in { + "warning", + "degraded", + "healthy", + } + assert helper.determine_performance_status(0.1, 10.0) == "unhealthy" + assert helper.determine_performance_status(0.95, 0.5) == "warning" + + +def test_health_check_helper_branches(): + helper = HealthCheckHelper() + assert helper.determine_status_by_time(0.9) in {"warning", "healthy"} + assert helper.determine_status_by_time(2.0) == "warning" + assert helper.determine_circuit_status("CLOSED", 0.99) == "healthy" + assert helper.determine_circuit_status("CLOSED", 0.6) == "warning" + assert helper.determine_circuit_status("CLOSED", 0.2) == "degraded" + assert helper.determine_performance_status(0.8, 5.0) == "degraded" + + +@pytest.mark.asyncio +async def test_cache_valid_and_quick_check_cached(monkeypatch): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def fake_check(*args: object, **kwargs: object) -> None: + return None + + await monitor.check(use_cache=False) + assert monitor.cache_valid is True + + monkeypatch.setattr(DummyClient, "get_server_info", fake_check) + assert await monitor.quick_check() is True + + +@pytest.mark.asyncio +async def test_quick_check_cached_result(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + monitor._cached_result = HealthStatus( + healthy=True, timestamp=1.0, checks={}, metrics={} + ) + monitor._last_check_time = time.monotonic() + assert await monitor.quick_check() is True + + +@pytest.mark.asyncio +async def test_circuit_metrics_invalid_success_rate(): + class BadCircuitClient(DummyClient): + def get_circuit_metrics(self): + return {"state": "CLOSED", "success_rate": "bad"} + + monitor = HealthMonitor(_as_client(BadCircuitClient()), cache_ttl=1.0) + result = await monitor.check(use_cache=False) + assert result.checks["circuit_breaker"]["success_rate"] == 0.0 + + +@pytest.mark.asyncio +async def test_custom_check_unhealthy_sets_overall(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def unhealthy_check(_client: AsyncOutlineClient) -> dict[str, object]: + return {"status": "unhealthy"} + + monitor.add_custom_check("unhealthy", unhealthy_check) + result = await monitor.check(use_cache=False) + assert result.healthy is False + + +def test_add_custom_check_validation_and_invalidate_cache(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def noop_check(_client: AsyncOutlineClient) -> dict[str, object]: + return {"status": "ok"} + + with pytest.raises(ValueError, match=r".*"): + monitor.add_custom_check("", noop_check) + with pytest.raises(ValueError, match=r".*"): + monitor.add_custom_check( + "x", + cast( + Callable[ + [AsyncOutlineClient], Coroutine[object, object, dict[str, object]] + ], + "bad", + ), + ) + + monitor._cached_result = HealthStatus( + healthy=True, timestamp=1.0, checks={}, metrics={} + ) + monitor.invalidate_cache() + assert monitor.cache_valid is False + assert monitor.custom_checks_count == 0 + + +@pytest.mark.asyncio +async def test_add_custom_check_logs_debug(caplog): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def ok_check(_client: AsyncOutlineClient) -> dict[str, object]: + return {"status": "healthy"} + + logging.getLogger("pyoutlineapi.health_monitoring").setLevel("DEBUG") + with caplog.at_level("DEBUG", logger="pyoutlineapi.health_monitoring"): + monitor.add_custom_check("ok", ok_check) + assert monitor.custom_checks_count == 1 + + +@pytest.mark.asyncio +async def test_wait_for_healthy_validation_errors(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + with pytest.raises(ValueError, match=r".*"): + await monitor.wait_for_healthy(timeout=0.0, check_interval=1.0) + with pytest.raises(ValueError, match=r".*"): + await monitor.wait_for_healthy(timeout=1.0, check_interval=0.0) + + +@pytest.mark.asyncio +async def test_wait_for_healthy_exception_in_check(monkeypatch): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def boom(*_args: object, **_kwargs: object) -> bool: + raise RuntimeError("boom") + + monkeypatch.setattr(HealthMonitor, "quick_check", boom) + result = await monitor.wait_for_healthy(timeout=0.01, check_interval=0.005) + assert result is False + + +@pytest.mark.asyncio +async def test_wait_for_healthy_exception_logs_debug(monkeypatch, caplog): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def boom(*_args: object, **_kwargs: object) -> bool: + raise RuntimeError("boom") + + monkeypatch.setattr(HealthMonitor, "quick_check", boom) + logging.getLogger("pyoutlineapi.health_monitoring").setLevel("DEBUG") + with caplog.at_level("DEBUG", logger="pyoutlineapi.health_monitoring"): + result = await monitor.wait_for_healthy(timeout=0.01, check_interval=0.005) + assert result is False + assert any("Health check failed" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_wait_for_healthy_success(monkeypatch): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + + async def always_true(*_args: object, **_kwargs: object) -> bool: + return True + + monkeypatch.setattr(HealthMonitor, "quick_check", always_true) + assert await monitor.wait_for_healthy(timeout=1.0, check_interval=0.005) is True + + +@pytest.mark.asyncio +async def test_check_performance_unhealthy(): + monitor = HealthMonitor(_as_client(DummyClient()), cache_ttl=1.0) + monitor._metrics.total_requests = 10 + monitor._metrics.successful_requests = 0 + monitor._metrics.failed_requests = 10 + status_data = {"healthy": True, "checks": {}, "metrics": {}} + await monitor._check_performance(status_data) + assert status_data["healthy"] is False diff --git a/tests/test_init.py b/tests/test_init.py index 5ec0c86..9513305 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,45 +1,57 @@ -""" -Tests for PyOutlineAPI __init__ module. +from __future__ import annotations -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. +from importlib import reload +from typing import Any -Copyright (c) 2025 Denis Rozhnovskiy -All rights reserved. +import pytest -This software is licensed under the MIT License. -You can find the full license text at: - https://opensource.org/licenses/MIT +import pyoutlineapi -Source code repository: - https://github.com/orenlab/pyoutlineapi -""" -from unittest import mock -import pytest +def test_get_version_monkeypatch(monkeypatch): + monkeypatch.setattr(pyoutlineapi, "__version__", "9.9.9") + assert pyoutlineapi.get_version() == "9.9.9" + + +def test_quick_setup_prints( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + called: dict[str, Any] = {"value": False} + + def fake_create_env_template() -> None: + called["value"] = True + + monkeypatch.setattr(pyoutlineapi, "create_env_template", fake_create_env_template) + pyoutlineapi.quick_setup() + out = capsys.readouterr().out + assert called["value"] is True + assert "Created .env.example" in out -import pyoutlineapi -from pyoutlineapi import check_python_version +def test_print_type_info(monkeypatch, capsys): + pyoutlineapi.print_type_info() + out = capsys.readouterr().out + assert "PyOutlineAPI Type Aliases" in out + assert "AuditLogger" in out -def test_check_python_version_raises(): - with mock.patch("sys.version_info", (3, 9)): - with pytest.raises(RuntimeError, match="requires Python 3.10"): - check_python_version() +def test_getattr_helpful_errors(): + with pytest.raises(AttributeError) as exc: + _ = pyoutlineapi.OutlineClient + assert "AsyncOutlineClient" in str(exc.value) -def test_version_from_metadata(): - """Ensure __version__ is loaded correctly from metadata or fallback.""" - assert isinstance(pyoutlineapi.__version__, str) - assert pyoutlineapi.__version__ != "" + with pytest.raises(AttributeError) as exc: + _ = pyoutlineapi.NonExistentThing + assert "has no attribute" in str(exc.value) -def test_all_exports(): - """Check that __all__ contains expected public API symbols.""" - for name in pyoutlineapi.__all__: - assert hasattr(pyoutlineapi, name) +def test_version_fallback(monkeypatch): + import pyoutlineapi as module + def raise_not_found(_name: str) -> str: + raise module.metadata.PackageNotFoundError -def test_metadata_constants(): - assert pyoutlineapi.__author__ == "Denis Rozhnovskiy" - assert pyoutlineapi.__email__ == "pytelemonbot@mail.ru" - assert pyoutlineapi.__license__ == "MIT" + monkeypatch.setattr(module.metadata, "version", raise_not_found) + reloaded = reload(module) + assert reloaded.__version__ == "0.4.0-dev" diff --git a/tests/test_metrics_collector.py b/tests/test_metrics_collector.py new file mode 100644 index 0000000..8356fc2 --- /dev/null +++ b/tests/test_metrics_collector.py @@ -0,0 +1,395 @@ +from __future__ import annotations + +import asyncio +from typing import cast + +import pytest + +from pyoutlineapi import ( + common_types as common_types_module, + metrics_collector as metrics_collector_module, +) +from pyoutlineapi.client import AsyncOutlineClient +from pyoutlineapi.metrics_collector import ( + MetricsCollector, + MetricsSnapshot, + PrometheusExporter, + UsageStats, +) + + +def _as_client(client: object) -> AsyncOutlineClient: + return cast(AsyncOutlineClient, client) + + +class DummyClient: + async def get_server_info(self, *, as_json: bool = False): + return {"name": "srv"} + + async def get_transfer_metrics(self, *, as_json: bool = False): + return {"bytesTransferredByUserId": {"u1": 100, "u2": 50}} + + async def get_access_keys(self, *, as_json: bool = False): + return {"accessKeys": [{"id": "key"}]} + + async def get_experimental_metrics(self, since: str, *, as_json: bool = False): + return { + "server": { + "tunnelTime": {"seconds": 1}, + "dataTransferred": {"bytes": 1}, + "bandwidth": { + "current": {"data": {"bytes": 1}, "timestamp": 1}, + "peak": {"data": {"bytes": 1}, "timestamp": 1}, + }, + "locations": [], + }, + "accessKeys": [], + } + + +class FailingClient(DummyClient): + async def get_server_info(self, *, as_json: bool = False): + raise RuntimeError("fail") + + +class WeirdTransferClient(DummyClient): + async def get_transfer_metrics(self, *, as_json: bool = False): + return {"bytesTransferredByUserId": ["bad"]} + + +@pytest.mark.asyncio +async def test_collect_single_snapshot(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + snapshot = await collector._collect_single_snapshot() + assert snapshot is not None + assert snapshot.key_count == 1 + assert snapshot.total_bytes_transferred == 150 + + +@pytest.mark.asyncio +async def test_collect_single_snapshot_non_dict_transfer(): + collector = MetricsCollector( + _as_client(WeirdTransferClient()), + interval=1.0, + max_history=5, + ) + snapshot = await collector._collect_single_snapshot() + assert snapshot is not None + assert snapshot.total_bytes_transferred == 0 + + +@pytest.mark.asyncio +async def test_collect_single_snapshot_error(monkeypatch): + collector = MetricsCollector( + _as_client(FailingClient()), interval=1.0, max_history=5 + ) + + def fake_getsizeof(_: object) -> int: + return 10 * 1024 * 1024 + 1 + + monkeypatch.setattr("sys.getsizeof", fake_getsizeof) + snapshot = await collector._collect_single_snapshot() + assert snapshot is None + + +def test_metrics_snapshot_size_limit(monkeypatch): + monkeypatch.setattr(common_types_module.Constants, "MAX_SNAPSHOT_SIZE_MB", 0) + with pytest.raises(ValueError, match=r".*"): + MetricsSnapshot( + timestamp=0.0, + server_info={"a": {"b": {"c": "d"}}}, + transfer_metrics={}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ) + + +def test_estimate_size_early_exit(): + size = metrics_collector_module._estimate_size({"a": [1, 2, 3]}, max_bytes=1) + assert size > 1 + + +@pytest.mark.asyncio +async def test_collector_start_stop(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + await collector.start() + await asyncio.sleep(0) + await collector.stop() + assert collector.is_running is False + + +def test_collector_stats_and_prometheus(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + snapshot = MetricsSnapshot( + timestamp=1.0, + server_info={"metricsEnabled": True, "portForNewAccessKeys": 1234}, + transfer_metrics={"bytesTransferredByUserId": {"u1": 10}}, + experimental_metrics={ + "server": { + "tunnelTime": {"seconds": 1}, + "dataTransferred": {"bytes": 5}, + "bandwidth": { + "current": {"data": {"bytes": 1}, "timestamp": 1}, + "peak": {"data": {"bytes": 2}, "timestamp": 2}, + }, + "locations": [ + { + "location": "US", + "tunnelTime": {"seconds": 1}, + "dataTransferred": {"bytes": 1}, + } + ], + }, + "accessKeys": [ + { + "accessKeyId": "u1", + "tunnelTime": {"seconds": 1}, + "dataTransferred": {"bytes": 1}, + "connection": { + "lastTrafficSeen": 1, + "peakDeviceCount": {"data": 1, "timestamp": 1}, + }, + } + ], + }, + key_count=1, + total_bytes_transferred=10, + ) + collector._history.append(snapshot) + collector._history.append( + MetricsSnapshot( + timestamp=2.0, + server_info={}, + transfer_metrics={"bytesTransferredByUserId": {}}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ) + ) + + latest = collector.get_latest_snapshot() + assert latest is not None + + stats = collector.get_usage_stats() + assert stats.snapshots_count == 2 + + filtered = collector.get_snapshots(start_time=1.5) + assert len(filtered) == 1 + + prom = collector.export_prometheus(include_per_key=True) + assert "outline_keys_total" in prom + + summary = collector.export_prometheus_summary() + assert "outline_bytes_transferred_total" in summary + + +def test_metrics_snapshot_and_usage_stats_dict(): + snapshot = MetricsSnapshot( + timestamp=1.0, + server_info={"a": 1}, + transfer_metrics={"bytesTransferredByUserId": {"k": 1}}, + experimental_metrics={}, + key_count=1, + total_bytes_transferred=1, + ) + assert snapshot.to_dict()["keys_count"] == 1 + + stats = UsageStats( + period_start=1.0, + period_end=3.0, + snapshots_count=2, + total_bytes_transferred=2048, + avg_bytes_per_snapshot=1024.0, + peak_bytes=2048, + active_keys=frozenset({"a"}), + ) + data = stats.to_dict() + assert data["active_keys_count"] == 1 + assert stats.megabytes_transferred > 0 + assert stats.gigabytes_transferred > 0 + + +def test_prometheus_exporter_cache(): + exporter = PrometheusExporter() + metrics = [ + ("metric_name", 1, "gauge", "desc", {"key": "value"}), + ] + out1 = exporter.format_metrics_batch(metrics, cache_key="a") + out2 = exporter.format_metrics_batch(metrics, cache_key="a") + assert out1 == out2 + exporter.clear_cache() + out3 = exporter.format_metrics_batch(metrics, cache_key="a") + assert out3 == out1 + + +def test_collector_empty_history(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + assert collector.get_latest_snapshot() is None + stats = collector.get_usage_stats() + assert stats.snapshots_count == 0 + assert collector.export_prometheus() == "" + assert collector.export_prometheus_summary() == "" + + +def test_collector_invalid_params(): + with pytest.raises(ValueError, match=r".*"): + MetricsCollector(_as_client(DummyClient()), interval=0.0) + with pytest.raises(ValueError, match=r".*"): + MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=0) + + +@pytest.mark.asyncio +async def test_collect_loop_cancel_and_timeout(monkeypatch): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + collector._interval = 0.01 + + async def return_none(self: MetricsCollector) -> MetricsSnapshot | None: + await asyncio.sleep(0.1) + return None + + monkeypatch.setattr(MetricsCollector, "_collect_single_snapshot", return_none) + collector._running = True + task = asyncio.create_task(collector._collect_loop()) + await asyncio.sleep(0.01) + task.cancel() + await task + assert task.done() + + +@pytest.mark.asyncio +async def test_collect_loop_unexpected_error(monkeypatch): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + collector._interval = 0.01 + + async def boom(self: MetricsCollector) -> MetricsSnapshot | None: + raise RuntimeError("boom") + + monkeypatch.setattr(MetricsCollector, "_collect_single_snapshot", boom) + collector._running = True + task = asyncio.create_task(collector._collect_loop()) + await asyncio.sleep(0.005) + collector._shutdown_event.set() + await task + + +@pytest.mark.asyncio +async def test_collector_start_twice_and_stop_not_running(monkeypatch): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + await collector.start() + with pytest.raises(RuntimeError): + await collector.start() + await collector.stop() + await collector.stop() + + +@pytest.mark.asyncio +async def test_collector_stop_timeout(monkeypatch): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + + async def fake_loop() -> None: + await asyncio.sleep(1) + + collector._task = asyncio.create_task(fake_loop()) + collector._running = True + + async def raise_timeout(_task: object, timeout: float | None = None) -> None: + raise asyncio.TimeoutError() + + monkeypatch.setattr(asyncio, "wait_for", raise_timeout) + await collector.stop() + + +def test_collector_clear_history(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + collector._history.append( + MetricsSnapshot( + timestamp=1.0, + server_info={}, + transfer_metrics={}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ) + ) + collector.clear_history() + assert collector.snapshots_count == 0 + + +def test_get_snapshots_end_time_and_limit(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + collector._history.extend( + [ + MetricsSnapshot( + timestamp=1.0, + server_info={}, + transfer_metrics={}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ), + MetricsSnapshot( + timestamp=2.0, + server_info={}, + transfer_metrics={}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ), + MetricsSnapshot( + timestamp=3.0, + server_info={}, + transfer_metrics={}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ), + ] + ) + assert len(collector.get_snapshots(end_time=2.0)) == 2 + assert len(collector.get_snapshots(limit=1)) == 1 + + +def test_export_prometheus_with_locations_and_keys(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + snapshot = MetricsSnapshot( + timestamp=1.0, + server_info={"metricsEnabled": True, "portForNewAccessKeys": 1234}, + transfer_metrics={"bytesTransferredByUserId": {"u1": 10}}, + experimental_metrics={ + "server": { + "tunnelTime": {"seconds": 3600}, + "bandwidth": { + "current": {"data": {"bytes": 1}}, + "peak": {"data": {"bytes": 2}}, + }, + "locations": [ + { + "location": "US", + "tunnelTime": {"seconds": 1}, + "dataTransferred": {"bytes": 1}, + }, + "invalid", + { + "location": "EU", + "tunnelTime": {"seconds": 0}, + "dataTransferred": {"bytes": 0}, + }, + ], + } + }, + key_count=1, + total_bytes_transferred=10, + ) + collector._history.append(snapshot) + metrics = collector.export_prometheus(include_per_key=True) + assert "outline_key_bytes_total" in metrics + + +@pytest.mark.asyncio +async def test_collector_uptime_and_context_manager(): + collector = MetricsCollector(_as_client(DummyClient()), interval=1.0, max_history=5) + assert collector.uptime == 0.0 + async with collector: + assert collector.is_running is True + assert collector.is_running is False diff --git a/tests/test_metrics_collector_extra.py b/tests/test_metrics_collector_extra.py new file mode 100644 index 0000000..c227e56 --- /dev/null +++ b/tests/test_metrics_collector_extra.py @@ -0,0 +1,437 @@ +from __future__ import annotations + +import asyncio +import logging +from collections import deque +from contextlib import suppress +from typing import cast + +import pytest + +from pyoutlineapi.client import AsyncOutlineClient +from pyoutlineapi.metrics_collector import ( + MetricsCollector, + MetricsSnapshot, + PrometheusExporter, +) + + +class DummyClient: + async def get_server_info( + self, + *, + as_json: bool = False, + ) -> dict[str, object]: + return {"metricsEnabled": True, "portForNewAccessKeys": 8080} + + async def get_transfer_metrics( + self, + *, + as_json: bool = False, + ) -> dict[str, object]: + return {"bytesTransferredByUserId": {"a": 1, "b": 0}} + + async def get_access_keys( + self, + *, + as_json: bool = False, + ) -> dict[str, object]: + return {"accessKeys": [{"id": "a"}]} + + async def get_experimental_metrics( + self, + since: str, + *, + as_json: bool = False, + ) -> dict[str, object]: + return { + "server": { + "tunnelTime": {"seconds": 3600}, + "bandwidth": { + "current": {"data": {"bytes": 10}}, + "peak": {"data": {"bytes": 20}}, + }, + "locations": [ + { + "location": "us", + "dataTransferred": {"bytes": 5}, + "tunnelTime": {"seconds": 7}, + } + ], + } + } + + +class FailingClient(DummyClient): + async def get_server_info( + self, + *, + as_json: bool = False, + ) -> dict[str, object]: + raise RuntimeError("boom") + + +class FailingKeysClient(DummyClient): + async def get_access_keys( + self, + *, + as_json: bool = False, + ) -> dict[str, object]: + raise RuntimeError("boom") + + +def _as_client(client: object) -> AsyncOutlineClient: + return cast(AsyncOutlineClient, client) + + +def _make_snapshot(timestamp: float, total_bytes: int) -> MetricsSnapshot: + return MetricsSnapshot( + timestamp=timestamp, + server_info={"metricsEnabled": True, "portForNewAccessKeys": 1234}, + transfer_metrics={"bytesTransferredByUserId": {"a": total_bytes}}, + experimental_metrics={}, + key_count=1, + total_bytes_transferred=total_bytes, + ) + + +def _make_snapshot_with_experimental(timestamp: float) -> MetricsSnapshot: + return MetricsSnapshot( + timestamp=timestamp, + server_info={"metricsEnabled": True, "portForNewAccessKeys": 1234}, + transfer_metrics={"bytesTransferredByUserId": {"a": 1024}}, + experimental_metrics={ + "server": { + "tunnelTime": {"seconds": 3600}, + "bandwidth": { + "current": {"data": {"bytes": 10}}, + "peak": {"data": {"bytes": 20}}, + }, + "locations": [ + { + "location": "us", + "dataTransferred": {"bytes": 5}, + "tunnelTime": {"seconds": 7}, + } + ], + } + }, + key_count=2, + total_bytes_transferred=1024, + ) + + +def test_prometheus_exporter_cache_and_clear(): + exporter = PrometheusExporter(cache_ttl=60) + metrics = [("test_metric", 1, "gauge", "help", None)] + out1 = exporter.format_metrics_batch(metrics, cache_key="k1") + out2 = exporter.format_metrics_batch(metrics, cache_key="k1") + assert out1 == out2 + exporter.clear_cache() + out3 = exporter.format_metrics_batch(metrics, cache_key="k1") + assert out3 == out1 + + +def test_prometheus_exporter_no_cache_key(): + exporter = PrometheusExporter(cache_ttl=60) + metrics = [("test_metric", 1, "gauge", "", None)] + out = exporter.format_metrics_batch(metrics, cache_key=None) + assert "test_metric" in out + + +def test_prometheus_format_metric_with_labels(): + exporter = PrometheusExporter(cache_ttl=60) + lines = exporter.format_metric( + "metric", + 1, + metric_type="gauge", + help_text="help", + labels={"key": "value"}, + ) + assert any("HELP metric help" in line for line in lines) + assert any('metric{key="value"} 1' in line for line in lines) + + +@pytest.mark.asyncio +async def test_collect_single_snapshot_with_fallbacks(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + snapshot = await collector._collect_single_snapshot() + assert snapshot is not None + assert snapshot.key_count == 1 + + collector_fail = MetricsCollector( + _as_client(FailingClient()), interval=1.0, max_history=10 + ) + snapshot2 = await collector_fail._collect_single_snapshot() + assert snapshot2 is not None + assert snapshot2.key_count == 1 + + collector_fail_keys = MetricsCollector( + _as_client(FailingKeysClient()), + interval=1.0, + max_history=10, + ) + snapshot3 = await collector_fail_keys._collect_single_snapshot() + assert snapshot3 is not None + assert snapshot3.key_count == 0 + + +@pytest.mark.asyncio +async def test_collect_single_snapshot_exception_returns_none(monkeypatch): + class BadDict(dict): + def get(self, *args: object, **kwargs: object) -> object: + raise ValueError("bad") + + class BadKeysClient(DummyClient): + async def get_access_keys( + self, + *, + as_json: bool = False, + ) -> dict[str, object]: + return BadDict() + + collector = MetricsCollector( + _as_client(BadKeysClient()), interval=1.0, max_history=10 + ) + assert await collector._collect_single_snapshot() is None + + +def test_get_snapshots_filters_and_latest(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque( + [ + _make_snapshot(1.0, 10), + _make_snapshot(2.0, 20), + _make_snapshot(3.0, 30), + ], + maxlen=10, + ) + + latest = collector.get_latest_snapshot() + assert latest is not None + assert latest.total_bytes_transferred == 30 + assert len(collector.get_snapshots(start_time=2.0)) == 2 + assert len(collector.get_snapshots(end_time=2.0)) == 2 + assert len(collector.get_snapshots(limit=1)) == 1 + + +def test_usage_stats_caching(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque([_make_snapshot(1.0, 10)], maxlen=10) + stats1 = collector.get_usage_stats() + stats2 = collector.get_usage_stats() + assert stats1 is stats2 + + +def test_usage_stats_empty_history(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + stats = collector.get_usage_stats() + assert stats.total_bytes_transferred == 0 + + +def test_export_prometheus_and_summary(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque([_make_snapshot_with_experimental(1.0)], maxlen=10) + output = collector.export_prometheus(include_per_key=True) + assert "outline_keys_total" in output + assert "outline_key_bytes_total" in output + assert "outline_tunnel_time_seconds_total" in output + assert "outline_bandwidth_peak_bytes" in output + + summary = collector.export_prometheus_summary() + assert "outline_bytes_per_second" in summary + + +def test_clear_history_resets_state(caplog): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque([_make_snapshot(1.0, 10)], maxlen=10) + collector._stats_cache = collector.get_usage_stats() + with caplog.at_level(logging.INFO, logger="pyoutlineapi.metrics_collector"): + collector.clear_history() + assert collector.snapshots_count == 0 + assert collector.get_latest_snapshot() is None + + +@pytest.mark.asyncio +async def test_collect_loop_warning_and_stop(caplog): + class ErrorCollector(MetricsCollector): + async def _collect_single_snapshot(self) -> MetricsSnapshot | None: + return None + + collector = ErrorCollector(_as_client(DummyClient()), interval=1.0, max_history=10) + collector._interval = 0.001 + collector._running = True + + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.metrics_collector"): + task = asyncio.create_task(collector._collect_loop()) + await asyncio.sleep(0.05) + collector._shutdown_event.set() + + try: + await asyncio.wait_for(task, timeout=0.1) + except (asyncio.TimeoutError, asyncio.CancelledError): + pass + finally: + if not task.done(): + task.cancel() + with suppress(asyncio.CancelledError): + await task + + assert any("Failed to collect metrics" in r.message for r in caplog.records) + + +@pytest.mark.asyncio +async def test_start_and_stop(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + await collector.start() + assert collector.is_running is True + await collector.stop() + assert collector.is_running is False + + +def test_uptime_when_running(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._running = True + collector._start_time = 1.0 + assert collector.uptime >= 0.0 + + +def test_export_prometheus_no_snapshot(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history.clear() + assert collector.export_prometheus() == "" + + +def test_export_prometheus_without_per_key_or_experimental(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque( + [ + MetricsSnapshot( + timestamp=1.0, + server_info={}, + transfer_metrics={}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ) + ], + maxlen=10, + ) + output = collector.export_prometheus(include_per_key=False) + assert "outline_keys_total" in output + + +def test_export_prometheus_per_key_non_dict(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque( + [ + MetricsSnapshot( + timestamp=1.0, + server_info={"metricsEnabled": False}, + transfer_metrics={"bytesTransferredByUserId": []}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ) + ], + maxlen=10, + ) + output = collector.export_prometheus(include_per_key=True) + assert "outline_metrics_enabled" in output + + +def test_export_prometheus_experimental_zero_location(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque( + [ + MetricsSnapshot( + timestamp=1.0, + server_info={"metricsEnabled": True}, + transfer_metrics={"bytesTransferredByUserId": {"a": 1}}, + experimental_metrics={ + "server": { + "locations": [ + {"location": "zero", "dataTransferred": {"bytes": 0}} + ], + "bandwidth": {"current": {"data": {}}}, + } + }, + key_count=1, + total_bytes_transferred=1, + ) + ], + maxlen=10, + ) + output = collector.export_prometheus(include_per_key=True) + assert "outline_keys_total" in output + + +@pytest.mark.asyncio +async def test_stop_cancels_task(caplog): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + + async def long_task() -> None: + await asyncio.sleep(1) + + collector._running = True + collector._task = asyncio.create_task(long_task()) + collector._shutdown_event.clear() + + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.metrics_collector"): + await collector.stop() + assert collector._task is None + + +def test_get_snapshots_limit_zero(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque([_make_snapshot(1.0, 10)], maxlen=10) + assert len(collector.get_snapshots(limit=0)) == 1 + + +def test_usage_stats_with_non_dict_bytes(): + collector = MetricsCollector( + _as_client(DummyClient()), interval=1.0, max_history=10 + ) + collector._history = deque( + [ + MetricsSnapshot( + timestamp=1.0, + server_info={}, + transfer_metrics={"bytesTransferredByUserId": []}, + experimental_metrics={}, + key_count=0, + total_bytes_transferred=0, + ) + ], + maxlen=10, + ) + stats = collector.get_usage_stats() + assert stats.active_keys == frozenset() diff --git a/tests/test_models.py b/tests/test_models.py index dedb8b8..36590a5 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,886 +1,195 @@ -""" -Tests for PyOutlineAPI exceptions module. - -PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API. - -Copyright (c) 2025 Denis Rozhnovskiy -All rights reserved. - -This software is licensed under the MIT License. -You can find the full license text at: - https://opensource.org/licenses/MIT - -Source code repository: - https://github.com/orenlab/pyoutlineapi -""" +from __future__ import annotations import pytest -from pydantic import ValidationError -from pyoutlineapi import OutlineError, APIError +from pyoutlineapi.common_types import Constants from pyoutlineapi.models import ( AccessKey, - AccessKeyCreateRequest, AccessKeyList, - AccessKeyNameRequest, + DataLimit, DataLimitRequest, ErrorResponse, ExperimentalMetrics, - HostnameRequest, - MetricsEnabledRequest, - MetricsStatusResponse, - PortRequest, + HealthCheckResult, Server, ServerMetrics, - ServerNameRequest, - DataLimit, + ServerSummary, TunnelTime, - DataTransferred, - BandwidthData, - BandwidthInfo, - LocationMetric, - PeakDeviceCount, - ConnectionInfo, - AccessKeyMetric, - ServerExperimentalMetric, ) - -@pytest.fixture -def sample_access_key_data(): - """Sample access key data.""" - return { - "id": "1", - "name": "Test Key", - "password": "test-password", - "port": 8080, - "method": "chacha20-ietf-poly1305", - "accessUrl": "ss://test-url", - "dataLimit": {"bytes": 1073741824} - } - - -@pytest.fixture -def sample_access_key_list_data(): - """Sample access key list data.""" - return { - "accessKeys": [ - { - "id": "1", - "password": "pass1", - "port": 8080, - "method": "aes-256-gcm", - "accessUrl": "ss://url1" - }, - { - "id": "2", - "password": "pass2", - "port": 8081, - "method": "chacha20-ietf-poly1305", - "accessUrl": "ss://url2" - } - ] - } - - -@pytest.fixture -def sample_server_data(): - """Sample server data.""" - return { - "name": "Test Server", - "serverId": "test-server-123", - "metricsEnabled": True, - "createdTimestampMs": 1640995200000, - "version": "1.0.0", - "portForNewAccessKeys": 8080, - "hostnameForAccessKeys": "test.example.com", - "accessKeyDataLimit": {"bytes": 1073741824} - } - - -class TestDataLimit: - """Test DataLimit model.""" - - def test_valid_data_limit(self): - """Test valid data limit creation.""" - limit = DataLimit(bytes=1024) - assert limit.bytes == 1024 - - def test_zero_bytes_allowed(self): - """Test that zero bytes is allowed.""" - limit = DataLimit(bytes=0) - assert limit.bytes == 0 - - def test_negative_bytes_validation(self): - """Test that negative bytes raises validation error.""" - with pytest.raises(ValidationError): - DataLimit(bytes=-1) - - def test_large_bytes_value(self): - """Test handling of large byte values.""" - large_value = 1024 * 1024 * 1024 * 1024 # 1TB - limit = DataLimit(bytes=large_value) - assert limit.bytes == large_value - - -class TestAccessKey: - """Test AccessKey model.""" - - def test_access_key_method_variations(self, sample_access_key_data): - """Test different encryption methods.""" - methods = ["aes-256-gcm", "aes-192-gcm", "aes-128-gcm", "chacha20-ietf-poly1305"] - - for method in methods: - sample_access_key_data["method"] = method - key = AccessKey(**sample_access_key_data) - assert key.method == method - - def test_access_key_with_minimal_data(self): - """Test access key with only required fields.""" - minimal_data = { - "id": "minimal", - "password": "pass", - "port": 8080, - "method": "aes-256-gcm", - "accessUrl": "ss://minimal-url" - } - key = AccessKey(**minimal_data) - assert key.id == "minimal" - assert key.name is None - assert key.data_limit is None - - def test_valid_access_key(self, sample_access_key_data): - """Test valid access key creation.""" - key = AccessKey(**sample_access_key_data) - assert key.id == "1" - assert key.name == "Test Key" - assert key.password == "test-password" - assert key.port == 8080 - assert key.method == "chacha20-ietf-poly1305" - assert key.access_url == "ss://test-url" - assert key.data_limit.bytes == 1073741824 - - def test_access_key_without_name(self, sample_access_key_data): - """Test access key creation without name.""" - del sample_access_key_data["name"] - key = AccessKey(**sample_access_key_data) - assert key.name is None - - def test_access_key_without_data_limit(self, sample_access_key_data): - """Test access key creation without data limit.""" - del sample_access_key_data["dataLimit"] - key = AccessKey(**sample_access_key_data) - assert key.data_limit is None - - def test_invalid_port_validation(self, sample_access_key_data): - """Test port validation.""" - sample_access_key_data["port"] = 0 - with pytest.raises(ValidationError): - AccessKey(**sample_access_key_data) - - sample_access_key_data["port"] = 65536 - with pytest.raises(ValidationError): - AccessKey(**sample_access_key_data) - - def test_valid_port_boundaries(self, sample_access_key_data): - """Test valid port boundaries.""" - sample_access_key_data["port"] = 1 - key = AccessKey(**sample_access_key_data) - assert key.port == 1 - - sample_access_key_data["port"] = 65535 - key = AccessKey(**sample_access_key_data) - assert key.port == 65535 - - def test_field_aliases(self): - """Test field aliases work correctly.""" - data = { - "id": "1", - "password": "pass", - "port": 8080, - "method": "aes-256-gcm", - "accessUrl": "ss://url", # Using alias - "dataLimit": {"bytes": 1024}, # Using alias - } - key = AccessKey(**data) - assert key.access_url == "ss://url" - assert key.data_limit.bytes == 1024 - - -class TestAccessKeyList: - """Test AccessKeyList model.""" - - def test_valid_access_key_list(self, sample_access_key_list_data): - """Test valid access key list creation.""" - key_list = AccessKeyList(**sample_access_key_list_data) - assert len(key_list.access_keys) == 2 - assert key_list.access_keys[0].id == "1" - assert key_list.access_keys[1].id == "2" - - def test_empty_access_key_list(self): - """Test empty access key list.""" - key_list = AccessKeyList(accessKeys=[]) - assert len(key_list.access_keys) == 0 - - def test_field_alias(self): - """Test field alias works correctly.""" - data = {"accessKeys": []} - key_list = AccessKeyList(**data) - assert isinstance(key_list.access_keys, list) - - -class TestServer: - """Test Server model.""" - - def test_server_metrics_enabled_false(self, sample_server_data): - """Test server with metrics disabled.""" - sample_server_data["metricsEnabled"] = False - server = Server(**sample_server_data) - assert server.metrics_enabled is False - - def test_server_with_minimal_data(self): - """Test server with only required fields.""" - minimal_data = { - "name": "Minimal Server", - "serverId": "minimal-123", - "metricsEnabled": False, - "createdTimestampMs": 1640995200000, - "version": "1.0.0", - "portForNewAccessKeys": 8080 - } - server = Server(**minimal_data) - assert server.hostname_for_access_keys is None - assert server.access_key_data_limit is None - - def test_server_timestamp_boundaries(self, sample_server_data): - """Test server with different timestamp values.""" - # Test with zero timestamp - sample_server_data["createdTimestampMs"] = 0 - server = Server(**sample_server_data) - assert server.created_timestamp_ms == 0 - - # Test with large timestamp - large_timestamp = 9999999999999 - sample_server_data["createdTimestampMs"] = large_timestamp - server = Server(**sample_server_data) - assert server.created_timestamp_ms == large_timestamp - - def test_valid_server(self, sample_server_data): - """Test valid server creation.""" - server = Server(**sample_server_data) - assert server.name == "Test Server" - assert server.server_id == "test-server-123" - assert server.metrics_enabled is True - assert server.created_timestamp_ms == 1640995200000 - assert server.version == "1.0.0" - assert server.port_for_new_access_keys == 8080 - assert server.hostname_for_access_keys == "test.example.com" - assert server.access_key_data_limit.bytes == 1073741824 - - def test_server_without_optional_fields(self, sample_server_data): - """Test server without optional fields.""" - del sample_server_data["hostnameForAccessKeys"] - del sample_server_data["accessKeyDataLimit"] - - server = Server(**sample_server_data) - assert server.hostname_for_access_keys is None - assert server.access_key_data_limit is None - - def test_invalid_port_validation(self, sample_server_data): - """Test port validation for server.""" - sample_server_data["portForNewAccessKeys"] = 0 - with pytest.raises(ValidationError): - Server(**sample_server_data) - - sample_server_data["portForNewAccessKeys"] = 65536 - with pytest.raises(ValidationError): - Server(**sample_server_data) - - def test_valid_port_boundaries(self, sample_server_data): - """Test valid port boundaries for server.""" - sample_server_data["portForNewAccessKeys"] = 1 - server = Server(**sample_server_data) - assert server.port_for_new_access_keys == 1 - - sample_server_data["portForNewAccessKeys"] = 65535 - server = Server(**sample_server_data) - assert server.port_for_new_access_keys == 65535 - - -class TestServerMetrics: - """Test ServerMetrics model.""" - - def test_server_metrics_with_string_keys(self): - """Test ServerMetrics with various string key formats.""" - data = { - "bytesTransferredByUserId": { - "1": 1024, - "key-with-dashes": 2048, - "key_with_underscores": 512, - "very-long-key-name-12345": 256 - } - } - metrics = ServerMetrics(**data) - assert metrics.bytes_transferred_by_user_id["key-with-dashes"] == 2048 - assert metrics.bytes_transferred_by_user_id["key_with_underscores"] == 512 - - def test_server_metrics_with_zero_values(self): - """Test ServerMetrics with zero transfer values.""" - data = {"bytesTransferredByUserId": {"user1": 0, "user2": 0, "user3": 0}} - metrics = ServerMetrics(**data) - assert all(v == 0 for v in metrics.bytes_transferred_by_user_id.values()) - - def test_valid_server_metrics(self): - """Test valid server metrics creation.""" - data = {"bytesTransferredByUserId": {"1": 1024, "2": 2048, "3": 0}} - metrics = ServerMetrics(**data) - assert metrics.bytes_transferred_by_user_id["1"] == 1024 - assert metrics.bytes_transferred_by_user_id["2"] == 2048 - assert metrics.bytes_transferred_by_user_id["3"] == 0 - - def test_empty_metrics(self): - """Test empty server metrics.""" - data = {"bytesTransferredByUserId": {}} - metrics = ServerMetrics(**data) - assert len(metrics.bytes_transferred_by_user_id) == 0 - - -class TestTunnelTime: - """Test TunnelTime model.""" - - def test_tunnel_time_negative_validation(self): - """Test that negative seconds might be allowed (no explicit validation).""" - # Check that model doesn't have explicit restrictions on negative values - tunnel_time = TunnelTime(seconds=-100) - assert tunnel_time.seconds == -100 - - def test_valid_tunnel_time(self): - """Test valid tunnel time creation.""" - tunnel_time = TunnelTime(seconds=3600) - assert tunnel_time.seconds == 3600 - - def test_zero_seconds(self): - """Test zero seconds.""" - tunnel_time = TunnelTime(seconds=0) - assert tunnel_time.seconds == 0 - - -class TestDataTransferred: - """Test DataTransferred model.""" - - def test_data_transferred_negative_validation(self): - """Test that negative bytes might be allowed (no explicit validation).""" - # Check that model doesn't have explicit restrictions on negative values - data_transferred = DataTransferred(bytes=-1024) - assert data_transferred.bytes == -1024 - - def test_data_transferred_large_value(self): - """Test handling of very large byte values.""" - large_value = 2 ** 63 - 1 # Max int64 - data_transferred = DataTransferred(bytes=large_value) - assert data_transferred.bytes == large_value - - def test_valid_data_transferred(self): - """Test valid data transferred creation.""" - data_transferred = DataTransferred(bytes=1048576) - assert data_transferred.bytes == 1048576 - - def test_zero_bytes(self): - """Test zero bytes.""" - data_transferred = DataTransferred(bytes=0) - assert data_transferred.bytes == 0 - - -class TestBandwidthData: - """Test BandwidthData model.""" - - def test_bandwidth_data_without_timestamp(self): - """Test bandwidth data without timestamp.""" - bandwidth_data = BandwidthData(data={"bytes": 1024}) - assert bandwidth_data.data == {"bytes": 1024} - assert bandwidth_data.timestamp is None - - def test_bandwidth_data_with_complex_data(self): - """Test bandwidth data with complex data structure.""" - complex_data = { - "bytes": 1024, - "packets": 100, - "errors": 0 - } - bandwidth_data = BandwidthData( - data=complex_data, - timestamp=1640995200 - ) - assert bandwidth_data.data == complex_data - assert bandwidth_data.timestamp == 1640995200 - - def test_valid_bandwidth_data(self): - """Test valid bandwidth data creation.""" - bandwidth_data = BandwidthData( - data={"bytes": 1024}, - timestamp=1640995200 - ) - assert bandwidth_data.data == {"bytes": 1024} - assert bandwidth_data.timestamp == 1640995200 - - -class TestBandwidthInfo: - """Test BandwidthInfo model.""" - - def test_valid_bandwidth_info(self): - """Test valid bandwidth info creation.""" - bandwidth_info = BandwidthInfo( - current=BandwidthData(data={"bytes": 1024}, timestamp=1640995200), - peak=BandwidthData(data={"bytes": 2048}, timestamp=1640995300) +PLACEHOLDER_CREDENTIAL = "pwd" + + +def test_data_limit_conversions(): + limit = DataLimit.from_megabytes(1) + assert limit.bytes == 1024 * 1024 + assert DataLimit.from_kilobytes(1).kilobytes == 1 + assert DataLimit.from_gigabytes(1).gigabytes == 1 + assert limit.megabytes == 1 + + +def test_access_key_properties(): + key = AccessKey( + id="key-1", + name=None, + password=PLACEHOLDER_CREDENTIAL, + port=12345, + method="aes-256-gcm", + accessUrl="ss://example", + dataLimit=None, + ) + assert key.display_name == "Key-key-1" + assert key.has_data_limit is False + + +def test_access_key_name_validation(): + key = AccessKey( + id="key-1", + name=" ", + password=PLACEHOLDER_CREDENTIAL, + port=12345, + method="aes-256-gcm", + accessUrl="ss://example", + dataLimit=None, + ) + assert key.name is None + + too_long = "a" * (Constants.MAX_NAME_LENGTH + 1) + with pytest.raises(ValueError, match=r".*"): + AccessKey( + id="key-1", + name=too_long, + password=PLACEHOLDER_CREDENTIAL, + port=12345, + method="aes-256-gcm", + accessUrl="ss://example", + dataLimit=None, ) - assert bandwidth_info.current.data == {"bytes": 1024} - assert bandwidth_info.peak.data == {"bytes": 2048} - -class TestLocationMetric: - """Test LocationMetric model.""" - def test_location_metric_with_valid_asn_and_org(self): - """Test location metric with valid ASN and organization.""" - location_metric = LocationMetric( - location="US", - asn=12345, - asOrg="Test Organization", - tunnelTime=TunnelTime(seconds=1800), - dataTransferred=DataTransferred(bytes=524288) +def test_access_key_list(): + key = AccessKey( + id="key-1", + name="Name", + password=PLACEHOLDER_CREDENTIAL, + port=12345, + method="aes-256-gcm", + accessUrl="ss://example", + dataLimit=None, + ) + lst = AccessKeyList(accessKeys=[key]) + assert lst.count == 1 + assert lst.is_empty is False + assert lst.get_by_id("key-1") is not None + assert lst.get_by_id("missing") is None + assert lst.get_by_name("Name") == [key] + assert lst.filter_without_limits() == [key] + assert lst.filter_with_limits() == [] + + +def test_server_and_metrics(): + server = Server( + name="Server", + serverId="srv", + metricsEnabled=True, + createdTimestampMs=1000, + portForNewAccessKeys=12345, + hostnameForAccessKeys="host", + accessKeyDataLimit=None, + version="1.0", + ) + assert server.has_global_limit is False + assert server.created_timestamp_seconds == 1.0 + + metrics = ServerMetrics(bytesTransferredByUserId={"u": 100, "v": 200}) + assert metrics.total_bytes == 300 + assert metrics.user_count == 2 + assert metrics.total_gigabytes > 0 + assert metrics.get_user_bytes("u") == 100 + assert metrics.get_user_bytes("missing") == 0 + assert metrics.top_users(limit=1)[0][0] in {"u", "v"} + + +def test_server_name_validation_error(): + with pytest.raises(ValueError, match=r".*"): + Server( + name="", + serverId="srv", + metricsEnabled=True, + createdTimestampMs=1000, + portForNewAccessKeys=12345, + hostnameForAccessKeys=None, + accessKeyDataLimit=None, ) - assert location_metric.asn == 12345 - assert location_metric.as_org == "Test Organization" - - def test_valid_location_metric(self): - """Test valid location metric creation.""" - location_metric = LocationMetric( - location="US", - asn=12345, - asOrg="Test AS", - tunnelTime=TunnelTime(seconds=1800), - dataTransferred=DataTransferred(bytes=524288) - ) - assert location_metric.location == "US" - assert location_metric.asn == 12345 - assert location_metric.as_org == "Test AS" - assert location_metric.tunnel_time.seconds == 1800 - assert location_metric.data_transferred.bytes == 524288 -class TestPeakDeviceCount: - """Test PeakDeviceCount model.""" +def test_server_name_validation_none(monkeypatch): + from pyoutlineapi import common_types - def test_valid_peak_device_count(self): - """Test valid peak device count creation.""" - peak_device_count = PeakDeviceCount(data=5, timestamp=1640995500) - assert peak_device_count.data == 5 - assert peak_device_count.timestamp == 1640995500 + def fake_validate_name(_v: str) -> None: + return None + monkeypatch.setattr(common_types.Validators, "validate_name", fake_validate_name) + with pytest.raises(ValueError, match=r".*"): + Server( + name="Server", + serverId="srv", + metricsEnabled=True, + createdTimestampMs=1000, + portForNewAccessKeys=12345, + hostnameForAccessKeys=None, + accessKeyDataLimit=None, + ) -class TestConnectionInfo: - """Test ConnectionInfo model.""" - def test_connection_info_with_zero_timestamp(self): - """Test connection info with zero timestamp.""" - connection_info = ConnectionInfo( - lastTrafficSeen=0, - peakDeviceCount=PeakDeviceCount(data=1, timestamp=0) - ) - assert connection_info.last_traffic_seen == 0 - assert connection_info.peak_device_count.timestamp == 0 - - def test_valid_connection_info(self): - """Test valid connection info creation.""" - connection_info = ConnectionInfo( - lastTrafficSeen=1640995400, - peakDeviceCount=PeakDeviceCount(data=3, timestamp=1640995500) - ) - assert connection_info.last_traffic_seen == 1640995400 - assert connection_info.peak_device_count.data == 3 - - -class TestAccessKeyMetric: - """Test AccessKeyMetric model.""" - - def test_valid_access_key_metric(self): - """Test valid access key metric creation.""" - access_key_metric = AccessKeyMetric( - accessKeyId=1, - tunnelTime=TunnelTime(seconds=900), - dataTransferred=DataTransferred(bytes=262144), - connection=ConnectionInfo( - lastTrafficSeen=1640995400, - peakDeviceCount=PeakDeviceCount(data=2, timestamp=1640995500) - ) - ) - assert access_key_metric.access_key_id == 1 - assert access_key_metric.tunnel_time.seconds == 900 - assert access_key_metric.data_transferred.bytes == 262144 - assert access_key_metric.connection.last_traffic_seen == 1640995400 - - -class TestServerExperimentalMetric: - """Test ServerExperimentalMetric model.""" - - def test_valid_server_experimental_metric(self): - """Test valid server experimental metric creation.""" - server_metric = ServerExperimentalMetric( - tunnelTime=TunnelTime(seconds=3600), - dataTransferred=DataTransferred(bytes=1048576), - bandwidth=BandwidthInfo( - current=BandwidthData(data={"bytes": 1024}, timestamp=1640995200), - peak=BandwidthData(data={"bytes": 2048}, timestamp=1640995300) - ), - locations=[ - LocationMetric( - location="US", - tunnelTime=TunnelTime(seconds=1800), - dataTransferred=DataTransferred(bytes=524288) - ) - ] - ) - assert server_metric.tunnel_time.seconds == 3600 - assert server_metric.data_transferred.bytes == 1048576 - assert len(server_metric.locations) == 1 - - -class TestExperimentalMetrics: - """Test ExperimentalMetrics model.""" - - def test_experimental_metrics_field_aliases(self): - """Test that field aliases work correctly.""" - data = { - "server": { - "tunnelTime": {"seconds": 100}, - "dataTransferred": {"bytes": 200}, - "bandwidth": { - "current": {"data": {"bytes": 10}, "timestamp": 1640995200}, - "peak": {"data": {"bytes": 20}, "timestamp": 1640995300}, - }, - "locations": [], - }, - "accessKeys": [], # Using alias - } - - metrics = ExperimentalMetrics(**data) - assert isinstance(metrics.access_keys, list) - assert len(metrics.access_keys) == 0 - - def test_valid_experimental_metrics(self): - """Test valid experimental metrics creation.""" - data = { - "server": { - "tunnelTime": {"seconds": 3600}, - "dataTransferred": {"bytes": 1048576}, - "bandwidth": { - "current": {"data": {"bytes": 1024}, "timestamp": 1640995200}, - "peak": {"data": {"bytes": 2048}, "timestamp": 1640995300}, - }, - "locations": [ - { - "location": "US", - "asn": 12345, - "asOrg": "Test AS", - "tunnelTime": {"seconds": 1800}, - "dataTransferred": {"bytes": 524288}, - } - ], - }, - "accessKeys": [ - { - "accessKeyId": 1, - "tunnelTime": {"seconds": 900}, - "dataTransferred": {"bytes": 262144}, - "connection": { - "lastTrafficSeen": 1640995400, - "peakDeviceCount": {"data": 5, "timestamp": 1640995500}, - }, - } - ], - } - - metrics = ExperimentalMetrics(**data) - assert metrics.server.tunnel_time.seconds == 3600 - assert metrics.server.data_transferred.bytes == 1048576 - assert len(metrics.access_keys) == 1 - assert metrics.access_keys[0].access_key_id == 1 - - def test_experimental_metrics_with_empty_lists(self): - """Test experimental metrics with empty access keys list.""" - data = { - "server": { - "tunnelTime": {"seconds": 0}, - "dataTransferred": {"bytes": 0}, - "bandwidth": { - "current": {"data": {"bytes": 0}, "timestamp": 1640995200}, - "peak": {"data": {"bytes": 0}, "timestamp": 1640995300}, - }, - "locations": [], - }, - "accessKeys": [], - } - - metrics = ExperimentalMetrics(**data) - assert len(metrics.server.locations) == 0 - assert len(metrics.access_keys) == 0 - - -class TestRequestModels: - """Test request models.""" - - def test_access_key_create_request_with_method_variations(self): - """Test AccessKeyCreateRequest with different methods.""" - methods = ["aes-256-gcm", "chacha20-ietf-poly1305", None] - - for method in methods: - request = AccessKeyCreateRequest(method=method) - assert request.method == method - - def test_server_name_request_with_special_characters(self): - """Test ServerNameRequest with special characters.""" - special_names = [ - "Server-123", - "Server_with_underscores", - "Server in Russian", - "Server with spaces", - "Server@#$%" - ] - - for name in special_names: - request = ServerNameRequest(name=name) - assert request.name == name - - def test_hostname_request_variations(self): - """Test HostnameRequest with different hostname formats.""" - hostnames = [ - "example.com", - "sub.example.com", - "192.168.1.1", - "localhost", - "server-123.domain.org" - ] - - for hostname in hostnames: - request = HostnameRequest(hostname=hostname) - assert request.hostname == hostname - - def test_access_key_name_request_empty_string(self): - """Test AccessKeyNameRequest with empty string.""" - request = AccessKeyNameRequest(name="") - assert request.name == "" - - def test_metrics_enabled_request_field_alias(self): - """Test MetricsEnabledRequest field alias.""" - # Test using alias - request = MetricsEnabledRequest(metricsEnabled=True) - assert request.metrics_enabled is True - - # Test field access - request = MetricsEnabledRequest(metricsEnabled=False) - assert request.metrics_enabled is False - - def test_access_key_create_request_full(self): - """Test AccessKeyCreateRequest model with all fields.""" - request = AccessKeyCreateRequest( - name="Test", - password="pass", - port=8080, - method="aes-256-gcm", - limit=DataLimit(bytes=1024), - ) - assert request.name == "Test" - assert request.password == "pass" - assert request.port == 8080 - assert request.method == "aes-256-gcm" - assert request.limit.bytes == 1024 - - def test_access_key_create_request_empty(self): - """Test AccessKeyCreateRequest model with no fields.""" - request = AccessKeyCreateRequest() - assert request.name is None - assert request.password is None - assert request.port is None - assert request.method is None - assert request.limit is None - - def test_access_key_create_request_invalid_port(self): - """Test AccessKeyCreateRequest with invalid port.""" - with pytest.raises(ValidationError): - AccessKeyCreateRequest(port=0) - - with pytest.raises(ValidationError): - AccessKeyCreateRequest(port=65536) - - def test_server_name_request(self): - """Test ServerNameRequest model.""" - request = ServerNameRequest(name="New Server Name") - assert request.name == "New Server Name" - - def test_hostname_request(self): - """Test HostnameRequest model.""" - request = HostnameRequest(hostname="new.example.com") - assert request.hostname == "new.example.com" - - def test_port_request(self): - """Test PortRequest model.""" - request = PortRequest(port=9090) - assert request.port == 9090 - - with pytest.raises(ValidationError): - PortRequest(port=0) - - with pytest.raises(ValidationError): - PortRequest(port=65536) - - def test_port_request_boundaries(self): - """Test PortRequest boundaries.""" - request = PortRequest(port=1) - assert request.port == 1 - - request = PortRequest(port=65535) - assert request.port == 65535 - - def test_access_key_name_request(self): - """Test AccessKeyNameRequest model.""" - request = AccessKeyNameRequest(name="New Key Name") - assert request.name == "New Key Name" - - def test_data_limit_request(self): - """Test DataLimitRequest model.""" - request = DataLimitRequest(limit=DataLimit(bytes=2048)) - assert request.limit.bytes == 2048 - - def test_metrics_enabled_request(self): - """Test MetricsEnabledRequest model.""" - request = MetricsEnabledRequest(metricsEnabled=True) - assert request.metrics_enabled is True - - request = MetricsEnabledRequest(metricsEnabled=False) - assert request.metrics_enabled is False - - -class TestResponseModels: - """Test response models.""" - - def test_metrics_status_response_field_alias(self): - """Test MetricsStatusResponse field alias.""" - # Test using alias - response = MetricsStatusResponse(metricsEnabled=True) - assert response.metrics_enabled is True - - def test_error_response_with_empty_strings(self): - """Test ErrorResponse with empty strings.""" - error = ErrorResponse(code="", message="") - assert error.code == "" - assert error.message == "" - - def test_error_response_with_long_messages(self): - """Test ErrorResponse with long messages.""" - long_message = "A" * 1000 # Very long error message - error = ErrorResponse(code="LONG_ERROR", message=long_message) - assert error.code == "LONG_ERROR" - assert error.message == long_message - assert len(error.message) == 1000 - - def test_metrics_status_response(self): - """Test MetricsStatusResponse model.""" - response = MetricsStatusResponse(metricsEnabled=False) - assert response.metrics_enabled is False - - response = MetricsStatusResponse(metricsEnabled=True) - assert response.metrics_enabled is True - - def test_error_response(self): - """Test ErrorResponse model.""" - error = ErrorResponse(code="NOT_FOUND", message="Resource not found") - assert error.code == "NOT_FOUND" - assert error.message == "Resource not found" - - def test_error_response_different_codes(self): - """Test ErrorResponse with different error codes.""" - error = ErrorResponse(code="BAD_REQUEST", message="Invalid input") - assert error.code == "BAD_REQUEST" - assert error.message == "Invalid input" - - error = ErrorResponse(code="INTERNAL_ERROR", message="Server error") - assert error.code == "INTERNAL_ERROR" - assert error.message == "Server error" - - -class TestOutlineError: - """Test OutlineError base exception.""" - - def test_basic_outline_error(self): - """Test basic OutlineError creation.""" - error = OutlineError("Test error") - assert str(error) == "Test error" - assert isinstance(error, Exception) - - def test_outline_error_inheritance(self): - """Test OutlineError inheritance.""" - error = OutlineError("Test") - assert isinstance(error, Exception) - - def test_outline_error_no_message(self): - """Test OutlineError without message.""" - error = OutlineError() - assert isinstance(error, Exception) - - def test_outline_error_with_args(self): - """Test OutlineError with multiple args.""" - error = OutlineError("Error", "Additional info") - assert "Error" in str(error) - - -class TestAPIError: - """Test APIError exception.""" - - def test_basic_api_error(self): - """Test basic APIError creation.""" - error = APIError("API failed") - assert str(error) == "API failed" - assert error.status_code is None - assert error.attempt is None - - def test_api_error_with_status_code(self): - """Test APIError with status code.""" - error = APIError("Not found", status_code=404) - assert str(error) == "Not found" - assert error.status_code == 404 - - def test_api_error_with_attempt(self): - """Test APIError with attempt number.""" - error = APIError("Timeout", attempt=2) - assert str(error) == "[Attempt 2] Timeout" - assert error.attempt == 2 - - def test_api_error_with_all_params(self): - """Test APIError with all parameters.""" - error = APIError("Server error", status_code=500, attempt=3) - assert str(error) == "[Attempt 3] Server error" - assert error.status_code == 500 - assert error.attempt == 3 - - def test_api_error_inheritance(self): - """Test APIError inheritance.""" - error = APIError("Test") - assert isinstance(error, OutlineError) - assert isinstance(error, Exception) - - def test_api_error_attempt_zero(self): - """Test APIError with attempt 0.""" - error = APIError("Failed", attempt=0) - assert str(error) == "[Attempt 0] Failed" - assert error.attempt == 0 - - def test_api_error_different_status_codes(self): - """Test APIError with different status codes.""" - error_400 = APIError("Bad request", status_code=400) - assert error_400.status_code == 400 - - error_500 = APIError("Internal error", status_code=500) - assert error_500.status_code == 500 - - error_403 = APIError("Forbidden", status_code=403) - assert error_403.status_code == 403 +def test_experimental_metrics_lookup(experimental_metrics_dict): + metrics = ExperimentalMetrics(**experimental_metrics_dict) + assert metrics.get_key_metric("key-1") is not None + assert metrics.get_key_metric("missing") is None + + +def test_health_check_result_and_summary(): + result = HealthCheckResult( + healthy=True, + timestamp=1.0, + checks={"c1": {"status": "healthy"}, "c2": {"status": "unhealthy"}}, + ) + assert result.failed_checks == ["c2"] + assert result.success_rate == 0.5 + + summary = ServerSummary( + server={"id": "s"}, + access_keys_count=1, + healthy=True, + transfer_metrics={"a": 10, "b": 20}, + ) + assert summary.total_bytes_transferred == 30 + assert summary.total_gigabytes_transferred > 0 + assert summary.has_errors is False + + empty_summary = ServerSummary( + server={"id": "s"}, + access_keys_count=0, + healthy=False, + transfer_metrics=None, + error="fail", + ) + assert empty_summary.total_bytes_transferred == 0 + assert empty_summary.has_errors is True + + +def test_error_response_and_requests(): + err = ErrorResponse(code="x", message="oops") + assert str(err) == "x: oops" + + payload = DataLimitRequest(limit=DataLimit(bytes=123)).to_payload() + assert payload == {"limit": {"bytes": 123}} + + time = TunnelTime(seconds=120) + assert time.minutes == 2 + assert time.hours == 2 / 60 + + +def test_health_check_result_success_rate_empty(): + result = HealthCheckResult(healthy=True, timestamp=1.0, checks={}) + assert result.success_rate == 1.0 diff --git a/tests/test_response_parser.py b/tests/test_response_parser.py new file mode 100644 index 0000000..2945157 --- /dev/null +++ b/tests/test_response_parser.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import logging +from typing import Any, cast + +import pytest +from pydantic import BaseModel, ValidationError +from pydantic_core import InitErrorDetails + +from pyoutlineapi.common_types import JsonValue +from pyoutlineapi.exceptions import ValidationError as OutlineValidationError +from pyoutlineapi.response_parser import ResponseParser + + +class SimpleModel(BaseModel): + id: int + name: str + + +class MultiErrorModel(BaseModel): + a: int + b: int + + +def test_parse_non_dict_raises_validation_error(): + bad_data = cast(dict[str, JsonValue], cast(object, ["bad"])) + with pytest.raises(OutlineValidationError) as exc: + ResponseParser.parse(bad_data, SimpleModel) + assert exc.value.safe_details["model"] == "SimpleModel" + + +def test_parse_as_json_returns_dict() -> None: + data: dict[str, JsonValue] = {"id": 1, "name": "test"} + result = ResponseParser.parse(data, SimpleModel, as_json=True) + assert result["id"] == 1 + assert result["name"] == "test" + + +def test_parse_invalid_data_logs_and_raises( + caplog: pytest.LogCaptureFixture, +) -> None: + data: dict[str, JsonValue] = {"id": "bad"} # missing name and wrong id type + with ( + caplog.at_level( + logging.WARNING, + logger="pyoutlineapi.response_parser", + ), + pytest.raises(OutlineValidationError) as exc, + ): + ResponseParser.parse(data, SimpleModel, as_json=False) + assert exc.value.safe_details["model"] == "SimpleModel" + + +def test_parse_empty_dict_debug_log(caplog): + with ( + caplog.at_level( + logging.DEBUG, + logger="pyoutlineapi.response_parser", + ), + pytest.raises(OutlineValidationError), + ): + ResponseParser.parse({}, SimpleModel) + assert any("Parsing empty dict" in r.message for r in caplog.records) + + +def test_parse_validation_error_without_details(): + class EmptyErrorsModel(SimpleModel): + @classmethod + def model_validate(cls, obj: Any, **kwargs: Any) -> EmptyErrorsModel: + raise ValidationError.from_exception_data("EmptyErrorsModel", []) + + with pytest.raises(OutlineValidationError) as exc: + ResponseParser.parse({"id": 1, "name": "x"}, EmptyErrorsModel) + assert "no error details" in str(exc.value) + + +def test_parse_validation_error_many_details(caplog): + logger = logging.getLogger("pyoutlineapi.response_parser") + logger.setLevel(logging.DEBUG) + + class ManyErrorsModel(SimpleModel): + @classmethod + def model_validate(cls, obj: Any, **kwargs: Any) -> ManyErrorsModel: + errors: list[InitErrorDetails] = [ + { + "type": "value_error", + "loc": ("field", idx), + "input": obj, + "ctx": {"error": "boom"}, + } + for idx in range(11) + ] + raise ValidationError.from_exception_data( + "ManyErrorsModel", + errors, + ) + + with ( + caplog.at_level( + logging.DEBUG, + logger="pyoutlineapi.response_parser", + ), + pytest.raises(OutlineValidationError), + ): + ResponseParser.parse({"id": 1, "name": "x"}, ManyErrorsModel) + assert any("Validation error details" in r.message for r in caplog.records) + assert any("more error(s)" in r.message for r in caplog.records) + + +def test_parse_validation_error_multiple_fields(caplog): + logger = logging.getLogger("pyoutlineapi.response_parser") + logger.setLevel(logging.DEBUG) + with ( + caplog.at_level( + logging.DEBUG, + logger="pyoutlineapi.response_parser", + ), + pytest.raises(OutlineValidationError), + ): + ResponseParser.parse({}, MultiErrorModel) + assert any("Multiple validation errors" in r.message for r in caplog.records) + + +def test_parse_validation_error_multiple_fields_without_logging(): + logger = logging.getLogger("pyoutlineapi.response_parser") + logger.setLevel(logging.ERROR) + with pytest.raises(OutlineValidationError): + ResponseParser.parse({}, MultiErrorModel) + + +def test_parse_unexpected_exception(): + class BadModel(SimpleModel): + @classmethod + def model_validate(cls, obj: Any, **kwargs: Any) -> BadModel: + raise RuntimeError("boom") + + with pytest.raises(OutlineValidationError) as exc: + ResponseParser.parse({"id": 1, "name": "x"}, BadModel) + assert "Unexpected error during validation" in str(exc.value) + + +def test_parse_unexpected_exception_logs_error(caplog): + logger = logging.getLogger("pyoutlineapi.response_parser") + logger.setLevel(logging.ERROR) + + class BadModel(SimpleModel): + @classmethod + def model_validate(cls, obj: Any, **kwargs: Any) -> BadModel: + raise RuntimeError("boom") + + with ( + caplog.at_level( + logging.ERROR, + logger="pyoutlineapi.response_parser", + ), + pytest.raises(OutlineValidationError), + ): + ResponseParser.parse({"id": 1, "name": "x"}, BadModel) + assert any( + "Unexpected error during validation" in r.message for r in caplog.records + ) + + +def test_parse_simple_variants(caplog): + logger = logging.getLogger("pyoutlineapi.response_parser") + logger.setLevel(logging.WARNING) + assert ResponseParser.parse_simple({"success": True}) is True + assert ResponseParser.parse_simple({"success": False}) is False + + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.response_parser"): + assert ResponseParser.parse_simple({"success": "yes"}) is True + assert any("success field is not bool" in r.message for r in caplog.records) + assert ResponseParser.parse_simple({"error": "fail"}) is False + assert ResponseParser.parse_simple({"message": "fail"}) is False + assert ResponseParser.parse_simple({}) is True + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.response_parser"): + assert ResponseParser.parse_simple(["bad"]) is False + assert any("Expected dict in parse_simple" in r.message for r in caplog.records) + + +def test_parse_simple_logs_warning_for_non_dict(caplog): + logger = logging.getLogger("pyoutlineapi.response_parser") + logger.setLevel(logging.WARNING) + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.response_parser"): + assert ResponseParser.parse_simple(123) is False + assert any("Expected dict in parse_simple" in r.message for r in caplog.records) + + +def test_parse_simple_no_warning_when_logger_disabled(): + logger = logging.getLogger("pyoutlineapi.response_parser") + logger.setLevel(logging.ERROR) + assert ResponseParser.parse_simple(123) is False + + +def test_validate_response_structure(): + assert ResponseParser.validate_response_structure({"id": 1}, ["id"]) is True + assert ( + ResponseParser.validate_response_structure({"id": 1}, ["id", "name"]) is False + ) + assert ResponseParser.validate_response_structure({}, None) is True + assert ResponseParser.validate_response_structure({"id": 1}, []) is True + assert ResponseParser.validate_response_structure(["bad"], ["id"]) is False + + +def test_extract_error_message_and_is_error_response(): + assert ResponseParser.extract_error_message({"error": 1}) == "1" + assert ResponseParser.extract_error_message({"message": None}) is None + assert ResponseParser.extract_error_message({"msg": "oops"}) == "oops" + assert ResponseParser.extract_error_message({"success": True}) is None + assert ResponseParser.extract_error_message(["bad"]) is None + + assert ResponseParser.is_error_response({"error_message": "bad"}) is True + assert ResponseParser.is_error_response({"success": False}) is True + assert ResponseParser.is_error_response({"success": True}) is False + assert ResponseParser.is_error_response({}) is False + assert ResponseParser.is_error_response(["bad"]) is False diff --git a/tests/test_strict_coverage.py b/tests/test_strict_coverage.py new file mode 100644 index 0000000..3be0110 --- /dev/null +++ b/tests/test_strict_coverage.py @@ -0,0 +1,128 @@ +import asyncio +import logging +from typing import Any, cast + +import pytest + +from pyoutlineapi.audit import DefaultAuditLogger +from pyoutlineapi.exceptions import ( + APIError, + CircuitOpenError, + ConfigurationError, + OutlineConnectionError, + OutlineTimeoutError, + ValidationError, + get_safe_error_dict, +) + +# ===== Exceptions Coverage ===== + + +def test_get_safe_error_dict_all_variants(): + """Cover all branches in get_safe_error_dict.""" + + # 1. APIError with all fields + err1 = APIError("msg", status_code=400, endpoint="/test", response_data={"a": 1}) + d1 = get_safe_error_dict(err1) + assert d1["status_code"] == 400 + assert d1["is_client_error"] is True + + # 2. APIError without status_code + err2 = APIError("msg") + d2 = get_safe_error_dict(err2) + assert d2["status_code"] is None + assert "is_client_error" not in d2 + + # 3. CircuitOpenError + err3 = CircuitOpenError("msg", retry_after=10.0) + d3 = get_safe_error_dict(err3) + assert d3["retry_after"] == 10.0 + + # 4. ConfigurationError with all fields + err4 = ConfigurationError("msg", field="api_url", security_issue=True) + d4 = get_safe_error_dict(err4) + assert d4["field"] == "api_url" + assert d4["security_issue"] is True + + # 5. ConfigurationError minimal + err5 = ConfigurationError("msg") + d5 = get_safe_error_dict(err5) + assert "field" not in d5 + assert d5["security_issue"] is False + + # 6. ValidationError with all fields + err6 = ValidationError("msg", field="port", model="Server") + d6 = get_safe_error_dict(err6) + assert d6["field"] == "port" + assert d6["model"] == "Server" + + # 7. ValidationError minimal + err7 = ValidationError("msg") + d7 = get_safe_error_dict(err7) + assert "field" not in d7 + + # 8. OutlineConnectionError with all fields + err8 = OutlineConnectionError("msg", host="1.1.1.1", port=80) + d8 = get_safe_error_dict(err8) + assert d8["host"] == "1.1.1.1" + assert d8["port"] == 80 + + # 9. OutlineConnectionError minimal + err9 = OutlineConnectionError("msg") + d9 = get_safe_error_dict(err9) + assert "host" not in d9 + + # 10. OutlineTimeoutError with all fields + err10 = OutlineTimeoutError("msg", timeout=5.0, operation="op") + d10 = get_safe_error_dict(err10) + assert d10["timeout"] == 5.0 + assert d10["operation"] == "op" + + # 11. OutlineTimeoutError minimal + err11 = OutlineTimeoutError("msg") + d11 = get_safe_error_dict(err11) + assert "timeout" not in d11 + + +# ===== Audit Coverage ===== + + +@pytest.mark.asyncio +async def test_audit_logger_queue_full_coverage(caplog): + """Cover the queue full branch in alog_action.""" + logger = DefaultAuditLogger(queue_size=1) + + # Fill the queue + await logger._queue.put({"test": 1}) + + # Force full exception triggering by not consuming + # Specify logger name explicitly to ensure capture + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.audit"): + await logger.alog_action("action", "resource") + + assert "Queue full" in caplog.text + await logger.shutdown() + + +@pytest.mark.asyncio +async def test_audit_logger_shutdown_timeout_branches(caplog): + """Cover queue drain timeout logic.""" + logger = DefaultAuditLogger() + + # Start processor + await logger.alog_action("test", "res") + + # Mock queue join to timeout + async def mock_join(): + # Wait a bit to simulate work + await asyncio.sleep(0.01) + # Raise timeout to trigger the warning + raise asyncio.TimeoutError() + + cast(Any, logger._queue).join = mock_join + + with caplog.at_level(logging.WARNING, logger="pyoutlineapi.audit"): + # Use a very short timeout for the shutdown call + await logger.shutdown(timeout=0.001) + + assert "Queue did not drain" in caplog.text diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1352320 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2173 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4.0" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aioresponses" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/03/532bbc645bdebcf3b6af3b25d46655259d66ce69abba7720b71ebfabbade/aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11", size = 40253, upload-time = "2025-01-19T18:14:03.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b7/584157e43c98aa89810bc2f7099e7e01c728ecf905a66cf705106009228f/aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94", size = 12518, upload-time = "2025-01-19T18:13:59.633Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[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.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/76/a7f3e639b78601118aaa4a394db2c66ae2597fbd8c39644c32874ed11e0c/bandit-1.9.3.tar.gz", hash = "sha256:ade4b9b7786f89ef6fc7344a52b34558caec5da74cb90373aed01de88472f774", size = 4242154, upload-time = "2026-01-19T04:05:22.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/0b/8bdc52111c83e2dc2f97403dc87c0830b8989d9ae45732b34b686326fb2c/bandit-1.9.3-py3-none-any.whl", hash = "sha256:4745917c88d2246def79748bde5e08b9d5e9b92f877863d43fab70cd8814ce6a", size = 134451, upload-time = "2026-01-19T04:05:20.938Z" }, +] + +[[package]] +name = "build" +version = "1.4.0" +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/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "codeclone" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/41/0e9f6ab4c80fa17dc6e33851ebc86c48e13d8f044ba4ae20ffdc0fe7e828/codeclone-1.4.0.tar.gz", hash = "sha256:2339dfe735d40b029ebc4c1c595b459cfc5890ecf0af4fd6872a7d4e12b0617e", size = 118246, upload-time = "2026-02-12T15:37:58.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f6/5069859399f40761b9f28e35ce8866d8396f49850bd1ac506a7ed182bb62/codeclone-1.4.0-py3-none-any.whl", hash = "sha256:fe312afb62e3ef3f2ad4ed693e05fbf74063cb2b62ccc178a8ace59281f4f897", size = 83625, upload-time = "2026-02-12T15:37:57.349Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.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" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "librt" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/e9/018cfd60629e0404e6917943789800aa2231defbea540a17b90cc4547b97/librt-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db63cf3586a24241e89ca1ce0b56baaec9d371a328bd186c529b27c914c9a1ef", size = 65690, upload-time = "2026-02-12T14:51:57.761Z" }, + { url = "https://files.pythonhosted.org/packages/b5/80/8d39980860e4d1c9497ee50e5cd7c4766d8cfd90d105578eae418e8ffcbc/librt-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba9d9e60651615bc614be5e21a82cdb7b1769a029369cf4b4d861e4f19686fb6", size = 68373, upload-time = "2026-02-12T14:51:59.013Z" }, + { url = "https://files.pythonhosted.org/packages/2d/76/6e6f7a443af63977e421bd542551fec4072d9eaba02e671b05b238fe73bc/librt-0.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb4b3ad543084ed79f186741470b251b9d269cd8b03556f15a8d1a99a64b7de5", size = 197091, upload-time = "2026-02-12T14:52:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/fa064181c231334c9f4cb69eb338132d39510c8928e84beba34b861d0a71/librt-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d2720335020219197380ccfa5c895f079ac364b4c429e96952cd6509934d8eb", size = 207350, upload-time = "2026-02-12T14:52:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/e7f8438dd226305e3e5955d495114ad01448e6a6ffc0303289b4153b5fc5/librt-0.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726305d3e53419d27fc8cdfcd3f9571f0ceae22fa6b5ea1b3662c2e538f833e", size = 219962, upload-time = "2026-02-12T14:52:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2c/74086fc5d52e77107a3cc80a9a3209be6ad1c9b6bc99969d8d9bbf9fdfe4/librt-0.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3d107f603b5ee7a79b6aa6f166551b99b32fb4a5303c4dfcb4222fc6a0335e", size = 212939, upload-time = "2026-02-12T14:52:05.537Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ae/d6917c0ebec9bc2e0293903d6a5ccc7cdb64c228e529e96520b277318f25/librt-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41064a0c07b4cc7a81355ccc305cb097d6027002209ffca51306e65ee8293630", size = 221393, upload-time = "2026-02-12T14:52:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/04/97/15df8270f524ce09ad5c19cbbe0e8f95067582507149a6c90594e7795370/librt-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c6e4c10761ddbc0d67d2f6e2753daf99908db85d8b901729bf2bf5eaa60e0567", size = 216721, upload-time = "2026-02-12T14:52:08.857Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/17cbcf9b7a1bae5016d9d3561bc7169b32c3bd216c47d934d3f270602c0c/librt-0.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba581acad5ac8f33e2ff1746e8a57e001b47c6721873121bf8bbcf7ba8bd3aa4", size = 214790, upload-time = "2026-02-12T14:52:10.033Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2d/010a236e8dc4d717dd545c46fd036dcced2c7ede71ef85cf55325809ff92/librt-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bdab762e2c0b48bab76f1a08acb3f4c77afd2123bedac59446aeaaeed3d086cf", size = 237384, upload-time = "2026-02-12T14:52:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/f1c0eff3df8760dee761029efb72991c554d9f3282f1048e8c3d0eb60997/librt-0.8.0-cp310-cp310-win32.whl", hash = "sha256:6a3146c63220d814c4a2c7d6a1eacc8d5c14aed0ff85115c1dfea868080cd18f", size = 54289, upload-time = "2026-02-12T14:52:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0b/2684d473e64890882729f91866ed97ccc0a751a0afc3b4bf1a7b57094dbb/librt-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:bbebd2bba5c6ae02907df49150e55870fdd7440d727b6192c46b6f754723dde9", size = 61347, upload-time = "2026-02-12T14:52:13.793Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/42af181c89b65abfd557c1b017cba5b82098eef7bf26d1649d82ce93ccc7/librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369", size = 65314, upload-time = "2026-02-12T14:52:14.778Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4a/15a847fca119dc0334a4b8012b1e15fdc5fc19d505b71e227eaf1bcdba09/librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6", size = 68015, upload-time = "2026-02-12T14:52:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/ffc8dbd6ab68dd91b736c88529411a6729649d2b74b887f91f3aaff8d992/librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa", size = 194508, upload-time = "2026-02-12T14:52:16.835Z" }, + { url = "https://files.pythonhosted.org/packages/89/92/a7355cea28d6c48ff6ff5083ac4a2a866fb9b07b786aa70d1f1116680cd5/librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f", size = 205630, upload-time = "2026-02-12T14:52:18.58Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/54509038d7ac527828db95b8ba1c8f5d2649bc32fd8f39b1718ec9957dce/librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef", size = 218289, upload-time = "2026-02-12T14:52:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/6d/17/0ee0d13685cefee6d6f2d47bb643ddad3c62387e2882139794e6a5f1288a/librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9", size = 211508, upload-time = "2026-02-12T14:52:21.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a8/1714ef6e9325582e3727de3be27e4c1b2f428ea411d09f1396374180f130/librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4", size = 219129, upload-time = "2026-02-12T14:52:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/89/d3/2d9fe353edff91cdc0ece179348054a6fa61f3de992c44b9477cb973509b/librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6", size = 213126, upload-time = "2026-02-12T14:52:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8e/9f5c60444880f6ad50e3ff7475e5529e787797e7f3ad5432241633733b92/librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da", size = 212279, upload-time = "2026-02-12T14:52:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/eb/d4a2cfa647da3022ae977f50d7eda1d91f70d7d1883cf958a4b6ef689eab/librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1", size = 234654, upload-time = "2026-02-12T14:52:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/26b978861c7983b036a3aea08bdbb2ec32bbaab1ad1d57c5e022be59afc1/librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258", size = 54603, upload-time = "2026-02-12T14:52:27.342Z" }, + { url = "https://files.pythonhosted.org/packages/d0/78/f194ed7c48dacf875677e749c5d0d1d69a9daa7c994314a39466237fb1be/librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817", size = 61730, upload-time = "2026-02-12T14:52:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/97/ee/ad71095478d02137b6f49469dc808c595cfe89b50985f6b39c5345f0faab/librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4", size = 52274, upload-time = "2026-02-12T14:52:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, + { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, + { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, + { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, + { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, + { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, + { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, + { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, + { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markdown2" +version = "2.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/f8/b2ae8bf5f28f9b510ae097415e6e4cb63226bb28d7ee01aec03a755ba03b/markdown2-2.5.4.tar.gz", hash = "sha256:a09873f0b3c23dbfae589b0080587df52ad75bb09a5fa6559147554736676889", size = 145652, upload-time = "2025-07-27T16:16:24.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/06/2697b5043c3ecb720ce0d243fc7cf5024c0b5b1e450506e9b21939019963/markdown2-2.5.4-py3-none-any.whl", hash = "sha256:3c4b2934e677be7fec0e6f2de4410e116681f4ad50ec8e5ba7557be506d3f439", size = 49954, upload-time = "2025-07-27T16:16:23.026Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" }, + { url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" }, + { url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" }, + { url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" }, + { url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, + { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pdoc" +version = "16.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown2" }, + { name = "markupsafe" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/fe/ab3f34a5fb08c6b698439a2c2643caf8fef0d61a86dd3fdcd5501c670ab8/pdoc-16.0.0.tar.gz", hash = "sha256:fdadc40cc717ec53919e3cd720390d4e3bcd40405cb51c4918c119447f913514", size = 111890, upload-time = "2025-10-27T16:02:16.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/a1/56a17b7f9e18c2bb8df73f3833345d97083b344708b97bab148fdd7e0b82/pdoc-16.0.0-py3-none-any.whl", hash = "sha256:070b51de2743b9b1a4e0ab193a06c9e6c12cf4151cf9137656eebb16e8556628", size = 100014, upload-time = "2025-10-27T16:02:15.007Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyoutlineapi" +version = "0.4.0" +source = { editable = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, +] + +[package.dev-dependencies] +dev = [ + { name = "aioresponses" }, + { name = "bandit" }, + { name = "build" }, + { name = "codeclone" }, + { name = "mypy" }, + { name = "pdoc" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "rich" }, + { name = "ruff" }, + { name = "twine" }, + { name = "types-aiofiles" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.13.3" }, + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pydantic-settings", specifier = ">=2.12.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "aioresponses", specifier = ">=0.7.8" }, + { name = "bandit", specifier = ">=1.8" }, + { name = "build", specifier = ">=1.2.0" }, + { name = "codeclone", specifier = ">=1.4.0" }, + { name = "mypy", specifier = ">=1.13" }, + { name = "pdoc", specifier = ">=15.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-asyncio", specifier = ">=0.25" }, + { name = "pytest-cov", specifier = ">=6.0" }, + { name = "pytest-timeout", specifier = ">=2.3" }, + { name = "pytest-xdist", specifier = ">=3.6" }, + { name = "rich", specifier = ">=14.2" }, + { name = "ruff", specifier = ">=0.8" }, + { name = "twine", specifier = ">=5.0.0" }, + { name = "types-aiofiles", specifier = ">=24.1" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "stevedore" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "types-aiofiles" +version = "25.1.0.20251011" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6c/6d23908a8217e36704aa9c79d99a620f2fdd388b66a4b7f72fbc6b6ff6c6/types_aiofiles-25.1.0.20251011.tar.gz", hash = "sha256:1c2b8ab260cb3cd40c15f9d10efdc05a6e1e6b02899304d80dfa0410e028d3ff", size = 14535, upload-time = "2025-10-11T02:44:51.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/0f/76917bab27e270bb6c32addd5968d69e558e5b6f7fb4ac4cbfa282996a96/types_aiofiles-25.1.0.20251011-py3-none-any.whl", hash = "sha256:8ff8de7f9d42739d8f0dadcceeb781ce27cd8d8c4152d4a7c52f6b20edb8149c", size = 14338, upload-time = "2025-10-11T02:44:50.054Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[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" }, +]