-pyoutlineapi
+ +-
+
- + DefaultAuditLogger + +
- + alog_action + +
- + log_action + +
- + shutdown + +
-
+
- + enable_logging + +
- + enable_circuit_breaker + +
- + timeout + +
-
+
- + server + +
- + access_keys + +
- + get_key_metric + +
-
+
- + healthy + +
- + timestamp + +
- + checks + +
- + failed_checks + +
- + success_rate + +
-
+
- + hostname + +
-
+
- + location + +
- + asn + +
- + as_org + +
- + tunnel_time + +
- + data_transferred + +
-
+
- + MetricsCollector + +
- + increment + +
- + timing + +
- + gauge + +
-
+
- + metrics_enabled + +
-
+
- + metrics_enabled + +
-
+
- + MultiServerManager + +
- + server_count + +
- + active_servers + +
- + get_server_names + +
- + get_client + +
- + get_all_clients + +
- + health_check_all + +
- + get_healthy_servers + +
- + get_status_summary + +
-
+
- + alog_action + +
- + log_action + +
- + shutdown + +
-
+
- + api_url + +
- + cert_sha256 + +
- + timeout + +
- + retry_attempts + +
- + max_connections + +
- + rate_limit + +
- + user_agent + +
- + enable_circuit_breaker + +
- + enable_logging + +
- + json_format + +
- + allow_private_networks + +
- + resolve_dns_for_ssrf + +
- + circuit_failure_threshold + +
- + circuit_recovery_timeout + +
- + circuit_success_threshold + +
- + circuit_call_timeout + +
- + validate_api_url + +
- + validate_cert + +
- + validate_user_agent + +
- + validate_config + +
- + get_sanitized_config + +
- + model_copy_immutable + +
- + circuit_config + +
- + from_env + +
- + create_minimal + +
-
+
- + OutlineConnectionError + +
- + host + +
- + port + +
-
+
- + OutlineError + +
- + details + +
- + safe_details + +
- + is_retryable + +
- + default_retry_delay + +
-
+
- + OutlineTimeoutError + +
- + timeout + +
- + operation + +
-
+
- + port + +
-
+
- + enable_circuit_breaker + +
- + enable_logging + +
- + enforce_security + +
-
+
- + parse + +
- + parse_simple + +
- + validate_response_structure + +
- + extract_error_message + +
- + is_error_response + +
-
+
- + generate_correlation_id + +
- + generate_request_id + +
-
+
- + name + +
- + server_id + +
- + metrics_enabled + +
- + created_timestamp_ms + +
- + port_for_new_access_keys + +
- + hostname_for_access_keys + +
- + access_key_data_limit + +
- + version + +
- + validate_name + +
- + has_global_limit + +
- + created_timestamp_seconds + +
-
+
- + tunnel_time + +
- + data_transferred + +
- + bandwidth + +
- + locations + +
-
+
- + bytes_transferred_by_user_id + +
- + total_bytes + +
- + total_gigabytes + +
- + user_count + +
- + get_user_bytes + +
- + top_users + +
-
+
- + name + +
-
+
- + server + +
- + access_keys_count + +
- + healthy + +
- + transfer_metrics + +
- + experimental_metrics + +
- + error + +
- + total_bytes_transferred + +
- + total_gigabytes_transferred + +
- + has_errors + +
-
+
- + seconds + +
-
+
- + ValidationError + +
- + field + +
- + model + +
-
+
- + validate_cert_fingerprint + +
- + validate_port + +
- + validate_name + +
- + validate_url + +
- + validate_string_not_empty + +
- + validate_non_negative + +
- + validate_since + +
- + validate_key_id + +
- + sanitize_url_for_logging + +
- + sanitize_endpoint_for_logging + +
+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:
+ ++ ++ +
Source code repository:
+ ++ ++ +
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)") +
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 +
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) +
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.
+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
+
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
+
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
+
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
+Port number (1-65535)
+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
+
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 +
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
+
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
+
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
+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
+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
+
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
+
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
+
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)
+
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
+
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
+
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
+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
+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.
+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() +
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
+
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
+
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
+
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() +
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() +
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 + }
+
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": [] + }
+
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": {...} + }
+
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.
+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
+
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.
+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) +
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).
+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).
+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
+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
+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
+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).
+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.
+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)
+
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)
+
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
+
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 +
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 +
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
+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.
+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 +... ) +
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 +
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.
+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.
+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
+
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.
+Size in bytes
+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
+
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
+
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
+
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.
+
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
+
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
+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.
+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) +
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) +
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) +
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) +
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 +
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
+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
+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
+
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.
+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
+
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)
+
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
+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
+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.
+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) +
76 def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None: +77 """Increment counter metric.""" +78 ... +
Increment counter metric.
+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
+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
+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 +
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 +
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
+
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
+
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
+
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 +
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
+
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
+
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
+
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
+
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.
+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.
+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.
+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.
+101 def increment(self, metric: str, *, tags: MetricsTags | None = None) -> None: +102 """No-op increment (zero overhead).""" +
No-op increment (zero overhead).
+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).
+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).
+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.
+SHA-256 certificate fingerprint
+Allow private or local network addresses in api_url
+Resolve DNS for SSRF checks (strict mode)
+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 +
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 +
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 +
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
+
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
+
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) +
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
+
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 +... ) +
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 +... ) +
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 +
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 +
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'} +
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 +
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_detailsinstead.
Returns:
+ +++Copy of internal details dictionary
+
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
+
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 +
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 +
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
+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
+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 +
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 +
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.
+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) +
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 +
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.
+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 +
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 +
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.
+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
+
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
+
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
+Unix timestamp in milliseconds
+Port number (1-65535)
+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 +
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
+
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
+
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
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() -
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
- -- - 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.
+ + ++ + + + 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") -- --- -@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") -- --@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
- ---@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)
-+ + + + 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
- -- - - - +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.
--- - -- 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.
--- port: int + +-- -- -- ---- 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
--- 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 + ...
+- - - - +-+
-- 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.
- --- 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
- -- --@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:
- ---- 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 + +-+ + - - - + + -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 resultRequest 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, ...} +- --- 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.
- ---- 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:
- ---- 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.
- ---- 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 +-- 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
+--- 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] + +-+ + + + def + print_type_info() -> None: + + -- - - + +-277def print_type_info() -> None: +278 """Print information about available type aliases for advanced usage.""" +279 info = """ +280🎯 PyOutlineAPI Type Aliases for Advanced Usage +281=============================================== +282 +283For creating custom AuditLogger: +284 from pyoutlineapi import AuditLogger, AuditDetails +285 +286 class MyAuditLogger: +287 def log_action( +288 self, +289 action: str, +290 resource: str, +291 *, +292 details: AuditDetails | None = None, +293 ... +294 ) -> None: ... +295 +296 async def alog_action( +297 self, +298 action: str, +299 resource: str, +300 *, +301 details: AuditDetails | None = None, +302 ... +303 ) -> None: ... +304 +305For creating custom MetricsCollector: +306 from pyoutlineapi import MetricsCollector, MetricsTags +307 +308 class MyMetrics: +309 def increment( +310 self, +311 metric: str, +312 *, +313 tags: MetricsTags | None = None +314 ) -> None: ... +315 +316Available Type Aliases: +317 - TimestampMs, TimestampSec # Unix timestamps +318 - JsonPayload, ResponseData # JSON data types +319 - QueryParams # URL query parameters +320 - AuditDetails # Audit log details +321 - MetricsTags # Metrics tags +322 +323Constants and Validators: +324 from pyoutlineapi import Constants, Validators +325 +326 # Access constants +327 Constants.RETRY_STATUS_CODES +328 Constants.MIN_PORT, Constants.MAX_PORT +329 +330 # Use validators +331 Validators.validate_port(8080) +332 Validators.validate_key_id("my-key") +333 +334Utility Classes: +335 from pyoutlineapi import ( +336 CredentialSanitizer, +337 SecureIDGenerator, +338 ResponseParser, +339 ) +340 +341 # Sanitize sensitive data +342 safe_url = CredentialSanitizer.sanitize(url) +343 +344 # Generate secure IDs +345 secure_id = SecureIDGenerator.generate() +346 +347 # Parse API responses +348 parsed = ResponseParser.parse(data, Model) +349 +350📖 Documentation: https://github.com/orenlab/pyoutlineapi +351 """ +352 print(info) +-- 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.examplefile with all available configuration options.-- name: str + +-+ + - - - + + -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] = -{} - -- - -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;uConfiguration 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
+0&&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;e 1;){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();o 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\nThis software is licensed under the MIT License.
\n\nYou can find the full license text at:
\n\n\n \n\n\nSource code repository:
\n\n\n \n\n"}, {"fullname": "pyoutlineapi.AsyncOutlineClient", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient", "kind": "class", "doc": "Asynchronous client for the Outline VPN Server API.
\n\nArguments:
\n\n\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)
\nExamples:
\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": "\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()\nFactory method that returns an async context manager.
\n\nThis 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\nArguments:
\n\n\n
\n\n- force: Force health check even if recently performed
\nReturns:
\n\n\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": "True if server is healthy
\nGet server information.
\n\nReturns:
\n\n\n\n\nServer information including name, ID, and configuration.
\nExamples:
\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": "\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}")\nRename the server.
\n\nArguments:
\n\n\n
\n\n- name: New server name
\nReturns:
\n\n\n\n\nTrue if successful
\nExamples:
\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": "\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")\nSet server hostname for access keys.
\n\nArguments:
\n\n\n
\n\n- hostname: New hostname or IP address
\nReturns:
\n\n\n\n\nTrue if successful
\nRaises:
\n\n\n
\n\n- APIError: If hostname is invalid
\nExamples:
\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": "\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")\nSet default port for new access keys.
\n\nArguments:
\n\n\n
\n\n- port: Port number (1025-65535)
\nReturns:
\n\n\n\n\nTrue if successful
\nRaises:
\n\n\n
\n\n- APIError: If port is invalid or in use
\nExamples:
\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": "\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)\nGet whether metrics collection is enabled.
\n\nReturns:
\n\n\n\n\nCurrent metrics collection status
\nExamples:
\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": "\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")\nEnable or disable metrics collection.
\n\nArguments:
\n\n\n
\n\n- enabled: Whether to enable metrics
\nReturns:
\n\n\n\n\nTrue if successful
\nExamples:
\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": "\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()\nGet transfer metrics for all access keys.
\n\nReturns:
\n\n\n\n\nTransfer metrics data for each access key
\nExamples:
\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": "\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")\nGet experimental server metrics.
\n\nArguments:
\n\n\n
\n\n- since: Required time range filter (e.g., \"24h\", \"7d\", \"30d\", or ISO timestamp)
\nReturns:
\n\n\n\n\nDetailed server and access key metrics
\nExamples:
\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": "\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")\nCreate a new access key.
\n\nArguments:
\n\n\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
\nReturns:
\n\n\n\n\nNew access key details
\nExamples:
\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": "\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}")\nCreate a new access key with specific ID.
\n\nArguments:
\n\n\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
\nReturns:
\n\n\n\n\nNew access key details
\nExamples:
\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": "\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... )\nGet all access keys.
\n\nReturns:
\n\n\n\n\nList of all access keys
\nExamples:
\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": "\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")\nGet specific access key.
\n\nArguments:
\n\n\n
\n\n- key_id: Access key ID
\nReturns:
\n\n\n\n\nAccess key details
\nRaises:
\n\n\n
\n\n- APIError: If key doesn't exist
\nExamples:
\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": "\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}")\nRename access key.
\n\nArguments:
\n\n\n
\n\n- key_id: Access key ID
\n- name: New name
\nReturns:
\n\n\n\n\nTrue if successful
\nRaises:
\n\n\n
\n\n- APIError: If key doesn't exist
\nExamples:
\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": "\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"\nDelete access key.
\n\nArguments:
\n\n\n
\n\n- key_id: Access key ID
\nReturns:
\n\n\n\n\nTrue if successful
\nRaises:
\n\n\n
\n\n- APIError: If key doesn't exist
\nExamples:
\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": "\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")\nSet data transfer limit for access key.
\n\nArguments:
\n\n\n
\n\n- key_id: Access key ID
\n- bytes_limit: Limit in bytes (must be non-negative)
\nReturns:
\n\n\n\n\nTrue if successful
\nRaises:
\n\n\n
\n\n- APIError: If key doesn't exist or limit is invalid
\nExamples:
\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": "\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\nRemove data transfer limit from access key.
\n\nArguments:
\n\n\n
\n\n- key_id: Access key ID
\nReturns:
\n\n\n\n\nTrue if successful
\nRaises:
\n\n\n
\n\n- APIError: If key doesn't exist
\nExamples:
\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": "\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")\nSet global data transfer limit for all access keys.
\n\nArguments:
\n\n\n
\n\n- bytes_limit: Limit in bytes (must be non-negative)
\nReturns:
\n\n\n\n\nTrue if successful
\nExamples:
\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": "\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)\nRemove global data transfer limit.
\n\nReturns:
\n\n\n\n\nTrue if successful
\nExamples:
\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": "\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()\nCreate multiple access keys in batch.
\n\nArguments:
\n\n\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.
\nReturns:
\n\n\n\n\nList of created keys or exceptions
\nExamples:
\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": "\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)\nGet comprehensive server summary including info, metrics, and key count.
\n\nArguments:
\n\n\n
\n\n- metrics_since: Time range for experimental metrics (default: \"24h\")
\nReturns:
\n\n\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": "Dictionary with server info, health status, and statistics
\nConfigure logging for the client.
\n\nArguments:
\n\n\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": "- level: Logging level (DEBUG, INFO, WARNING, ERROR)
\n- format_string: Custom format string for log messages
\nCheck 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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyList", "modulename": "pyoutlineapi", "qualname": "AccessKeyList", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.AccessKeyNameRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyNameRequest", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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\nThis class' __get__ returns the wrapped item's __get__ result,\nwhich makes it transparent for classmethods and staticmethods.
\n\nAttributes:
\n\n\n
\n", "signature": "(unknown):", "funcdef": "def"}, {"fullname": "pyoutlineapi.DataLimit.model_config", "modulename": "pyoutlineapi", "qualname": "DataLimit.model_config", "kind": "variable", "doc": "- wrapped: The decorator that has to be wrapped.
\n- decorator_info: The decorator info.
\n- shim: A wrapper function to wrap V1 style function.
\nConfiguration for the model, should be a dictionary conforming to [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.DataLimitRequest", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ErrorResponse", "modulename": "pyoutlineapi", "qualname": "ErrorResponse", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ExperimentalMetrics", "modulename": "pyoutlineapi", "qualname": "ExperimentalMetrics", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.HostnameRequest", "modulename": "pyoutlineapi", "qualname": "HostnameRequest", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsEnabledRequest", "modulename": "pyoutlineapi", "qualname": "MetricsEnabledRequest", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.MetricsStatusResponse", "modulename": "pyoutlineapi", "qualname": "MetricsStatusResponse", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.PortRequest", "modulename": "pyoutlineapi", "qualname": "PortRequest", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerMetrics", "modulename": "pyoutlineapi", "qualname": "ServerMetrics", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\n", "annotation": ": ClassVar[pydantic.config.ConfigDict]", "default_value": "{}"}, {"fullname": "pyoutlineapi.ServerNameRequest", "modulename": "pyoutlineapi", "qualname": "ServerNameRequest", "kind": "class", "doc": "ConfigDict][pydantic.config.ConfigDict].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 [
\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": "ConfigDict][pydantic.config.ConfigDict].PyOutlineAPI: A modern, async-first Python client for the Outline VPN Server API.
\n\nCopyright (c) 2025 Denis Rozhnovskiy pytelemonbot@mail.ru\nAll rights reserved.
\n\nThis software is licensed under the MIT License.
\n\nYou can find the full license text at:
\n\n\n \n\n\nSource code repository:
\n\n\n \n\n\nQuick Start:
\n\n\n\n\n\nfrom 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()\nAdvanced Usage - Type Hints:
\n\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": "\nfrom 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")\nHTTP API request failure.
\n\nAutomatically determines retry eligibility based on HTTP status code.
\n\nAttributes:
\n\n\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)
\nExample:
\n\n\n\n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.APIError.__init__": {"fullname": "pyoutlineapi.APIError.__init__", "modulename": "pyoutlineapi", "qualname": "APIError.__init__", "kind": "function", "doc": "\n\n\n>>> error = APIError("Not found", status_code=404, endpoint="/server")\n>>> error.is_client_error # True\n>>> error.is_retryable # False\nInitialize API error with sanitized endpoint.
\n\nArguments:
\n\n\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": "- 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)
\nCheck 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\nReturns:
\n\n\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": "True if status code is 400-499
\nCheck if error is a server error (5xx status).
\n\nReturns:
\n\n\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": "True if status code is 500-599
\nCheck if error is a rate limit error (429 status).
\n\nReturns:
\n\n\n\n", "annotation": ": bool"}, "pyoutlineapi.AccessKey": {"fullname": "pyoutlineapi.AccessKey", "modulename": "pyoutlineapi", "qualname": "AccessKey", "kind": "class", "doc": "True if status code is 429
\nAccess key model matching API schema with optimized properties.
\n\nSCHEMA: 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\nParameters
\n\n\n
\n\n- v: Name value
\nReturns
\n\n\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": "Validated name or None
\nValidate key ID.
\n\nParameters
\n\n\n
\n\n- v: Key ID
\nReturns
\n\n\n\n\nValidated key ID
\nRaises
\n\n\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": "- ValueError: If ID is invalid
\nCheck if key has data limit (optimized None check).
\n\nReturns
\n\n\n\n", "annotation": ": bool"}, "pyoutlineapi.AccessKey.display_name": {"fullname": "pyoutlineapi.AccessKey.display_name", "modulename": "pyoutlineapi", "qualname": "AccessKey.display_name", "kind": "variable", "doc": "True if data limit exists
\nGet display name with optimized conditional.
\n\nReturns
\n\n\n\n", "annotation": ": str"}, "pyoutlineapi.AccessKeyCreateRequest": {"fullname": "pyoutlineapi.AccessKeyCreateRequest", "modulename": "pyoutlineapi", "qualname": "AccessKeyCreateRequest", "kind": "class", "doc": "Display name
\nRequest model for creating access keys.
\n\nSCHEMA: 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\nSCHEMA: 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\nNOTE: Cached because list is immutable after creation
\n\nReturns
\n\n\n\n", "annotation": ": int"}, "pyoutlineapi.AccessKeyList.is_empty": {"fullname": "pyoutlineapi.AccessKeyList.is_empty", "modulename": "pyoutlineapi", "qualname": "AccessKeyList.is_empty", "kind": "variable", "doc": "Key count
\nCheck if list is empty (uses cached count).
\n\nReturns
\n\n\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": "True if no keys
\nGet key by ID with early return optimization.
\n\nParameters
\n\n\n
\n\n- key_id: Access key ID
\nReturns
\n\n\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": "Access key or None if not found
\nGet keys by name with optimized list comprehension.
\n\nParameters
\n\n\n
\n\n- name: Key name
\nReturns
\n\n\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": "List of matching keys (may be multiple)
\nGet keys with data limits (optimized comprehension).
\n\nReturns
\n\n\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": "List of keys with limits
\nGet keys without data limits (optimized comprehension).
\n\nReturns
\n\n\n\n", "signature": "(self) -> list[pyoutlineapi.models.AccessKey]:", "funcdef": "def"}, "pyoutlineapi.AccessKeyMetric": {"fullname": "pyoutlineapi.AccessKeyMetric", "modulename": "pyoutlineapi", "qualname": "AccessKeyMetric", "kind": "class", "doc": "List of keys without limits
\nPer-key experimental metrics.
\n\nSCHEMA: 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\nSCHEMA: 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\nUses structural pattern matching for configuration resolution.
\n\nParameters
\n\n\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.)
\nRaises
\n\n\n
\n\n- ConfigurationError: If configuration is invalid
\nExample:
\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": "\n\n\n>>> async with AsyncOutlineClient.from_env() as client:\n... info = await client.get_server_info()\nGet immutable copy of configuration.
\n\nReturns
\n\n\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": "Deep copy of configuration
\nDelegate to config's sanitized representation.
\n\nSee: OutlineClientConfig.get_sanitized_config().
\n\nReturns
\n\n\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": "Sanitized configuration from underlying config object
\nGet JSON format preference.
\n\nReturns
\n\n\n\n", "annotation": ": bool"}, "pyoutlineapi.AsyncOutlineClient.create": {"fullname": "pyoutlineapi.AsyncOutlineClient.create", "modulename": "pyoutlineapi", "qualname": "AsyncOutlineClient.create", "kind": "function", "doc": "True if raw JSON format is preferred
\nCreate and initialize client as async context manager.
\n\nAutomatically handles initialization and cleanup.\nRecommended way to create clients in async contexts.
\n\nParameters
\n\n\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
\nRaises
\n\n\n
\n\n- ConfigurationError: If configuration is invalid
\nExample:
\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": "\n\n\n>>> async with AsyncOutlineClient.from_env() as client:\n... keys = await client.get_access_keys()\nCreate client from environment variables.
\n\nReads configuration from environment or .env file.\nModern approach using **overrides for runtime configuration.
\n\nParameters
\n\n\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.)
\nReturns
\n\n\n\n\nConfigured client instance
\nRaises
\n\n\n
\n\n- ConfigurationError: If environment configuration is invalid
\nExample:
\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": "\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()\nPerform basic health check.
\n\nNon-intrusive check that tests server connectivity without\nmodifying any state. Returns comprehensive health metrics.
\n\nReturns
\n\n\n\n\nHealth check result dictionary with response time
\nExample result:
\n\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": "{\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 }
\nGet comprehensive server overview.
\n\nAggregates multiple API calls into a single summary.\nContinues on partial failures to return maximum information.\nExecutes non-dependent calls concurrently for performance.
\n\nReturns
\n\n\n\n\nServer summary dictionary with aggregated data
\nExample result:
\n\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": "{\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 }
\nGet current client status (synchronous).
\n\nReturns immediate status without making API calls.\nUseful for monitoring and debugging.
\n\nReturns
\n\n\n\n\nStatus dictionary with all client metrics
\nExample result:
\n\n\n\n", "signature": "(self) -> dict[str, typing.Any]:", "funcdef": "def"}, "pyoutlineapi.AuditContext": {"fullname": "pyoutlineapi.AuditContext", "modulename": "pyoutlineapi", "qualname": "AuditContext", "kind": "class", "doc": "{\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 }
\nImmutable audit context extracted from function call.
\n\nUses 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\nParameters
\n\n\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)
\nReturns
\n\n\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": "Complete audit context
\nProtocol for audit logging implementations.
\n\nDesigned 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\nSCHEMA: 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\nSCHEMA: 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\nSCHEMA: 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\nImmutable 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\nUses 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\nReturns
\n\n\n\n", "annotation": ": float"}, "pyoutlineapi.CircuitMetrics.failure_rate": {"fullname": "pyoutlineapi.CircuitMetrics.failure_rate", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.failure_rate", "kind": "variable", "doc": "Success rate as decimal (0.0 to 1.0)
\nCalculate failure rate (O(1), no allocations).
\n\nReturns
\n\n\n\n", "annotation": ": float"}, "pyoutlineapi.CircuitMetrics.to_dict": {"fullname": "pyoutlineapi.CircuitMetrics.to_dict", "modulename": "pyoutlineapi", "qualname": "CircuitMetrics.to_dict", "kind": "function", "doc": "Failure rate as decimal (0.0 to 1.0)
\nConvert metrics to dictionary for serialization.
\n\nPre-computes rates to avoid repeated calculations.
\n\nReturns
\n\n\n\n", "signature": "(self) -> dict[str, int | float]:", "funcdef": "def"}, "pyoutlineapi.CircuitOpenError": {"fullname": "pyoutlineapi.CircuitOpenError", "modulename": "pyoutlineapi", "qualname": "CircuitOpenError", "kind": "class", "doc": "Dictionary representation
\nCircuit breaker is open due to repeated failures.
\n\nIndicates temporary service unavailability. Clients should wait\nfor
\n\nretry_afterseconds before retrying.Attributes:
\n\n\n
\n\n- retry_after: Seconds to wait before retry
\nExample:
\n\n\n\n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.CircuitOpenError.__init__": {"fullname": "pyoutlineapi.CircuitOpenError.__init__", "modulename": "pyoutlineapi", "qualname": "CircuitOpenError.__init__", "kind": "function", "doc": "\n\n\n>>> error = CircuitOpenError("Circuit open", retry_after=60.0)\n>>> error.is_retryable # True\n>>> error.retry_after # 60.0\nInitialize circuit open error.
\n\nArguments:
\n\n\n
\n\n- message: Error message
\n- retry_after: Seconds to wait before retry
\nRaises:
\n\n\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": "- ValueError: If retry_after is negative
\nSuggested delay before retry.
\n", "annotation": ": float"}, "pyoutlineapi.CircuitState": {"fullname": "pyoutlineapi.CircuitState", "modulename": "pyoutlineapi", "qualname": "CircuitState", "kind": "class", "doc": "Circuit breaker states.
\n\nCLOSED: 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\nAll 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\nAttributes:
\n\n\n
\n\n- field: Configuration field name that failed
\n- security_issue: Whether this is a security-related issue
\nExample:
\n\n\n\n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.ConfigurationError.__init__": {"fullname": "pyoutlineapi.ConfigurationError.__init__", "modulename": "pyoutlineapi", "qualname": "ConfigurationError.__init__", "kind": "function", "doc": "\n\n\n>>> error = ConfigurationError(\n... "Missing API URL", field="api_url", security_issue=True\n... )\nInitialize configuration error.
\n\nArguments:
\n\n\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": "- message: Error message
\n- field: Configuration field name
\n- security_issue: Whether this is a security issue
\nApplication-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\nParameters
\n\n\n
\n\n- text: Text that may contain credentials
\nReturns
\n\n\n\n", "signature": "(cls, text: str) -> str:", "funcdef": "def"}, "pyoutlineapi.DataLimit": {"fullname": "pyoutlineapi.DataLimit", "modulename": "pyoutlineapi", "qualname": "DataLimit", "kind": "class", "doc": "Sanitized text
\nData 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\nParameters
\n\n\n
\n\n- kb: Size in kilobytes
\nReturns
\n\n\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": "DataLimit instance
\nCreate DataLimit from megabytes.
\n\nParameters
\n\n\n
\n\n- mb: Size in megabytes
\nReturns
\n\n\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": "DataLimit instance
\nCreate DataLimit from gigabytes.
\n\nParameters
\n\n\n
\n\n- gb: Size in gigabytes
\nReturns
\n\n\n\n", "signature": "(cls, gb: float) -> Self:", "funcdef": "def"}, "pyoutlineapi.DataLimitRequest": {"fullname": "pyoutlineapi.DataLimitRequest", "modulename": "pyoutlineapi", "qualname": "DataLimitRequest", "kind": "class", "doc": "DataLimit instance
\nRequest model for setting data limit.
\n\nNote:
\n\n\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": "The API expects the DataLimit object directly.\n Use to_payload() to produce the correct request body.
\nConvert to API request payload.
\n\nReturns
\n\n\n\n", "signature": "(self) -> dict[str, int]:", "funcdef": "def"}, "pyoutlineapi.DataTransferred": {"fullname": "pyoutlineapi.DataTransferred", "modulename": "pyoutlineapi", "qualname": "DataTransferred", "kind": "class", "doc": "Payload dict with bytes field
\nData transfer metric with byte conversions.
\n\nSCHEMA: 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\nParameters
\n\n\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": "- 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)
\nLog auditable action asynchronously with automatic batching.
\n\nParameters
\n\n\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": "- 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)
\nLog auditable action synchronously (fallback method).
\n\nParameters
\n\n\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": "- 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)
\nGracefully shutdown audit logger with queue draining.
\n\nParameters
\n\n\n
\n", "signature": "(self, *, timeout: float = 5.0) -> None:", "funcdef": "async def"}, "pyoutlineapi.DevelopmentConfig": {"fullname": "pyoutlineapi.DevelopmentConfig", "modulename": "pyoutlineapi", "qualname": "DevelopmentConfig", "kind": "class", "doc": "- timeout: Maximum time to wait for queue to drain (seconds)
\nDevelopment configuration with relaxed security.
\n\nOptimized for local development and testing with:
\n\n\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": "- Extended timeouts for debugging
\n- Detailed logging enabled by default
\n- Circuit breaker disabled for easier testing
\nError response with optimized string formatting.
\n\nSCHEMA: 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\nSCHEMA: 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\nParameters
\n\n\n
\n\n- key_id: Access key ID
\nReturns
\n\n\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": "Key metrics or None if not found
\nHealth 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\nReturns
\n\n\n\n", "annotation": ": list[str]"}, "pyoutlineapi.HealthCheckResult.success_rate": {"fullname": "pyoutlineapi.HealthCheckResult.success_rate", "modulename": "pyoutlineapi", "qualname": "HealthCheckResult.success_rate", "kind": "variable", "doc": "List of failed check names
\nCalculate success rate (uses cached failed_checks).
\n\nReturns
\n\n\n\n", "annotation": ": float"}, "pyoutlineapi.HostnameRequest": {"fullname": "pyoutlineapi.HostnameRequest", "modulename": "pyoutlineapi", "qualname": "HostnameRequest", "kind": "class", "doc": "Success rate (0.0 to 1.0)
\nRequest model for setting hostname.
\n\nSCHEMA: 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\nSCHEMA: 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\nAllows 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\nSCHEMA: 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\nReturns 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\nFeatures:
\n\n\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
\nLimits:
\n\n\n
\n"}, "pyoutlineapi.MultiServerManager.__init__": {"fullname": "pyoutlineapi.MultiServerManager.__init__", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.__init__", "kind": "function", "doc": "- Maximum 50 servers (configurable via _MAX_SERVERS)
\n- Automatic cleanup with weak references
\nInitialize multiserver manager.
\n\nParameters
\n\n\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)
\nRaises
\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.MultiServerManager.server_count": {"fullname": "pyoutlineapi.MultiServerManager.server_count", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.server_count", "kind": "variable", "doc": "- ConfigurationError: If too many servers or invalid configs
\nGet total number of configured servers.
\n\nReturns
\n\n\n\n", "annotation": ": int"}, "pyoutlineapi.MultiServerManager.active_servers": {"fullname": "pyoutlineapi.MultiServerManager.active_servers", "modulename": "pyoutlineapi", "qualname": "MultiServerManager.active_servers", "kind": "variable", "doc": "Number of servers
\nGet number of active (connected) servers.
\n\nReturns
\n\n\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": "Number of active servers
\nGet list of sanitized server URLs.
\n\nURLs are sanitized to remove sensitive path information.
\n\nReturns
\n\n\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": "List of safe server identifiers
\nGet client by server identifier or index.
\n\nParameters
\n\n\n
\n\n- server_identifier: Server URL (sanitized) or 0-based index
\nReturns
\n\n\n\n\nClient instance
\nRaises
\n\n\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": "- KeyError: If server not found
\n- IndexError: If index out of range
\nGet all active clients.
\n\nReturns
\n\n\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": "List of client instances
\nPerform health check on all servers concurrently.
\n\nParameters
\n\n\n
\n\n- timeout: Timeout for each health check
\nReturns
\n\n\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": "Dictionary mapping server IDs to health check results
\nGet list of healthy servers after health check.
\n\nParameters
\n\n\n
\n\n- timeout: Timeout for health checks
\nReturns
\n\n\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": "List of healthy clients
\nGet aggregated status summary for all servers.
\n\nSynchronous operation - no API calls made.
\n\nReturns
\n\n\n\n", "signature": "(self) -> dict[str, typing.Any]:", "funcdef": "def"}, "pyoutlineapi.NoOpAuditLogger": {"fullname": "pyoutlineapi.NoOpAuditLogger", "modulename": "pyoutlineapi", "qualname": "NoOpAuditLogger", "kind": "class", "doc": "Status summary dictionary
\nZero-overhead no-op audit logger.
\n\nImplements 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\nUses __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\nParameters
\n\n\n
\n\n- v: URL to validate
\nReturns
\n\n\n\n\nValidated URL
\nRaises
\n\n\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": "- ValueError: If URL is invalid
\nValidate certificate fingerprint with constant-time comparison.
\n\nParameters
\n\n\n
\n\n- v: Certificate fingerprint
\nReturns
\n\n\n\n\nValidated fingerprint
\nRaises
\n\n\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": "- ValueError: If fingerprint is invalid
\nValidate user agent string with efficient control char check.
\n\nParameters
\n\n\n
\n\n- v: User agent to validate
\nReturns
\n\n\n\n\nValidated user agent
\nRaises
\n\n\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": "- ValueError: If user agent is invalid
\nAdditional validation after model creation with pattern matching.
\n\nReturns
\n\n\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": "Validated configuration instance
\nGet configuration with sensitive data masked (cached).
\n\nSafe for logging, debugging, and display.
\n\nPerformance: ~20x speedup with caching for repeated calls\nMemory: Single cached result per instance
\n\nReturns
\n\n\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": "Sanitized configuration dictionary
\nCreate immutable copy with overrides (optimized validation).
\n\nParameters
\n\n\n
\n\n- overrides: Configuration parameters to override
\nReturns
\n\n\n\n\nDeep copy of configuration with applied updates
\nRaises
\n\n\n
\n\n- ValueError: If invalid override keys provided
\nExample:
\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": "\n\n\n>>> new_config = config.model_copy_immutable(timeout=20)\nGet circuit breaker configuration if enabled.
\n\nReturns None if circuit breaker is disabled, otherwise CircuitConfig instance.\nCached as property for performance.
\n\nReturns
\n\n\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": "Circuit config or None if disabled
\nLoad configuration from environment with overrides.
\n\nParameters
\n\n\n
\n\n- env_file: Path to .env file
\n- overrides: Configuration parameters to override
\nReturns
\n\n\n\n\nConfiguration instance
\nRaises
\n\n\n
\n\n- ConfigurationError: If environment configuration is invalid
\nExample:
\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": "\n\n\n>>> config = OutlineClientConfig.from_env(\n... env_file=".env.prod",\n... timeout=20,\n... enable_logging=True\n... )\nCreate minimal configuration (optimized validation).
\n\nParameters
\n\n\n
\n\n- api_url: API URL
\n- cert_sha256: Certificate fingerprint
\n- overrides: Optional configuration parameters
\nReturns
\n\n\n\n\nConfiguration instance
\nRaises
\n\n\n
\n\n- TypeError: If cert_sha256 is not str or SecretStr
\nExample:
\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": "\n\n\n>>> config = OutlineClientConfig.create_minimal(\n... api_url="https://server.com/path",\n... cert_sha256="a" * 64,\n... timeout=20\n... )\nNetwork connection failure.
\n\nAttributes:
\n\n\n
\n\n- host: Host that failed
\n- port: Port that failed
\nExample:
\n\n\n\n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.OutlineConnectionError.__init__": {"fullname": "pyoutlineapi.OutlineConnectionError.__init__", "modulename": "pyoutlineapi", "qualname": "OutlineConnectionError.__init__", "kind": "function", "doc": "\n\n\n>>> error = OutlineConnectionError(\n... "Connection refused", host="server.com", port=443\n... )\n>>> error.is_retryable # True\nInitialize connection error.
\n\nArguments:
\n\n\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": "- message: Error message
\n- host: Host that failed
\n- port: Port that failed
\nBase exception for all PyOutlineAPI errors.
\n\nProvides rich error context, retry guidance, and safe serialization\nwith automatic credential sanitization.
\n\nAttributes:
\n\n\n
\n\n- is_retryable: Whether this error type should be retried
\n- default_retry_delay: Suggested delay before retry in seconds
\nExample:
\n\n\n\n", "bases": "builtins.Exception"}, "pyoutlineapi.OutlineError.__init__": {"fullname": "pyoutlineapi.OutlineError.__init__", "modulename": "pyoutlineapi", "qualname": "OutlineError.__init__", "kind": "function", "doc": "\n\n\n>>> try:\n... raise OutlineError("Connection failed", details={"host": "server"})\n... except OutlineError as e:\n... print(e.safe_details) # {'host': 'server'}\nInitialize exception with automatic credential sanitization.
\n\nArguments:
\n\n\n
\n\n- message: Error message (automatically sanitized)
\n- details: Internal details (may contain sensitive data)
\n- safe_details: Safe details for logging/display
\nRaises:
\n\n\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": "- ValueError: If message exceeds maximum length after sanitization
\nGet internal error details (may contain sensitive data).
\n\nWarning:
\n\n\n\n\nUse with caution - may contain credentials or sensitive information.\n For logging, use
\nsafe_detailsinstead.Returns:
\n\n\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": "Copy of internal details dictionary
\nGet sanitized error details safe for logging.
\n\nReturns:
\n\n\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": "Copy of safe details dictionary
\nReturn 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\nAttributes:
\n\n\n
\n\n- timeout: Timeout value in seconds
\n- operation: Operation that timed out
\nExample:
\n\n\n\n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.OutlineTimeoutError.__init__": {"fullname": "pyoutlineapi.OutlineTimeoutError.__init__", "modulename": "pyoutlineapi", "qualname": "OutlineTimeoutError.__init__", "kind": "function", "doc": "\n\n\n>>> error = OutlineTimeoutError(\n... "Request timeout", timeout=30.0, operation="get_server_info"\n... )\n>>> error.is_retryable # True\nInitialize timeout error.
\n\nArguments:
\n\n\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": "- message: Error message
\n- timeout: Timeout value in seconds
\n- operation: Operation that timed out
\nPeak device count with timestamp.
\n\nSCHEMA: 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\nSCHEMA: 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\nEnforces HTTPS and enables all safety features:
\n\n\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": "- Circuit breaker enabled
\n- Logging disabled (performance)
\n- HTTPS enforcement
\n- Strict validation
\nEnforce production security with optimized checks.
\n\nReturns
\n\n\n\n\nValidated configuration
\nRaises
\n\n\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": "- ConfigurationError: If HTTP is used in production
\nHigh-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\nType-safe overloads ensure correct return type based on as_json parameter.
\n\nParameters
\n\n\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
\nReturns
\n\n\n\n\nValidated model instance or JSON dict
\nRaises
\n\n\n
\n\n- ValidationError: If validation fails with detailed error info
\nExample:
\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": "\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)\nParse simple success/error responses efficiently.
\n\nHandles various response formats with minimal overhead:
\n\n\n
\n\n- {\"success\": true/false}
\n- {\"error\": \"...\"} \u2192 False
\n- {\"message\": \"...\"} \u2192 False
\n- Empty dict \u2192 True (assumed success)
\nParameters
\n\n\n
\n\n- data: Response data
\nReturns
\n\n\n\n\nTrue if successful, False otherwise
\nExample:
\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": "\n\n\n>>> ResponseParser.parse_simple({"success": True})\nTrue\n>>> ResponseParser.parse_simple({"error": "Something failed"})\nFalse\n>>> ResponseParser.parse_simple({})\nTrue\nValidate response structure without full parsing.
\n\nLightweight validation before expensive Pydantic validation.\nUseful for early rejection of malformed responses.
\n\nParameters
\n\n\n
\n\n- data: Response data to validate
\n- required_fields: Sequence of required field names
\nReturns
\n\n\n\n\nTrue if structure is valid
\nExample:
\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": "\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\nExtract error message from response data efficiently.
\n\nChecks common error field names in order of preference.\nUses pre-computed tuple for fast iteration.
\n\nParameters
\n\n\n
\n\n- data: Response data
\nReturns
\n\n\n\n\nError message or None if not found
\nExample:
\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": "\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\nCheck if response indicates an error efficiently.
\n\nFast boolean check for error indicators in response.
\n\nParameters
\n\n\n
\n\n- data: Response data
\nReturns
\n\n\n\n\nTrue if response indicates an error
\nExample:
\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": "\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\nCryptographically 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\nFormat: {timestamp_us}-{random_hex}
\n\nReturns
\n\n\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": "Correlation ID string
\nGenerate secure request ID.
\n\nAlias for correlation ID for API compatibility.
\n\nReturns
\n\n\n\n", "signature": "() -> str:", "funcdef": "def"}, "pyoutlineapi.Server": {"fullname": "pyoutlineapi.Server", "modulename": "pyoutlineapi", "qualname": "Server", "kind": "class", "doc": "Request ID string
\nServer information model with optimized properties.
\n\nSCHEMA: 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\nParameters
\n\n\n
\n\n- v: Server name
\nReturns
\n\n\n\n\nValidated name
\nRaises
\n\n\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": "- ValueError: If name is empty
\nCheck if server has global data limit (optimized).
\n\nReturns
\n\n\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": "True if global limit exists
\nGet creation timestamp in seconds (cached).
\n\nNOTE: Cached because timestamp is immutable
\n\nReturns
\n\n\n\n", "annotation": ": float"}, "pyoutlineapi.ServerExperimentalMetric": {"fullname": "pyoutlineapi.ServerExperimentalMetric", "modulename": "pyoutlineapi", "qualname": "ServerExperimentalMetric", "kind": "class", "doc": "Timestamp in seconds
\nServer-level experimental metrics.
\n\nSCHEMA: 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\nSCHEMA: 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\nReturns
\n\n\n\n", "annotation": ": int"}, "pyoutlineapi.ServerMetrics.total_gigabytes": {"fullname": "pyoutlineapi.ServerMetrics.total_gigabytes", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.total_gigabytes", "kind": "variable", "doc": "Total bytes transferred
\nGet total in gigabytes (uses cached total_bytes).
\n\nReturns
\n\n\n\n", "annotation": ": float"}, "pyoutlineapi.ServerMetrics.user_count": {"fullname": "pyoutlineapi.ServerMetrics.user_count", "modulename": "pyoutlineapi", "qualname": "ServerMetrics.user_count", "kind": "variable", "doc": "Total GB transferred
\nGet number of users (cached).
\n\nReturns
\n\n\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": "Number of users
\nGet bytes for specific user (O(1) dict lookup).
\n\nParameters
\n\n\n
\n\n- user_id: User/key ID
\nReturns
\n\n\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": "Bytes transferred or 0 if not found
\nGet top users by bytes transferred (optimized sorting).
\n\nParameters
\n\n\n
\n\n- limit: Number of top users to return
\nReturns
\n\n\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": "List of (user_id, bytes) tuples
\nRequest model for renaming server.
\n\nSCHEMA: 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\nReturns
\n\n\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": "Total bytes or 0 if no metrics
\nGet total GB (uses total_bytes_transferred).
\n\nReturns
\n\n\n\n", "annotation": ": float"}, "pyoutlineapi.ServerSummary.has_errors": {"fullname": "pyoutlineapi.ServerSummary.has_errors", "modulename": "pyoutlineapi", "qualname": "ServerSummary.has_errors", "kind": "variable", "doc": "Total GB or 0.0 if no metrics
\nCheck if summary has errors (optimized None check).
\n\nReturns
\n\n\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": "True if errors present
\nTunnel time metric with time conversions.
\n\nSCHEMA: 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\nRaised when data fails validation against expected schema.
\n\nAttributes:
\n\n\n
\n\n- field: Field name that failed validation
\n- model: Model name
\nExample:
\n\n\n\n", "bases": "pyoutlineapi.exceptions.OutlineError"}, "pyoutlineapi.ValidationError.__init__": {"fullname": "pyoutlineapi.ValidationError.__init__", "modulename": "pyoutlineapi", "qualname": "ValidationError.__init__", "kind": "function", "doc": "\n\n\n>>> error = ValidationError(\n... "Invalid port number", field="port", model="ServerConfig"\n... )\nInitialize validation error.
\n\nArguments:
\n\n\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": "- message: Error message
\n- field: Field name that failed validation
\n- model: Model name
\nInput 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\nParameters
\n\n\n
\n\n- fingerprint: SHA-256 fingerprint
\nReturns
\n\n\n\n\nNormalized fingerprint (lowercase, no separators)
\nRaises
\n\n\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": "- ValueError: If format is invalid
\nValidate port number.
\n\nParameters
\n\n\n
\n\n- port: Port number
\nReturns
\n\n\n\n\nValidated port
\nRaises
\n\n\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": "- ValueError: If port is out of range
\nValidate name field.
\n\nParameters
\n\n\n
\n\n- name: Name to validate
\nReturns
\n\n\n\n\nValidated name
\nRaises
\n\n\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": "- ValueError: If name is invalid
\nValidate and sanitize URL.
\n\nParameters
\n\n\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
\nReturns
\n\n\n\n\nValidated URL
\nRaises
\n\n\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": "- ValueError: If URL is invalid
\nValidate string is not empty.
\n\nParameters
\n\n\n
\n\n- value: String value
\n- field_name: Field name for error messages
\nReturns
\n\n\n\n\nStripped string
\nRaises
\n\n\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": "- ValueError: If string is empty
\nValidate integer is non-negative.
\n\nParameters
\n\n\n
\n\n- value: Integer value
\n- name: Field name for error messages
\nReturns
\n\n\n\n\nValidated value
\nRaises
\n\n\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": "- ValueError: If value is negative
\nValidate experimental metrics 'since' parameter.
\n\nAccepts:
\n\n\n
\n\n- Relative durations: 24h, 7d, 30m, 15s
\n- ISO-8601 timestamps (e.g., 2024-01-01T00:00:00Z)
\nParameters
\n\n\n
\n\n- value: Since parameter
\nReturns
\n\n\n\n\nSanitized since value
\nRaises
\n\n\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": "- ValueError: If value is invalid
\nEnhanced key_id validation.
\n\nParameters
\n\n\n
\n\n- key_id: Key ID to validate
\nReturns
\n\n\n\n\nValidated key ID
\nRaises
\n\n\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": "- ValueError: If key ID is invalid
\nRemove secret path from URL for safe logging.
\n\nParameters
\n\n\n
\n\n- url: URL to sanitize
\nReturns
\n\n\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": "Sanitized URL
\nSanitize endpoint for safe logging.
\n\nParameters
\n\n\n
\n\n- endpoint: Endpoint to sanitize
\nReturns
\n\n\n\n", "signature": "(endpoint: str) -> str:", "funcdef": "def"}, "pyoutlineapi.audited": {"fullname": "pyoutlineapi.audited", "modulename": "pyoutlineapi", "qualname": "audited", "kind": "function", "doc": "Sanitized endpoint
\nAudit logging decorator with zero-config smart extraction.
\n\nAutomatically extracts ALL information from function signature and execution:
\n\n\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
\nUsage:
\n\n\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 ...
\nParameters
\n\n\n
\n\n- log_success: Log successful operations (default: True)
\n- log_failure: Log failed operations (default: True)
\nReturns
\n\n\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": "Decorated function with automatic audit logging
\nBuild configuration overrides dictionary from kwargs.
\n\nDRY implementation - single source of truth for config building.
\n\nParameters
\n\n\n
\n\n- kwargs: Configuration parameters
\nReturns
\n\n\n\n\nDictionary containing only non-None values
\nExample:
\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": "\n\n\n>>> overrides = build_config_overrides(timeout=20, enable_logging=True)\n>>> # Returns: {'timeout': 20, 'enable_logging': True}\nCreate client with minimal parameters.
\n\nConvenience function for quick client creation without\nexplicit configuration object. Uses modern **overrides approach.
\n\nParameters
\n\n\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.)
\nReturns
\n\n\n\n\nConfigured client instance (use with async context manager)
\nRaises
\n\n\n
\n\n- ConfigurationError: If parameters are invalid
\nExample (advanced, prefer from_env for production):
\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": "\n\n\n\nasync with AsyncOutlineClient.from_env() as client:\n ... info = await client.get_server_info()
\nCreate .env template file (optimized I/O).
\n\nPerformance: Uses cached template and efficient Path operations
\n\nParameters
\n\n\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": "- path: Path to template file
\nCreate multiserver manager with configurations.
\n\nConvenience function for creating a manager for multiple servers.
\n\nParameters
\n\n\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
\nReturns
\n\n\n\n\nMultiServerManager instance (use with async context manager)
\nRaises
\n\n\n
\n\n- ConfigurationError: If configurations are invalid
\nExample:
\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": "\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()\nFormat exception chain for structured logging.
\n\nArguments:
\n\n\n
\n\n- error: Exception to format
\nReturns:
\n\n\n\n\nList of error dictionaries ordered from root to leaf
\nExample:
\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": "\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\nGet audit logger from current context.
\n\nReturns
\n\n\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": "Audit logger instance or None
\nGet or create audit logger with weak reference caching.
\n\nParameters
\n\n\n
\n\n- instance_id: Instance ID for caching (optional)
\nReturns
\n\n\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": "Audit logger instance
\nGet suggested retry delay for an error.
\n\nArguments:
\n\n\n
\n\n- error: Exception to check
\nReturns:
\n\n\n\n\nRetry delay in seconds, or None if not retryable
\nExample:
\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": "\n\n\n>>> error = OutlineTimeoutError("Timeout")\n>>> get_retry_delay(error) # 2.0\nExtract safe error information for logging.
\n\nReturns only safe information without sensitive data.
\n\nArguments:
\n\n\n
\n\n- error: Exception to convert
\nReturns:
\n\n\n\n\nSafe error dictionary suitable for logging
\nExample:
\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": "\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, ...}\nGet package version string.
\n\nReturns
\n\n\n\n", "signature": "() -> str:", "funcdef": "def"}, "pyoutlineapi.is_json_serializable": {"fullname": "pyoutlineapi.is_json_serializable", "modulename": "pyoutlineapi", "qualname": "is_json_serializable", "kind": "function", "doc": "Package version
\nType guard for JSON-serializable values.
\n\nParameters
\n\n\n
\n\n- value: Value to check
\nReturns
\n\n\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": "True if value is JSON-serializable
\nCheck if error should be retried.
\n\nArguments:
\n\n\n
\n\n- error: Exception to check
\nReturns:
\n\n\n\n\nTrue if error is retryable
\nExample:
\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": "\n\n\n>>> error = APIError("Server error", status_code=503)\n>>> is_retryable(error) # True\nType guard for valid byte counts.
\n\nParameters
\n\n\n
\n\n- value: Value to check
\nReturns
\n\n\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": "True if value is valid bytes
\nType guard for valid port numbers.
\n\nParameters
\n\n\n
\n\n- value: Value to check
\nReturns
\n\n\n\n", "signature": "(value: object) -> TypeGuard[int]:", "funcdef": "def"}, "pyoutlineapi.load_config": {"fullname": "pyoutlineapi.load_config", "modulename": "pyoutlineapi", "qualname": "load_config", "kind": "function", "doc": "True if value is valid port
\nLoad configuration for environment (optimized lookup).
\n\nParameters
\n\n\n
\n\n- environment: Environment name (development, production, custom)
\n- overrides: Configuration parameters to override
\nReturns
\n\n\n\n\nConfiguration instance
\nRaises
\n\n\n
\n\n- ValueError: If environment name is invalid
\nExample:
\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": "\n\n\n>>> config = load_config("production", timeout=20)\nSensitive data masking with lazy copying and optimized recursion.
\n\nUses lazy copying - only creates new dict when needed.\nIncludes recursion depth protection.
\n\nParameters
\n\n\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)
\nReturns
\n\n\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": "Masked data dictionary (may be same object if no sensitive data found)
\nPrint 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\nCreates
\n", "signature": "() -> None:", "funcdef": "def"}, "pyoutlineapi.set_audit_logger": {"fullname": "pyoutlineapi.set_audit_logger", "modulename": "pyoutlineapi", "qualname": "set_audit_logger", "kind": "function", "doc": ".env.examplefile with all available configuration options.Set audit logger for current async context.
\n\nThread-safe and async-safe using contextvars.\nPreferred over global state for high-load applications.
\n\nParameters
\n\n\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- logger_instance: Audit logger instance
\nAll 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" }, +]