diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 0fb0fff0..ad301f62 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -111,8 +111,10 @@ class ProviderAdapter(BaseAdapter): ## 8. Asynchronous Programming * **`async`/`await`:** Use consistently for all I/O operations -* **Libraries:** Prefer `httpx` for HTTP, `asyncio` for concurrency -* **No Blocking Code:** Never use blocking I/O in async functions +* **Runtime Facade:** Import concurrency primitives from `ccproxy.core.async_runtime` (e.g., `create_task`, `sleep`, `create_lock`) instead of touching `asyncio` or `anyio` directly. This keeps the codebase portable as we evolve the underlying async backend. +* **Direct `asyncio` Usage:** Only interact with `asyncio` for functionality not yet exposed via the runtime (e.g., low-level loop or subprocess APIs). When that happens, contain it behind small helpers and add a TODO to fold it into the facade. +* **HTTP Clients:** Continue using `httpx` for network I/O. +* **No Blocking Code:** Never use blocking I/O in async functions; push work into `runtime.to_thread` when necessary. ## 9. Testing diff --git a/README.md b/README.md index 2c9ebb21..8f00f356 100644 --- a/README.md +++ b/README.md @@ -152,20 +152,18 @@ export PLUGINS__METRICS__ENABLED=true ## Running -To try the latest development branch without cloning the repo, use `uvx` to -install directly from GitHub: +To install the latest stable release without cloning the repository, use `uvx` +to grab the published wheel and launch the CLI: ```bash -uvx --with 'ccproxy-api[all] @ git+https://github.com/caddyglow/ccproxy-api@dev/v0.2' ccproxy +uvx --with "ccproxy-api[all]==0.2.0" ccproxy serve --port 8000 ``` -Or, if you prefer `pipx`, you can run the same branch directly: - -```bash -pipx run --spec 'ccproxy-api[all] @ git+https://github.com/caddyglow/ccproxy-api@dev/v0.2' ccproxy -``` +If you prefer `pipx`, install the package (optionally with extras) and use the +local shim: ```bash +pipx install "ccproxy-api[all]==0.2.0" ccproxy serve # default on localhost:8000 ``` diff --git a/TESTING.md b/TESTING.md index b40e8f92..2ed5949e 100644 --- a/TESTING.md +++ b/TESTING.md @@ -105,7 +105,7 @@ tests/ - Mock at **service boundaries only** - never mock internal components - Test **pure functions and single components** in isolation - **No HTTP layer testing** - use service layer mocks instead -- **No timing dependencies** - all asyncio.sleep() removed +- **No timing dependencies** - all direct `asyncio.sleep()` calls should instead patch `ccproxy.core.async_runtime.sleep` - **No database operations** - moved to integration tests **Integration Tests** (tests/integration/): diff --git a/ccproxy/api/app.py b/ccproxy/api/app.py index 15528b12..ba5f8d6f 100644 --- a/ccproxy/api/app.py +++ b/ccproxy/api/app.py @@ -175,7 +175,7 @@ async def shutdown_hook_system(app: FastAPI) -> None: # Get hook manager from app state - it will shutdown its own background manager hook_manager = getattr(app.state, "hook_manager", None) if hook_manager: - hook_manager.shutdown() + await hook_manager.shutdown() logger.debug("hook_system_shutdown_completed", category="lifecycle") except Exception as e: diff --git a/ccproxy/auth/oauth/base.py b/ccproxy/auth/oauth/base.py index 1ac02c6d..90121fa1 100644 --- a/ccproxy/auth/oauth/base.py +++ b/ccproxy/auth/oauth/base.py @@ -1,11 +1,11 @@ """Base OAuth client with common PKCE flow implementation.""" -import asyncio import base64 import hashlib import secrets import urllib.parse from abc import ABC, abstractmethod +from contextlib import suppress from datetime import UTC, datetime, timedelta from typing import Any, Generic, TypeVar @@ -18,6 +18,15 @@ from ccproxy.auth.models.credentials import BaseCredentials from ccproxy.auth.storage.base import TokenStorage from ccproxy.config.settings import Settings +from ccproxy.core.async_runtime import ( + Task, +) +from ccproxy.core.async_runtime import ( + create_event as runtime_create_event, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) from ccproxy.core.logging import get_logger from ccproxy.http.client import HTTPClientFactory @@ -89,8 +98,8 @@ def __init__( hook_manager_id=id(hook_manager) if hook_manager else None, ) - self._callback_server: asyncio.Task[None] | None = None - self._auth_complete = asyncio.Event() + self._callback_server: Task[None] | None = None + self._auth_complete = runtime_create_event() self._auth_result: Any | None = None self._auth_error: str | None = None @@ -106,13 +115,8 @@ def __del__(self) -> None: and self.http_client and not self.http_client.is_closed ): - try: - # Try to get the current event loop - loop = asyncio.get_running_loop() - loop.create_task(self.http_client.aclose()) - except RuntimeError: - # No running event loop, can't clean up async resources - pass + with suppress(RuntimeError): + runtime_create_task(self.http_client.aclose()) def _generate_pkce_pair(self) -> tuple[str, str]: """Generate PKCE code verifier and challenge. diff --git a/ccproxy/auth/oauth/flows.py b/ccproxy/auth/oauth/flows.py index 8d20f075..ac1070a7 100644 --- a/ccproxy/auth/oauth/flows.py +++ b/ccproxy/auth/oauth/flows.py @@ -1,6 +1,5 @@ """OAuth flow engines for CLI authentication.""" -import asyncio import base64 import secrets import sys @@ -13,6 +12,25 @@ from ccproxy.auth.oauth.cli_errors import AuthProviderError, PortBindError from ccproxy.auth.oauth.registry import OAuthProviderProtocol +from ccproxy.core.async_runtime import ( + Future, + Task, +) +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + create_future as runtime_create_future, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.logging import get_logger @@ -33,10 +51,10 @@ def __init__(self, port: int, callback_path: str = "/callback") -> None: self.port = port self.callback_path = callback_path self.server: Any = None - self._server_task: asyncio.Task[Any] | None = None + self._server_task: Task[Any] | None = None self.callback_received = False self.callback_data: dict[str, Any] = {} - self.callback_future: asyncio.Future[dict[str, Any]] | None = None + self.callback_future: Future[dict[str, Any]] | None = None async def start(self) -> None: """Start the callback server.""" @@ -92,10 +110,10 @@ async def _serve_with_error_handling() -> None: f"Failed to start callback server on port {self.port}: {e}" ) from e - self._server_task = asyncio.create_task(_serve_with_error_handling()) + self._server_task = runtime_create_task(_serve_with_error_handling()) # Wait briefly and check if server started successfully - await asyncio.sleep(0.1) + await runtime_sleep(0.1) if self._server_task.done(): # Server failed to start, re-raise the exception await self._server_task @@ -110,8 +128,8 @@ async def stop(self) -> None: self.server.should_exit = True if hasattr(self, "_server_task") and self._server_task is not None: try: - await asyncio.wait_for(self._server_task, timeout=2.0) - except TimeoutError: + await runtime_wait_for(self._server_task, timeout=2.0) + except RuntimeTimeoutError: self._server_task.cancel() self.server = None logger.debug("cli_callback_server_stopped", port=self.port) @@ -182,14 +200,14 @@ async def wait_for_callback( Callback data dictionary Raises: - asyncio.TimeoutError: If callback is not received within timeout + RuntimeTimeoutError: If callback is not received within timeout ValueError: If state validation fails """ - self.callback_future = asyncio.Future() + self.callback_future = runtime_create_future() try: # Wait for callback with timeout - callback_data = await asyncio.wait_for( + callback_data = await runtime_wait_for( self.callback_future, timeout=timeout ) @@ -215,9 +233,11 @@ async def wait_for_callback( return callback_data - except TimeoutError: + except RuntimeTimeoutError: logger.error("cli_callback_timeout", timeout=timeout, port=self.port) - raise TimeoutError(f"No OAuth callback received within {timeout} seconds") + raise RuntimeTimeoutError( + f"No OAuth callback received within {timeout} seconds" + ) def render_qr_code(url: str) -> None: @@ -311,7 +331,7 @@ async def run( callback_data["code"], state, code_verifier, redirect_uri ) return await provider.save_credentials(credentials, save_path) - except TimeoutError: + except RuntimeTimeoutError: # Fallback to manual code entry if callback times out console.print( "[yellow]Callback timed out. You can enter the code manually.[/yellow]" diff --git a/ccproxy/auth/storage/base.py b/ccproxy/auth/storage/base.py index fd4fd130..dd533039 100644 --- a/ccproxy/auth/storage/base.py +++ b/ccproxy/auth/storage/base.py @@ -1,6 +1,5 @@ """Abstract base class for token storage.""" -import asyncio import contextlib import json import shutil @@ -11,6 +10,7 @@ from ccproxy.auth.exceptions import CredentialsInvalidError, CredentialsStorageError from ccproxy.auth.models.credentials import BaseCredentials +from ccproxy.core.async_runtime import to_thread as runtime_to_thread from ccproxy.core.logging import get_logger @@ -113,7 +113,7 @@ def read_file() -> dict[str, Any]: with self.file_path.open("r") as f: return json.load(f) # type: ignore[no-any-return] - data = await asyncio.to_thread(read_file) + data = await runtime_to_thread(read_file) return data except json.JSONDecodeError as e: @@ -166,7 +166,7 @@ async def _create_backup(self) -> bool: backup_path = self.file_path.parent / backup_name # Copy file to backup location - await asyncio.to_thread(shutil.copy2, self.file_path, backup_path) + await runtime_to_thread(shutil.copy2, self.file_path, backup_path) logger.info( "backup_created", @@ -207,7 +207,7 @@ async def _write_json(self, data: dict[str, Any]) -> None: try: # Ensure parent directory exists - await asyncio.to_thread( + await runtime_to_thread( self.file_path.parent.mkdir, parents=True, exist_ok=True, @@ -225,7 +225,7 @@ def write_file() -> None: # Atomic rename temp_path.replace(self.file_path) - await asyncio.to_thread(write_file) + await runtime_to_thread(write_file) logger.debug( "json_write_success", @@ -273,7 +273,7 @@ async def exists(self) -> bool: True if file exists, False otherwise """ # Run file system check in thread pool for consistency - file_exists = await asyncio.to_thread( + file_exists = await runtime_to_thread( lambda: self.file_path.exists() and self.file_path.is_file() ) @@ -297,7 +297,7 @@ async def delete(self) -> bool: """ try: if await self.exists(): - await asyncio.to_thread(self.file_path.unlink) + await runtime_to_thread(self.file_path.unlink) logger.debug("file_deleted", path=str(self.file_path)) return True return False diff --git a/ccproxy/cli/__init__.py b/ccproxy/cli/__init__.py index 8f33f61f..325cbe72 100644 --- a/ccproxy/cli/__init__.py +++ b/ccproxy/cli/__init__.py @@ -1,4 +1,4 @@ -from .commands.serve import api, claude +from .commands.serve import api from .helpers import get_rich_toolkit from .main import app, app_main, main, version_callback @@ -8,7 +8,6 @@ "main", "version_callback", "api", - "claude", "app_main", "get_rich_toolkit", ] diff --git a/ccproxy/cli/commands/auth.py b/ccproxy/cli/commands/auth.py index bdbb1c4d..5e2139f5 100644 --- a/ccproxy/cli/commands/auth.py +++ b/ccproxy/cli/commands/auth.py @@ -1,6 +1,5 @@ """Authentication and credential management commands.""" -import asyncio import contextlib import inspect import logging @@ -28,6 +27,7 @@ from ccproxy.auth.oauth.registry import FlowType, OAuthRegistry from ccproxy.cli.helpers import get_rich_toolkit from ccproxy.config.settings import Settings +from ccproxy.core.async_runtime import run as runtime_run from ccproxy.core.logging import bootstrap_cli_logging, get_logger, setup_logging from ccproxy.core.plugins import load_cli_plugins from ccproxy.core.plugins.hooks.manager import HookManager @@ -482,7 +482,7 @@ def _provider_plugin_name(provider: str) -> str | None: def _await_if_needed(value: Any) -> Any: """Await coroutine values in synchronous CLI context.""" if inspect.isawaitable(value): - return asyncio.run(cast(Coroutine[Any, Any, Any], value)) + return runtime_run(cast(Coroutine[Any, Any, Any], value)) return value @@ -523,7 +523,7 @@ def _push(name: str | None) -> None: for candidate in candidates: try: - manager = asyncio.run(registry.get(candidate)) + manager = runtime_run(registry.get(candidate)) except Exception as exc: # pragma: no cover - defensive logger.debug( "auth_manager_registry_get_failed", name=candidate, error=str(exc) @@ -617,13 +617,8 @@ async def _lazy_register_oauth_provider( try: # Initialize all plugins; auth providers will register to oauth_registry - import asyncio as _asyncio - - if _asyncio.get_event_loop().is_running(): - # In practice, we're already in async context; just await directly - await plugin_registry.initialize_all(container) - else: # pragma: no cover - defensive path - _asyncio.run(plugin_registry.initialize_all(container)) + # We're already in an async context, so initialize directly. + await plugin_registry.initialize_all(container) except Exception as e: logger.debug( "plugin_initialization_failed_cli", @@ -676,7 +671,7 @@ async def discover_oauth_providers( def get_oauth_provider_choices() -> list[str]: """Get list of available OAuth provider names for CLI choices.""" container = _get_service_container() - providers = asyncio.run(discover_oauth_providers(container)) + providers = runtime_run(discover_oauth_providers(container)) return list(providers.keys()) @@ -768,7 +763,7 @@ def list_providers() -> None: try: container = _get_service_container() - providers = asyncio.run(discover_oauth_providers(container)) + providers = runtime_run(discover_oauth_providers(container)) if not providers: toolkit.print("No OAuth providers found", tag="warning") @@ -896,12 +891,12 @@ def login_command( try: container = _get_service_container() registry = container.get_oauth_registry() - oauth_provider = asyncio.run( + oauth_provider = runtime_run( get_oauth_provider_for_name(provider, registry, container) ) if not oauth_provider: - providers = asyncio.run(discover_oauth_providers(container)) + providers = runtime_run(discover_oauth_providers(container)) available = ", ".join(providers.keys()) if providers else "none" toolkit.print( f"Provider '{provider}' not found. Available: {available}", @@ -925,7 +920,7 @@ def login_command( f"Provider '{provider}' doesn't support manual code entry" ) flow_engine = ManualCodeFlow() - success = asyncio.run( + success = runtime_run( flow_engine.run(oauth_provider, save_path=custom_path_str) ) @@ -935,14 +930,14 @@ def login_command( ): # Device flow preferred and supported flow_engine = DeviceCodeFlow() - success = asyncio.run( + success = runtime_run( flow_engine.run(oauth_provider, save_path=custom_path_str) ) else: # Browser flow (default) flow_engine = BrowserFlow() - success = asyncio.run( + success = runtime_run( flow_engine.run( oauth_provider, no_browser=no_browser, @@ -960,7 +955,7 @@ def login_command( oauth_provider, disable=custom_path is not None ): flow_engine = ManualCodeFlow() - success = asyncio.run( + success = runtime_run( flow_engine.run(oauth_provider, save_path=custom_path_str) ) else: @@ -1034,12 +1029,12 @@ def _refresh_provider_tokens(provider: str, custom_path: Path | None = None) -> try: container = _get_service_container() registry = container.get_oauth_registry() - oauth_provider = asyncio.run( + oauth_provider = runtime_run( get_oauth_provider_for_name(provider_key, registry, container) ) if not oauth_provider: - providers = asyncio.run(discover_oauth_providers(container)) + providers = runtime_run(discover_oauth_providers(container)) available = ", ".join(providers.keys()) if providers else "none" toolkit.print( f"Provider '{provider_key}' not found. Available: {available}", @@ -1054,7 +1049,7 @@ def _refresh_provider_tokens(provider: str, custom_path: Path | None = None) -> ) raise typer.Exit(1) - credentials = asyncio.run(oauth_provider.load_credentials(**load_kwargs)) + credentials = runtime_run(oauth_provider.load_credentials(**load_kwargs)) if not credentials: toolkit.print( ( @@ -1079,7 +1074,7 @@ def _refresh_provider_tokens(provider: str, custom_path: Path | None = None) -> and manager and hasattr(manager, "refresh_token") ): - refreshed_credentials = asyncio.run(manager.refresh_token()) + refreshed_credentials = runtime_run(manager.refresh_token()) else: refresh_token = snapshot.refresh_token if snapshot else None if not refresh_token: @@ -1093,11 +1088,11 @@ def _refresh_provider_tokens(provider: str, custom_path: Path | None = None) -> with _temporary_disable_provider_storage( oauth_provider, disable=credential_path is not None ): - refreshed_credentials = asyncio.run( + refreshed_credentials = runtime_run( oauth_provider.refresh_access_token(refresh_token) ) if credential_path and refreshed_credentials: - saved = asyncio.run( + saved = runtime_run( oauth_provider.save_credentials( refreshed_credentials, **save_kwargs ) @@ -1119,7 +1114,7 @@ def _refresh_provider_tokens(provider: str, custom_path: Path | None = None) -> if refreshed_credentials is None: with contextlib.suppress(Exception): - refreshed_credentials = asyncio.run( + refreshed_credentials = runtime_run( oauth_provider.load_credentials(**load_kwargs) ) if ( @@ -1128,7 +1123,7 @@ def _refresh_provider_tokens(provider: str, custom_path: Path | None = None) -> and hasattr(manager, "load_credentials") ): with contextlib.suppress(Exception): - refreshed_credentials = asyncio.run(manager.load_credentials()) + refreshed_credentials = runtime_run(manager.load_credentials()) refreshed_snapshot = None if refreshed_credentials: @@ -1271,11 +1266,11 @@ def status_command( try: container = _get_service_container() registry = container.get_oauth_registry() - oauth_provider = asyncio.run( + oauth_provider = runtime_run( get_oauth_provider_for_name(provider, registry, container) ) if not oauth_provider: - providers = asyncio.run(discover_oauth_providers(container)) + providers = runtime_run(discover_oauth_providers(container)) available = ", ".join(providers.keys()) if providers else "none" expected = _expected_plugin_class_name(provider) toolkit.print( @@ -1291,7 +1286,7 @@ def status_command( if oauth_provider: try: # Delegate to provider; providers may internally use their managers - credentials = asyncio.run( + credentials = runtime_run( oauth_provider.load_credentials(**load_kwargs) ) @@ -1306,12 +1301,12 @@ def status_command( if credential_path is None: try: if hasattr(oauth_provider, "create_token_manager"): - manager = asyncio.run(oauth_provider.create_token_manager()) + manager = runtime_run(oauth_provider.create_token_manager()) elif hasattr(oauth_provider, "get_token_manager"): mgr = oauth_provider.get_token_manager() # may be sync # If coroutine, run it; else use directly if hasattr(mgr, "__await__"): - manager = asyncio.run(mgr) + manager = runtime_run(mgr) else: manager = mgr except Exception as e: @@ -1320,8 +1315,8 @@ def status_command( if manager and hasattr(manager, "get_token_snapshot"): with contextlib.suppress(Exception): result = manager.get_token_snapshot() - if asyncio.iscoroutine(result): - snapshot = asyncio.run(result) + if inspect.isawaitable(result): + snapshot = runtime_run(result) else: snapshot = cast(TokenSnapshot | None, result) @@ -1333,7 +1328,7 @@ def status_command( standard_profile = None if hasattr(oauth_provider, "get_standard_profile"): with contextlib.suppress(Exception): - standard_profile = asyncio.run( + standard_profile = runtime_run( oauth_provider.get_standard_profile(credentials) ) if not standard_profile and hasattr( @@ -1365,7 +1360,7 @@ def status_command( oauth_provider, "get_unified_profile_quick" ): with contextlib.suppress(Exception): - quick = asyncio.run( + quick = runtime_run( oauth_provider.get_unified_profile_quick() ) if ( @@ -1374,7 +1369,7 @@ def status_command( and hasattr(oauth_provider, "get_unified_profile") ): with contextlib.suppress(Exception): - quick = asyncio.run( + quick = runtime_run( oauth_provider.get_unified_profile() ) if quick and isinstance(quick, dict) and quick != {}: @@ -1438,7 +1433,7 @@ def status_command( standard_profile = None if hasattr(oauth_provider, "get_standard_profile"): with contextlib.suppress(Exception): - standard_profile = asyncio.run( + standard_profile = runtime_run( oauth_provider.get_standard_profile(credentials) ) if standard_profile is not None: @@ -1605,12 +1600,12 @@ def logout_command( try: container = _get_service_container() registry = container.get_oauth_registry() - oauth_provider = asyncio.run( + oauth_provider = runtime_run( get_oauth_provider_for_name(provider, registry, container) ) if not oauth_provider: - providers = asyncio.run(discover_oauth_providers(container)) + providers = runtime_run(discover_oauth_providers(container)) available = ", ".join(providers.keys()) if providers else "none" expected = _expected_plugin_class_name(provider) toolkit.print( @@ -1621,7 +1616,7 @@ def logout_command( existing_creds = None with contextlib.suppress(Exception): - existing_creds = asyncio.run(oauth_provider.load_credentials()) + existing_creds = runtime_run(oauth_provider.load_credentials()) if not existing_creds: console.print("[yellow]No credentials found. Already logged out.[/yellow]") @@ -1638,11 +1633,11 @@ def logout_command( try: storage = oauth_provider.get_storage() if storage and hasattr(storage, "delete"): - success = asyncio.run(storage.delete()) + success = runtime_run(storage.delete()) elif storage and hasattr(storage, "clear"): - success = asyncio.run(storage.clear()) + success = runtime_run(storage.clear()) else: - success = asyncio.run(oauth_provider.save_credentials(None)) + success = runtime_run(oauth_provider.save_credentials(None)) except Exception as e: logger.debug("logout_error", error=str(e), exc_info=e) diff --git a/ccproxy/cli/commands/serve.py b/ccproxy/cli/commands/serve.py index ba4b3cd8..f5bfa48a 100644 --- a/ccproxy/cli/commands/serve.py +++ b/ccproxy/cli/commands/serve.py @@ -1,8 +1,5 @@ """Serve command for CCProxy API server - consolidates server-related commands.""" -import os -import shutil -import subprocess from pathlib import Path from typing import Annotated, Any @@ -14,9 +11,7 @@ from ccproxy.cli.helpers import get_rich_toolkit from ccproxy.config.settings import ConfigurationError, Settings -from ccproxy.core._version import __version__ from ccproxy.core.logging import get_logger, setup_logging -from ccproxy.utils.binary_resolver import BinaryResolver from ..options.security_options import validate_auth_token from ..options.server_options import ( @@ -42,8 +37,8 @@ def _show_api_usage_info(toolkit: Any, settings: Settings) -> None: toolkit.print_title("API Client Configuration", tag="config") - anthropic_base_url = f"http://{settings.server.host}:{settings.server.port}" - openai_base_url = f"http://{settings.server.host}:{settings.server.port}/openai" + anthropic_base_url = f"http://{settings.server.host}:{settings.server.port}/claude" + openai_base_url = f"http://{settings.server.host}:{settings.server.port}/codex" toolkit.print("Environment Variables for API Clients:", tag="info") toolkit.print_line() @@ -60,137 +55,12 @@ def _show_api_usage_info(toolkit: Any, settings: Settings) -> None: toolkit.print_line() -# def _run_docker_server( -# settings: Settings, -# docker_image: str | None = None, -# docker_env: list[str] | None = None, -# docker_volume: list[str] | None = None, -# docker_arg: list[str] | None = None, -# docker_home: str | None = None, -# docker_workspace: str | None = None, -# user_mapping_enabled: bool | None = None, -# user_uid: int | None = None, -# user_gid: int | None = None, -# ) -> None: -# """Run the server using Docker.""" -# toolkit = get_rich_toolkit() -# logger = get_logger(__name__) -# -# docker_env = docker_env or [] -# docker_volume = docker_volume or [] -# docker_arg = docker_arg or [] -# -# docker_env_dict = {} -# for env_var in docker_env: -# if "=" in env_var: -# key, value = env_var.split("=", 1) -# docker_env_dict[key] = value -# -# if settings.server.reload: -# docker_env_dict["RELOAD"] = "true" -# docker_env_dict["PORT"] = str(settings.server.port) -# docker_env_dict["HOST"] = "0.0.0.0" -# -# toolkit.print_line() -# -# toolkit.print_title("Docker Configuration Summary", tag="config") -# -# docker_config = get_docker_config_with_fallback(settings) -# home_dir = docker_home or docker_config.docker_home_directory -# workspace_dir = docker_workspace or docker_config.docker_workspace_directory -# -# toolkit.print("Volumes:", tag="config") -# if home_dir: -# toolkit.print(f" Home: {home_dir} → /data/home", tag="volume") -# if workspace_dir: -# toolkit.print(f" Workspace: {workspace_dir} → /data/workspace", tag="volume") -# if docker_volume: -# for vol in docker_volume: -# toolkit.print(f" Additional: {vol}", tag="volume") -# toolkit.print_line() -# -# toolkit.print("Environment Variables:", tag="config") -# key_env_vars = { -# "CLAUDE_HOME": "/data/home", -# "CLAUDE_WORKSPACE": "/data/workspace", -# "PORT": str(settings.server.port), -# "HOST": "0.0.0.0", -# } -# if settings.server.reload: -# key_env_vars["RELOAD"] = "true" -# -# for key, value in key_env_vars.items(): -# toolkit.print(f" {key}={value}", tag="env") -# -# for env_var in docker_env: -# toolkit.print(f" {env_var}", tag="env") -# -# if settings.logging.level == "DEBUG": -# toolkit.print_line() -# toolkit.print_title("Debug: All Environment Variables", tag="debug") -# all_env = {**docker_env_dict} -# for key, value in sorted(all_env.items()): -# toolkit.print(f" {key}={value}", tag="debug") -# -# toolkit.print_line() -# -# toolkit.print_line() -# -# if settings.security.auth_token: -# _show_api_usage_info(toolkit, settings) -# -# adapter = create_docker_adapter() -# image, volumes, environment, command, user_context, _ = ( -# adapter.build_docker_run_args( -# settings, -# command=["ccproxy", "serve"], -# docker_image=docker_image, -# docker_env=[f"{k}={v}" for k, v in docker_env_dict.items()], -# docker_volume=docker_volume, -# docker_arg=docker_arg, -# docker_home=docker_home, -# docker_workspace=docker_workspace, -# user_mapping_enabled=user_mapping_enabled, -# user_uid=user_uid, -# user_gid=user_gid, -# ) -# ) -# -# logger.info( -# "docker_server_config", -# configured_image=docker_config.docker_image, -# effective_image=image, -# ) -# -# ports = [f"{settings.server.port}:{settings.server.port}"] -# -# adapter = create_docker_adapter() -# adapter.exec_container_legacy( -# image=image, -# volumes=volumes, -# environment=environment, -# command=command, -# user_context=user_context, -# ports=ports, -# ) - - def _run_local_server(settings: Settings) -> None: """Run the server locally.""" # in_docker = is_running_in_docker() toolkit = get_rich_toolkit() logger = get_logger(__name__) - # if in_docker: - # toolkit.print_title( - # f"Starting CCProxy API server in {warning('docker')}", - # tag="docker", - # ) - # toolkit.print( - # f"uid={warning(str(os.getuid()))} gid={warning(str(os.getgid()))}" - # ) - # toolkit.print(f"HOME={os.environ['HOME']}") - if settings.security.auth_token: _show_api_usage_info(toolkit, settings) @@ -359,8 +229,6 @@ def api( ), ) - # Docker execution is now handled by the Docker plugin - # Always run local server - plugins handle their own execution modes _run_local_server(settings) except ConfigurationError as e: @@ -381,231 +249,3 @@ def api( toolkit = get_rich_toolkit() toolkit.print(f"Error starting server: {e}", tag="error") raise typer.Exit(1) from e - - -def claude( - args: Annotated[ - list[str] | None, - typer.Argument( - help="Arguments to pass to claude CLI (e.g. --version, doctor, config)", - ), - ] = None, - docker: Annotated[ - bool, - typer.Option( - "--docker", - "-d", - help="Run claude command from docker image instead of local CLI", - ), - ] = False, - docker_image: Annotated[ - str | None, - typer.Option( - "--docker-image", - help="Docker image to use (overrides configuration)", - rich_help_panel="Docker Settings", - ), - ] = None, - docker_env: Annotated[ - list[str] | None, - typer.Option( - "--docker-env", - "-e", - help="Environment variables to pass to Docker container", - rich_help_panel="Docker Settings", - ), - ] = None, - docker_volume: Annotated[ - list[str] | None, - typer.Option( - "--docker-volume", - "-v", - help="Volume mounts for Docker container", - rich_help_panel="Docker Settings", - ), - ] = None, - docker_arg: Annotated[ - list[str] | None, - typer.Option( - "--docker-arg", - help="Additional arguments to pass to docker run", - rich_help_panel="Docker Settings", - ), - ] = None, - docker_home: Annotated[ - str | None, - typer.Option( - "--docker-home", - help="Override the home directory for Docker", - rich_help_panel="Docker Settings", - ), - ] = None, - docker_workspace: Annotated[ - str | None, - typer.Option( - "--docker-workspace", - help="Override the workspace directory for Docker", - rich_help_panel="Docker Settings", - ), - ] = None, - user_mapping_enabled: Annotated[ - bool | None, - typer.Option( - "--user-mapping/--no-user-mapping", - help="Enable user mapping for Docker", - rich_help_panel="Docker Settings", - ), - ] = None, - user_uid: Annotated[ - int | None, - typer.Option( - "--user-uid", - help="User UID for Docker user mapping", - rich_help_panel="Docker Settings", - ), - ] = None, - user_gid: Annotated[ - int | None, - typer.Option( - "--user-gid", - help="User GID for Docker user mapping", - rich_help_panel="Docker Settings", - ), - ] = None, -) -> None: - """Execute claude CLI commands directly.""" - if args is None: - args = [] - - toolkit = get_rich_toolkit() - - try: - logger = get_logger(__name__) - logger.info( - "cli_command_starting", - command="claude", - version=__version__, - docker=docker, - args=args if args else [], - ) - - settings = Settings.from_config(get_config_path_from_context()) - - # if docker: - # adapter = create_docker_adapter() - # docker_config = get_docker_config_with_fallback(settings) - # toolkit.print_title(f"image {docker_config.docker_image}", tag="docker") - # image, volumes, environment, command, user_context, _ = ( - # adapter.build_docker_run_args( - # settings, - # docker_image=docker_image, - # docker_env=docker_env, - # docker_volume=docker_volume, - # docker_arg=docker_arg, - # docker_home=docker_home, - # docker_workspace=docker_workspace, - # user_mapping_enabled=user_mapping_enabled, - # user_uid=user_uid, - # user_gid=user_gid, - # command=["claude"] + (args or []), - # ) - # ) - # - # cmd_str = " ".join(command or []) - # logger.info( - # "docker_execution", - # image=image, - # command=" ".join(command or []), - # volumes_count=len(volumes), - # env_vars_count=len(environment), - # ) - # toolkit.print(f"Executing: docker run ... {image} {cmd_str}", tag="docker") - # toolkit.print_line() - # - # adapter.exec_container_legacy( - # image=image, - # volumes=volumes, - # environment=environment, - # command=command, - # user_context=user_context, - # ) - # else: - claude_paths = [ - shutil.which("claude"), - Path.home() / ".cache" / ".bun" / "bin" / "claude", - Path.home() / ".local" / "bin" / "claude", - Path("/usr/local/bin/claude"), - ] - - claude_cmd: str | list[str] | None = None - for path in claude_paths: - if path and Path(str(path)).exists(): - claude_cmd = str(path) - break - - if not claude_cmd: - resolver = BinaryResolver() - result = resolver.find_binary("claude", "@anthropic-ai/claude-code") - if result: - claude_cmd = result.command[0] if result.is_direct else result.command - - if not claude_cmd: - toolkit.print("Error: Claude CLI not found.", tag="error") - toolkit.print( - "Please install Claude CLI.", - tag="error", - ) - raise typer.Exit(1) - - if isinstance(claude_cmd, str): - if not Path(claude_cmd).is_absolute(): - claude_cmd = str(Path(claude_cmd).resolve()) - - logger.info("local_claude_execution", claude_path=claude_cmd, args=args) - toolkit.print(f"Executing: {claude_cmd} {' '.join(args)}", tag="claude") - toolkit.print_line() - - try: - os.execvp(claude_cmd, [claude_cmd] + args) - except OSError as e: - toolkit.print(f"Failed to execute command: {e}", tag="error") - raise typer.Exit(1) from e - else: - if not isinstance(claude_cmd, list): - raise ValueError("Expected list for package manager command") - full_cmd = claude_cmd + args - logger.info( - "local_claude_execution_via_package_manager", - command=full_cmd, - package_manager=claude_cmd[0], - ) - toolkit.print(f"Executing: {' '.join(full_cmd)}", tag="claude") - toolkit.print_line() - - try: - proc_result = subprocess.run(full_cmd, check=False) - raise typer.Exit(proc_result.returncode) - except subprocess.SubprocessError as e: - toolkit.print(f"Failed to execute command: {e}", tag="error") - raise typer.Exit(1) from e - - except ConfigurationError as e: - logger = get_logger(__name__) - logger.error("cli_configuration_error", error=str(e), command="claude") - toolkit.print(f"Configuration error: {e}", tag="error") - raise typer.Exit(1) from e - except FileNotFoundError as e: - logger = get_logger(__name__) - logger.error("cli_command_not_found", error=str(e), command="claude") - toolkit.print(f"Claude command not found: {e}", tag="error") - raise typer.Exit(1) from e - except OSError as e: - logger = get_logger(__name__) - logger.error("cli_os_error", error=str(e), command="claude") - toolkit.print(f"System error executing claude command: {e}", tag="error") - raise typer.Exit(1) from e - except Exception as e: - logger = get_logger(__name__) - logger.error("cli_unexpected_error", error=str(e), command="claude") - toolkit.print(f"Error executing claude command: {e}", tag="error") - raise typer.Exit(1) from e diff --git a/ccproxy/core/async_runtime.py b/ccproxy/core/async_runtime.py new file mode 100644 index 00000000..1bbcee68 --- /dev/null +++ b/ccproxy/core/async_runtime.py @@ -0,0 +1,318 @@ +"""Abstraction layer for async primitives used across the project. + +The helpers in this module now lean on ``anyio`` for generic functionality while +preserving compatibility with existing ``asyncio`` constructs that remain in the +codebase (for example ``Task`` and the subprocess helpers). The intent is that +callers continue importing utilities from this module only, allowing us to +evolve the underlying runtime without broad churn. +""" + +from __future__ import annotations + +import asyncio +import functools +import time +from asyncio import subprocess as asyncio_subprocess +from builtins import TimeoutError as BuiltinTimeoutError +from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Iterable +from contextlib import asynccontextmanager +from typing import Any, TypeVar, cast + +import anyio +import sniffio +from anyio import abc as anyio_abc + + +_T = TypeVar("_T") + + +def _get_cancelled_error_type() -> type[BaseException]: + """Best-effort detection of the backend cancellation exception.""" + + try: + return anyio.get_cancelled_exc_class() + except (sniffio.AsyncLibraryNotFoundError, RuntimeError): + # No async context active yet – fall back to asyncio's cancelled error + # since we currently run atop the asyncio backend. + return asyncio.CancelledError + + +class AsyncRuntime: + """Facade around the active async backend.""" + + CancelledError = _get_cancelled_error_type() + TimeoutError = BuiltinTimeoutError + + def create_task( + self, + coro: Coroutine[Any, Any, _T], + *, + name: str | None = None, + ) -> asyncio.Task[_T]: + """Create a background task using the active runtime.""" + return asyncio.create_task(coro, name=name) + + async def gather( + self, + *aws: Awaitable[Any], + return_exceptions: bool = False, + ) -> list[Any]: + """Gather awaitables using the active runtime.""" + results = await asyncio.gather(*aws, return_exceptions=return_exceptions) + return list(results) + + async def wait_for( + self, + awaitable: Awaitable[_T], + timeout: float | None, + ) -> _T: + """Wait for an awaitable with an optional timeout.""" + if timeout is None: + return await awaitable + + try: + with anyio.fail_after(timeout): + return await awaitable + except BuiltinTimeoutError as exc: # pragma: no cover - defensive + raise self.TimeoutError(str(exc)) from exc + + async def wait( + self, + aws: Iterable[asyncio.Future[Any]], + *, + timeout: float | None = None, + return_when: str = asyncio.ALL_COMPLETED, + ) -> tuple[set[asyncio.Future[Any]], set[asyncio.Future[Any]]]: + """Wait for awaitables using the active runtime.""" + done, pending = await asyncio.wait( + aws, timeout=timeout, return_when=return_when + ) + return done, pending + + def create_lock(self) -> anyio.Lock: + """Return a new lock instance.""" + return anyio.Lock() + + def create_event(self) -> asyncio.Event: + """Return a new event instance.""" + return asyncio.Event() + + def create_semaphore(self, value: int) -> anyio.Semaphore: + """Return a new semaphore instance.""" + return anyio.Semaphore(value) + + def create_queue(self, maxsize: int = 0) -> asyncio.Queue[Any]: + """Return a new queue instance.""" + return asyncio.Queue(maxsize=maxsize) + + @asynccontextmanager + async def task_group(self) -> AsyncIterator[anyio_abc.TaskGroup]: + """Yield an anyio task group tied to the active runtime.""" + + async with anyio.create_task_group() as group: + yield group + + def cancel_scope(self) -> anyio.CancelScope: + """Return a new anyio cancel scope.""" + + return anyio.CancelScope() + + def memory_object_stream( + self, max_buffer_size: int = 0 + ) -> tuple[anyio_abc.ObjectSendStream[Any], anyio_abc.ObjectReceiveStream[Any]]: + """Return connected send/receive streams for in-memory messaging.""" + + return anyio.create_memory_object_stream(max_buffer_size) + + def create_future(self) -> asyncio.Future[Any]: + """Create a future bound to the active event loop.""" + loop = asyncio.get_running_loop() + return loop.create_future() + + async def sleep(self, delay: float) -> None: + """Sleep for the requested delay.""" + await anyio.sleep(delay) + + def run(self, awaitable: Awaitable[_T]) -> _T: + """Run an awaitable to completion using the active runtime.""" + if not asyncio.iscoroutine(awaitable): + raise TypeError("runtime.run() expects a coroutine object") + + async def _runner() -> _T: + return cast(_T, await awaitable) + + return anyio.run(_runner) + + async def run_in_executor( + self, func: Callable[..., _T], *args: Any, **kwargs: Any + ) -> _T: + """Execute ``func`` in the default executor.""" + if kwargs: + func = functools.partial(func, **kwargs) + + return await anyio.to_thread.run_sync(func, *args) + + async def to_thread( + self, func: Callable[..., _T], /, *args: Any, **kwargs: Any + ) -> _T: + """Execute ``func`` in a worker thread via the runtime.""" + if kwargs: + func = functools.partial(func, **kwargs) + + return await anyio.to_thread.run_sync(func, *args) + + async def create_subprocess_exec( + self, *cmd: Any, **kwargs: Any + ) -> asyncio_subprocess.Process: + """Spawn a subprocess using the active runtime.""" + return await asyncio.create_subprocess_exec(*cmd, **kwargs) + + def get_loop_time(self) -> float: + """Return the loop's time helper.""" + try: + return anyio.current_time() + except sniffio.AsyncLibraryNotFoundError: + return time.perf_counter() + + def current_task(self) -> asyncio.Task[Any] | None: + """Return the current task, if any.""" + try: + return asyncio.current_task() + except RuntimeError: + return None + + +runtime = AsyncRuntime() + + +def create_task( + coro: Coroutine[Any, Any, _T], + *, + name: str | None = None, +) -> asyncio.Task[_T]: + """Proxy helper for ``AsyncRuntime.create_task``.""" + return runtime.create_task(coro, name=name) + + +async def gather( + *aws: Awaitable[Any], + return_exceptions: bool = False, +) -> list[Any]: + """Gather awaitables using the active runtime.""" + return await runtime.gather(*aws, return_exceptions=return_exceptions) + + +async def wait_for(awaitable: Awaitable[_T], timeout: float | None) -> _T: + """Wait for an awaitable with a timeout using the active runtime.""" + return await runtime.wait_for(awaitable, timeout) + + +async def wait( + aws: Iterable[asyncio.Future[Any]], + *, + timeout: float | None = None, + return_when: str = asyncio.ALL_COMPLETED, +) -> tuple[set[asyncio.Future[Any]], set[asyncio.Future[Any]]]: + """Wait for awaitables using the active runtime.""" + return await runtime.wait(aws, timeout=timeout, return_when=return_when) + + +async def sleep(delay: float) -> None: + """Sleep for the requested delay using the runtime.""" + await runtime.sleep(delay) + + +def run(awaitable: Awaitable[_T]) -> _T: + """Run an awaitable to completion.""" + return runtime.run(awaitable) + + +async def run_in_executor(func: Callable[..., _T], *args: Any, **kwargs: Any) -> _T: + """Execute ``func`` in the default executor via the runtime.""" + return await runtime.run_in_executor(func, *args, **kwargs) + + +async def create_subprocess_exec( + *cmd: Any, **kwargs: Any +) -> asyncio_subprocess.Process: + """Spawn a subprocess via the runtime abstraction.""" + return await runtime.create_subprocess_exec(*cmd, **kwargs) + + +async def to_thread(func: Callable[..., _T], /, *args: Any, **kwargs: Any) -> _T: + """Execute ``func`` using the runtime's thread helper.""" + return await runtime.to_thread(func, *args, **kwargs) + + +def create_lock() -> anyio.Lock: + """Return a runtime-managed lock.""" + return runtime.create_lock() + + +def create_event() -> asyncio.Event: + """Return a runtime-managed event.""" + return runtime.create_event() + + +def create_semaphore(value: int) -> anyio.Semaphore: + """Return a runtime-managed semaphore.""" + return runtime.create_semaphore(value) + + +def create_queue(maxsize: int = 0) -> asyncio.Queue[Any]: + """Return a runtime-managed queue.""" + return runtime.create_queue(maxsize=maxsize) + + +@asynccontextmanager +async def task_group() -> AsyncIterator[anyio_abc.TaskGroup]: + """Yield an anyio task group tied to the runtime.""" + + async with runtime.task_group() as group: + yield group + + +def cancel_scope() -> anyio.CancelScope: + """Return a runtime-managed cancel scope.""" + + return runtime.cancel_scope() + + +def memory_object_stream( + max_buffer_size: int = 0, +) -> tuple[anyio_abc.ObjectSendStream[Any], anyio_abc.ObjectReceiveStream[Any]]: + """Return runtime-managed memory object streams.""" + + return runtime.memory_object_stream(max_buffer_size) + + +def create_future() -> asyncio.Future[Any]: + """Return a runtime-managed future.""" + return runtime.create_future() + + +def loop_time() -> float: + """Return the runtime loop time helper.""" + return runtime.get_loop_time() + + +def current_task() -> asyncio.Task[Any] | None: + """Return the current task from the runtime.""" + return runtime.current_task() + + +CancelledError = AsyncRuntime.CancelledError +TimeoutError = AsyncRuntime.TimeoutError +ALL_COMPLETED = asyncio.ALL_COMPLETED +FIRST_COMPLETED = asyncio.FIRST_COMPLETED +QueueEmpty = asyncio.QueueEmpty +QueueFull = asyncio.QueueFull +Task = asyncio.Task +Event = asyncio.Event +Lock = anyio.Lock +Queue = asyncio.Queue +Future = asyncio.Future +InvalidStateError = asyncio.InvalidStateError +Semaphore = anyio.Semaphore +PIPE = asyncio_subprocess.PIPE +STDOUT = asyncio_subprocess.STDOUT diff --git a/ccproxy/core/async_task_manager.py b/ccproxy/core/async_task_manager.py index 6143d6de..e37c0240 100644 --- a/ccproxy/core/async_task_manager.py +++ b/ccproxy/core/async_task_manager.py @@ -1,17 +1,28 @@ -"""Centralized async task management for lifecycle control and resource cleanup. +"""Centralized async task management with unified lifecycle control.""" -This module provides a centralized task manager that tracks all spawned async tasks, -handles proper cancellation on shutdown, and provides exception handling for -background tasks to prevent resource leaks and unhandled exceptions. -""" +from __future__ import annotations -import asyncio import contextlib import time import uuid -from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, Any, Optional, TypeVar - +from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, TypeVar + +import anyio + +from ccproxy.core.async_runtime import ( + CancelledError, + InvalidStateError, + Task, + create_lock, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + gather as runtime_gather, +) from ccproxy.core.logging import TraceBoundLogger, get_logger @@ -24,59 +35,38 @@ logger: TraceBoundLogger = get_logger(__name__) +@dataclass class TaskInfo: """Information about a managed task.""" - def __init__( - self, - task: asyncio.Task[Any], - name: str, - created_at: float, - creator: str | None = None, - cleanup_callback: Callable[[], None] | None = None, - ): - self.task = task - self.name = name - self.created_at = created_at - self.creator = creator - self.cleanup_callback = cleanup_callback - self.task_id = str(uuid.uuid4()) + task: Task[Any] + name: str + created_at: float + creator: str | None = None + cleanup_callback: Callable[[], None] | None = None + task_id: str = field(default_factory=lambda: str(uuid.uuid4())) @property def age_seconds(self) -> float: - """Get the age of the task in seconds.""" return time.time() - self.created_at @property def is_done(self) -> bool: - """Check if the task is done.""" return self.task.done() @property def is_cancelled(self) -> bool: - """Check if the task was cancelled.""" return self.task.cancelled() def get_exception(self) -> BaseException | None: - """Get the exception if the task failed.""" if self.task.done() and not self.task.cancelled(): - try: + with contextlib.suppress(InvalidStateError): return self.task.exception() - except asyncio.InvalidStateError: - return None return None class AsyncTaskManager: - """Centralized manager for async tasks with lifecycle control. - - This class provides: - - Task registration and tracking - - Automatic cleanup of completed tasks - - Graceful shutdown with cancellation - - Exception handling for background tasks - - Task monitoring and statistics - """ + """Centralized manager for async tasks with lifecycle control.""" def __init__( self, @@ -84,143 +74,137 @@ def __init__( shutdown_timeout: float = 30.0, max_tasks: int = 1000, ): - """Initialize the task manager. - - Args: - cleanup_interval: Interval for cleaning up completed tasks (seconds) - shutdown_timeout: Timeout for graceful shutdown (seconds) - max_tasks: Maximum number of tasks to track (prevents memory leaks) - """ self.cleanup_interval = cleanup_interval self.shutdown_timeout = shutdown_timeout self.max_tasks = max_tasks self._tasks: dict[str, TaskInfo] = {} - self._lock = asyncio.Lock() - self._shutdown_event = asyncio.Event() - self._cleanup_task: asyncio.Task[None] | None = None + self._lock = create_lock() self._started = False + self._active_tasks = 0 + self._completed_tasks = 0 + self._total_tasks_created = 0 async def start(self) -> None: - """Start the task manager and its cleanup task.""" + """Mark the task manager as ready.""" if self._started: logger.warning("task_manager_already_started") return self._started = True - logger.debug("task_manager_starting", cleanup_interval=self.cleanup_interval) - - # Start cleanup task - self._cleanup_task = asyncio.create_task( - self._cleanup_loop(), name="task_manager_cleanup" - ) - logger.debug("task_manager_started") async def stop(self) -> None: - """Stop the task manager and cancel all managed tasks.""" + """Cancel all managed tasks and reset state.""" if not self._started: return logger.debug("task_manager_stopping", active_tasks=len(self._tasks)) - self._shutdown_event.set() - # Stop cleanup task first - if self._cleanup_task and not self._cleanup_task.done(): - self._cleanup_task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await self._cleanup_task + async with self._lock: + tasks_to_cancel: list[Task[Any]] = [] + for info in self._tasks.values(): + if not info.task.done(): + info.task.cancel() + tasks_to_cancel.append(info.task) - # Cancel all managed tasks - await self._cancel_all_tasks() + if tasks_to_cancel: + try: + with anyio.move_on_after(self.shutdown_timeout) as scope: + await runtime_gather(*tasks_to_cancel, return_exceptions=True) + if scope.cancel_called: + logger.warning( + "task_cancellation_timeout", + timeout=self.shutdown_timeout, + remaining_tasks=sum( + not task.done() for task in tasks_to_cancel + ), + ) + except Exception as exc: # pragma: no cover - defensive + logger.error( + "task_manager_shutdown_error", + error=str(exc), + error_type=type(exc).__name__, + exc_info=True, + ) - # Clear task registry async with self._lock: self._tasks.clear() + self._active_tasks = 0 self._started = False logger.debug("task_manager_stopped") async def create_task( self, - coro: Awaitable[T], + coro: Coroutine[Any, Any, T], *, name: str | None = None, creator: str | None = None, cleanup_callback: Callable[[], None] | None = None, - ) -> asyncio.Task[T]: - """Create a managed task. - - Args: - coro: Coroutine to execute - name: Optional name for the task (auto-generated if None) - creator: Optional creator identifier for debugging - cleanup_callback: Optional callback to run when task completes - - Returns: - The created task - - Raises: - RuntimeError: If task manager is not started or has too many tasks - """ + ) -> Task[T]: + """Create a managed background task.""" if not self._started: raise RuntimeError("Task manager is not started") - # Check task limit - if len(self._tasks) >= self.max_tasks: - logger.warning( - "task_manager_at_capacity", - current_tasks=len(self._tasks), - max_tasks=self.max_tasks, - ) - # Clean up completed tasks to make room - await self._cleanup_completed_tasks() - - if len(self._tasks) >= self.max_tasks: + async with self._lock: + if self._active_tasks >= self.max_tasks: + logger.warning( + "task_manager_at_capacity", + current_tasks=self._active_tasks, + max_tasks=self.max_tasks, + ) raise RuntimeError(f"Task manager at capacity ({self.max_tasks} tasks)") + self._active_tasks += 1 + task_name = name or f"managed_task_{self._active_tasks}" - # Generate name if not provided - if name is None: - name = f"managed_task_{len(self._tasks)}" - - # Create the task with exception handling - task = asyncio.create_task( - self._wrap_with_exception_handling(coro, name), - name=name, - ) - - # Register the task + async def managed() -> T: + try: + return await self._wrap_with_exception_handling(coro, task_name) + finally: + if cleanup_callback: + with contextlib.suppress(Exception): + cleanup_callback() + await self._remove_task(task_id) + + task = runtime_create_task(managed(), name=task_name) task_info = TaskInfo( task=task, - name=name, + name=task_name, created_at=time.time(), creator=creator, cleanup_callback=cleanup_callback, ) - async with self._lock: - self._tasks[task_info.task_id] = task_info + task_id = task_info.task_id - # Add done callback for automatic cleanup - task.add_done_callback(lambda t: self._schedule_cleanup_callback(task_info)) + async with self._lock: + self._tasks[task_id] = task_info + self._total_tasks_created += 1 logger.debug( "task_created", - task_id=task_info.task_id, - task_name=name, + task_id=task_id, + task_name=task_name, creator=creator, total_tasks=len(self._tasks), ) return task + async def _remove_task(self, task_id: str) -> None: + """Remove a task from tracking when it completes.""" + async with self._lock: + self._tasks.pop(task_id, None) + self._active_tasks = max(0, self._active_tasks - 1) + self._completed_tasks += 1 + async def _wrap_with_exception_handling( - self, coro: Awaitable[T], task_name: str + self, coro: Coroutine[Any, Any, T], task_name: str ) -> T: - """Wrap coroutine with exception handling.""" try: return await coro - except asyncio.CancelledError: + except CancelledError: logger.debug("task_cancelled", task_name=task_name) raise except Exception as e: @@ -233,99 +217,7 @@ async def _wrap_with_exception_handling( ) raise - def _schedule_cleanup_callback(self, task_info: TaskInfo) -> None: - """Schedule cleanup callback for completed task.""" - try: - # Run cleanup callback if provided - if task_info.cleanup_callback: - task_info.cleanup_callback() - except Exception as e: - logger.warning( - "task_cleanup_callback_failed", - task_id=task_info.task_id, - task_name=task_info.name, - error=str(e), - exc_info=True, - ) - - async def _cleanup_loop(self) -> None: - """Background loop for cleaning up completed tasks.""" - logger.debug("task_cleanup_loop_started") - - while not self._shutdown_event.is_set(): - try: - await asyncio.wait_for( - self._shutdown_event.wait(), timeout=self.cleanup_interval - ) - break # Shutdown event set - except TimeoutError: - pass # Continue with cleanup - - await self._cleanup_completed_tasks() - - logger.debug("task_cleanup_loop_stopped") - - async def _cleanup_completed_tasks(self) -> None: - """Clean up completed tasks from the registry.""" - completed_tasks = [] - - async with self._lock: - for task_id, task_info in list(self._tasks.items()): - if task_info.is_done: - completed_tasks.append((task_id, task_info)) - del self._tasks[task_id] - - if completed_tasks: - logger.debug( - "tasks_cleaned_up", - completed_count=len(completed_tasks), - remaining_tasks=len(self._tasks), - ) - - # Log any task exceptions - for task_id, task_info in completed_tasks: - if task_info.get_exception(): - logger.warning( - "completed_task_had_exception", - task_id=task_id, - task_name=task_info.name, - exception=str(task_info.get_exception()), - ) - - async def _cancel_all_tasks(self) -> None: - """Cancel all managed tasks with timeout.""" - if not self._tasks: - return - - logger.debug("cancelling_all_tasks", task_count=len(self._tasks)) - - # Cancel all tasks - tasks_to_cancel = [] - async with self._lock: - for task_info in self._tasks.values(): - if not task_info.is_done: - task_info.task.cancel() - tasks_to_cancel.append(task_info.task) - - if not tasks_to_cancel: - return - - # Wait for cancellation with timeout - try: - await asyncio.wait_for( - asyncio.gather(*tasks_to_cancel, return_exceptions=True), - timeout=self.shutdown_timeout, - ) - logger.debug("all_tasks_cancelled_gracefully") - except TimeoutError: - logger.warning( - "task_cancellation_timeout", - timeout=self.shutdown_timeout, - remaining_tasks=sum(1 for t in tasks_to_cancel if not t.done()), - ) - async def get_task_stats(self) -> dict[str, Any]: - """Get statistics about managed tasks.""" async with self._lock: active_tasks = sum(1 for t in self._tasks.values() if not t.is_done) cancelled_tasks = sum(1 for t in self._tasks.values() if t.is_cancelled) @@ -334,63 +226,43 @@ async def get_task_stats(self) -> dict[str, Any]: for t in self._tasks.values() if t.is_done and not t.is_cancelled and t.get_exception() ) - return { - "total_tasks": len(self._tasks), + "total_tasks": self._total_tasks_created, "active_tasks": active_tasks, "cancelled_tasks": cancelled_tasks, "failed_tasks": failed_tasks, - "completed_tasks": len(self._tasks) - active_tasks, + "completed_tasks": self._completed_tasks, "started": self._started, "max_tasks": self.max_tasks, } async def list_active_tasks(self) -> list[dict[str, Any]]: - """Get list of active tasks with details.""" - active_tasks = [] - async with self._lock: - for task_info in self._tasks.values(): - if not task_info.is_done: - active_tasks.append( - { - "task_id": task_info.task_id, - "name": task_info.name, - "creator": task_info.creator, - "age_seconds": task_info.age_seconds, - "created_at": task_info.created_at, - } - ) - - return active_tasks + return [ + { + "task_id": info.task_id, + "name": info.name, + "creator": info.creator, + "age_seconds": info.age_seconds, + "created_at": info.created_at, + } + for info in self._tasks.values() + if not info.is_done + ] @property def is_started(self) -> bool: - """Check if the task manager is started.""" return self._started -# Dependency-injected access helpers +# Dependency-injected helpers def _resolve_task_manager( *, - container: Optional["ServiceContainer"] = None, - task_manager: Optional["AsyncTaskManager"] = None, -) -> "AsyncTaskManager": - """Resolve the async task manager instance using dependency injection. - - Args: - container: Optional service container to resolve the manager from - task_manager: Optional explicit manager instance (takes precedence) - - Returns: - AsyncTaskManager instance - - Raises: - RuntimeError: If the manager cannot be resolved - """ - + container: ServiceContainer | None = None, + task_manager: AsyncTaskManager | None = None, +) -> AsyncTaskManager: if task_manager is not None: return task_manager @@ -415,28 +287,14 @@ def _resolve_task_manager( async def create_managed_task( - coro: Awaitable[T], + coro: Coroutine[Any, Any, T], *, name: str | None = None, creator: str | None = None, cleanup_callback: Callable[[], None] | None = None, - container: Optional["ServiceContainer"] = None, - task_manager: Optional["AsyncTaskManager"] = None, -) -> asyncio.Task[T]: - """Create a managed task using the dependency-injected task manager. - - Args: - coro: Coroutine to execute - name: Optional name for the task - creator: Optional creator identifier - cleanup_callback: Optional cleanup callback - container: Optional service container for resolving the task manager - task_manager: Optional explicit task manager instance - - Returns: - The created managed task - """ - + container: ServiceContainer | None = None, + task_manager: AsyncTaskManager | None = None, +) -> Task[T]: manager = _resolve_task_manager(container=container, task_manager=task_manager) return await manager.create_task( coro, name=name, creator=creator, cleanup_callback=cleanup_callback @@ -445,72 +303,42 @@ async def create_managed_task( async def start_task_manager( *, - container: Optional["ServiceContainer"] = None, - task_manager: Optional["AsyncTaskManager"] = None, + container: ServiceContainer | None = None, + task_manager: AsyncTaskManager | None = None, ) -> None: - """Start the dependency-injected task manager.""" - manager = _resolve_task_manager(container=container, task_manager=task_manager) await manager.start() async def stop_task_manager( *, - container: Optional["ServiceContainer"] = None, - task_manager: Optional["AsyncTaskManager"] = None, + container: ServiceContainer | None = None, + task_manager: AsyncTaskManager | None = None, ) -> None: - """Stop the dependency-injected task manager.""" - manager = _resolve_task_manager(container=container, task_manager=task_manager) await manager.stop() def create_fire_and_forget_task( - coro: Awaitable[T], + coro: Coroutine[Any, Any, T], *, name: str | None = None, creator: str | None = None, - container: Optional["ServiceContainer"] = None, - task_manager: Optional["AsyncTaskManager"] = None, + container: ServiceContainer | None = None, + task_manager: AsyncTaskManager | None = None, ) -> None: - """Create a fire-and-forget managed task from a synchronous context. - - This function schedules a coroutine to run as a managed task without - needing to await it. Useful for calling from synchronous functions - that need to schedule background work. - - Args: - coro: Coroutine to execute - name: Optional name for the task - creator: Optional creator identifier - container: Optional service container to resolve the task manager - task_manager: Optional explicit task manager instance - """ - manager = _resolve_task_manager(container=container, task_manager=task_manager) if not manager.is_started: - # If task manager isn't started, fall back to regular asyncio.create_task logger.warning( "task_manager_not_started_fire_and_forget", name=name, creator=creator, ) - asyncio.create_task(coro, name=name) # type: ignore[arg-type] + runtime_create_task(coro, name=name) return - # Schedule the task creation as a fire-and-forget operation async def _create_managed_task() -> None: - try: - await manager.create_task(coro, name=name, creator=creator) - except Exception as e: - logger.error( - "fire_and_forget_task_creation_failed", - name=name, - creator=creator, - error=str(e), - exc_info=True, - ) + await manager.create_task(coro, name=name, creator=creator) - # Use asyncio.create_task to schedule the managed task creation - asyncio.create_task(_create_managed_task(), name=f"create_{name or 'unnamed'}") + runtime_create_task(_create_managed_task(), name=f"create_{name or 'unnamed'}") diff --git a/ccproxy/core/async_utils.py b/ccproxy/core/async_utils.py index 0a02b776..1da4aa86 100644 --- a/ccproxy/core/async_utils.py +++ b/ccproxy/core/async_utils.py @@ -1,12 +1,29 @@ """Async utilities for the CCProxy API.""" -import asyncio import re from collections.abc import AsyncIterator, Awaitable, Callable, Iterator from contextlib import asynccontextmanager, contextmanager +from inspect import isawaitable from pathlib import Path from typing import Any, TypeVar, cast +from ccproxy.core.async_runtime import ( + CancelledError, + create_semaphore, + loop_time, +) +from ccproxy.core.async_runtime import ( + gather as runtime_gather, +) +from ccproxy.core.async_runtime import ( + run_in_executor as runtime_run_in_executor, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.logging import get_logger @@ -81,15 +98,7 @@ async def run_in_executor(func: Callable[..., T], *args: Any, **kwargs: Any) -> Returns: The result of the function call """ - loop = asyncio.get_event_loop() - - # Create a partial function if we have kwargs - if kwargs: - from functools import partial - - func = partial(func, **kwargs) - - return await loop.run_in_executor(None, func, *args) + return await runtime_run_in_executor(func, *args, **kwargs) async def safe_await(awaitable: Awaitable[T], timeout: float | None = None) -> T | None: @@ -104,11 +113,11 @@ async def safe_await(awaitable: Awaitable[T], timeout: float | None = None) -> T """ try: if timeout is not None: - return await asyncio.wait_for(awaitable, timeout=timeout) + return await runtime_wait_for(awaitable, timeout) return await awaitable except TimeoutError: return None - except asyncio.CancelledError: + except CancelledError: return None except Exception as e: logger = get_logger(__name__) @@ -129,7 +138,7 @@ async def gather_with_concurrency( Returns: List of results from the awaitables """ - semaphore = asyncio.Semaphore(limit) + semaphore = create_semaphore(limit) async def _limited_awaitable(awaitable: Awaitable[T]) -> T: async with semaphore: @@ -137,9 +146,9 @@ async def _limited_awaitable(awaitable: Awaitable[T]) -> T: limited_awaitables = [_limited_awaitable(aw) for aw in awaitables] if return_exceptions: - return await asyncio.gather(*limited_awaitables, return_exceptions=True) + return await runtime_gather(*limited_awaitables, return_exceptions=True) else: - return await asyncio.gather(*limited_awaitables) + return await runtime_gather(*limited_awaitables) @asynccontextmanager @@ -194,7 +203,7 @@ async def retry_async( except exceptions as e: last_exception = e if attempt < max_retries: - await asyncio.sleep(current_delay) + await runtime_sleep(current_delay) current_delay *= backoff else: raise @@ -218,26 +227,26 @@ async def wait_for_condition( Returns: True if condition was met, False if timeout occurred """ - start_time = asyncio.get_event_loop().time() + start_time = loop_time() while True: try: result = condition() - if asyncio.iscoroutine(result): + if isawaitable(result): result = await result if result: return True - except (asyncio.CancelledError, KeyboardInterrupt): + except (CancelledError, KeyboardInterrupt): return False except Exception as e: logger = get_logger(__name__) logger.debug("condition_check_error", error=str(e), exc_info=e) pass - if asyncio.get_event_loop().time() - start_time > timeout: + if loop_time() - start_time > timeout: return False - await asyncio.sleep(interval) + await runtime_sleep(interval) _cache: dict[str, tuple[float, Any]] = {} diff --git a/ccproxy/core/plugins/hooks/manager.py b/ccproxy/core/plugins/hooks/manager.py index d46658a6..5977151f 100644 --- a/ccproxy/core/plugins/hooks/manager.py +++ b/ccproxy/core/plugins/hooks/manager.py @@ -5,7 +5,9 @@ both async and sync hooks. """ -import asyncio +from __future__ import annotations + +import inspect from datetime import datetime from typing import Any @@ -68,8 +70,7 @@ async def emit( ) if fire_and_forget and self._background_manager: - # Execute in background thread - non-blocking - self._background_manager.emit_async(context, self._registry) + await self._background_manager.emit_async(context, self._registry) return elif fire_and_forget and not self._background_manager: # No background manager available, log warning and fall back to sync @@ -119,8 +120,7 @@ async def emit_with_context( fire_and_forget: If True, execute hooks in background thread (default) """ if fire_and_forget and self._background_manager: - # Execute in background thread - non-blocking - self._background_manager.emit_async(context, self._registry) + await self._background_manager.emit_async(context, self._registry) return elif fire_and_forget and not self._background_manager: # No background manager available, log warning and fall back to sync @@ -172,15 +172,15 @@ async def _execute_hook(self, hook: Hook, context: HookContext) -> None: context: The context to pass to the hook """ result = hook(context) - if asyncio.iscoroutine(result): + if inspect.isawaitable(result): await result # If result is None, it was a sync hook and we're done - def shutdown(self) -> None: + async def shutdown(self) -> None: """Shutdown the background hook processing. This method should be called during application shutdown to ensure proper cleanup of the background thread. """ if self._background_manager: - self._background_manager.stop() + await self._background_manager.shutdown() diff --git a/ccproxy/core/plugins/hooks/thread_manager.py b/ccproxy/core/plugins/hooks/thread_manager.py index 98404bd4..2794edb0 100644 --- a/ccproxy/core/plugins/hooks/thread_manager.py +++ b/ccproxy/core/plugins/hooks/thread_manager.py @@ -1,18 +1,35 @@ -"""Background thread manager for async hook execution.""" +"""Async hook execution manager built on top of the runtime task manager.""" -import asyncio -import threading +from __future__ import annotations + +import inspect import uuid from dataclasses import dataclass, field from datetime import datetime from typing import Any +import anyio import structlog +from ccproxy.core.async_runtime import ( + Task, + create_lock, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + current_task as runtime_current_task, +) +from ccproxy.core.async_runtime import ( + gather as runtime_gather, +) +from ccproxy.core.async_task_manager import AsyncTaskManager + from .base import Hook, HookContext -logger = structlog.get_logger(__name__) +logger = structlog.get_logger(__name__).bind(component="hook_executor") @dataclass @@ -25,179 +42,87 @@ class HookTask: class BackgroundHookThreadManager: - """Manages a dedicated async thread for hook execution.""" - - def __init__(self) -> None: - """Initialize the background thread manager.""" - self._loop: asyncio.AbstractEventLoop | None = None - self._thread: threading.Thread | None = None - self._queue: asyncio.Queue[tuple[HookTask, Any]] | None = None - self._shutdown_event: asyncio.Event | None = None - self._running = False - self._logger = logger.bind(component="background_hook_thread") - # Signals when the background loop and its resources are ready - self._ready_event: threading.Event | None = None - - def start(self) -> None: - """Start the background thread with its own event loop.""" - if self._running: - return - - # Create readiness event so callers can safely enqueue without sleeps - self._ready_event = threading.Event() - - self._thread = threading.Thread( - target=self._run_background_loop, name="hook-background-thread", daemon=True - ) - self._thread.start() + """Fire-and-forget hook execution via the async task manager.""" - # Block briefly until the background loop has initialized its resources - if self._ready_event and not self._ready_event.wait(timeout=1.0): - self._logger.warning("background_hook_thread_startup_timeout") - self._running = True + def __init__(self, task_manager: AsyncTaskManager | None = None) -> None: + self._task_manager = task_manager + self._tasks: set[Task[Any]] = set() + self._lock = create_lock() - self._logger.debug("background_hook_thread_started") + async def emit_async(self, context: HookContext, registry: Any) -> None: + """Schedule hook execution without blocking the caller.""" - def stop(self, timeout: float = 5.0) -> None: - """Gracefully shutdown the background thread.""" - if not self._running: - return - - self._logger.debug("stopping_background_hook_thread") + hook_task = HookTask(context=context) - # Signal shutdown to the background loop - if self._loop and self._shutdown_event: - self._loop.call_soon_threadsafe(self._shutdown_event.set) + async def runner() -> None: + task = runtime_current_task() + try: + await self._execute_task(hook_task, registry) + finally: + if task: + async with self._lock: + self._tasks.discard(task) - # Wait for thread to complete - if self._thread: - self._thread.join(timeout=timeout) - if self._thread.is_alive(): - self._logger.warning("background_thread_shutdown_timeout") + task_name = f"hook_{getattr(context.event, 'value', context.event)}" - self._running = False - self._loop = None - self._thread = None - self._queue = None - self._shutdown_event = None - self._ready_event = None + if self._task_manager: + task = await self._task_manager.create_task( + runner(), name=task_name, creator="HookManager" + ) + else: + task = runtime_create_task(runner(), name=task_name) - self._logger.debug("background_hook_thread_stopped") + async with self._lock: + self._tasks.add(task) - def emit_async(self, context: HookContext, registry: Any) -> None: - """Queue a hook task for background execution. + async def stop(self, timeout: float = 5.0) -> None: + """Cancel any outstanding hook executions.""" - Args: - context: Hook context to execute - registry: Hook registry to get hooks from - """ - if not self._running: - self.start() + async with self._lock: + tasks = list(self._tasks) + self._tasks.clear() - if not self._loop or not self._queue: - self._logger.warning("background_thread_not_ready_dropping_task") + if not tasks: return - task = HookTask(context=context) - - # Add task to queue in a thread-safe way - try: - self._loop.call_soon_threadsafe(self._add_task_to_queue, task, registry) - except Exception as e: - self._logger.error("failed_to_queue_hook_task", error=str(e)) - - def _add_task_to_queue(self, task: HookTask, registry: Any) -> None: - """Add task to queue (called from background thread).""" - if self._queue: - try: - self._queue.put_nowait((task, registry)) - except asyncio.QueueFull: - self._logger.warning("hook_task_queue_full_dropping_task") + for task in tasks: + task.cancel() - def _run_background_loop(self) -> None: - """Run the background event loop for hook processing.""" try: - # Create new event loop for this thread - self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) - - # Create queue and shutdown event - self._queue = asyncio.Queue[tuple[HookTask, Any]](maxsize=1000) - self._shutdown_event = asyncio.Event() - - # Signal to the starter that we're ready to accept tasks - if self._ready_event: - self._ready_event.set() - - # Run the processing loop - self._loop.run_until_complete(self._process_tasks()) - except Exception as e: - logger.error("background_hook_thread_error", error=str(e)) - finally: - if self._loop: - self._loop.close() - - async def _process_tasks(self) -> None: - """Main task processing loop.""" - self._logger.debug("background_hook_processor_started") - - while self._shutdown_event and not self._shutdown_event.is_set(): - try: - # Wait for either a task or shutdown signal - if not self._queue: - break - task_data = await asyncio.wait_for(self._queue.get(), timeout=0.1) - - task, registry = task_data - await self._execute_task(task, registry) - - except TimeoutError: - # Normal timeout, continue loop - continue - except Exception as e: - self._logger.error("hook_task_processing_error", error=str(e)) - - self._logger.debug("background_hook_processor_stopped") + with anyio.move_on_after(timeout): + await runtime_gather(*tasks, return_exceptions=True) + except Exception as exc: # pragma: no cover - defensive logging + logger.warning("hook_executor_stop_error", error=str(exc), exc_info=True) async def _execute_task(self, task: HookTask, registry: Any) -> None: - """Execute a single hook task. - - Args: - task: The hook task to execute - registry: Hook registry to get hooks from - """ try: hooks = registry.get(task.context.event) if not hooks: return - # Execute all hooks for this event for hook in hooks: try: await self._execute_hook(hook, task.context) - except Exception as e: - self._logger.error( + except Exception as exc: + logger.error( "background_hook_execution_failed", hook=hook.name, event_type=task.context.event.value if hasattr(task.context.event, "value") else str(task.context.event), - error=str(e), + error=str(exc), task_id=task.task_id, ) - except Exception as e: - self._logger.error( - "hook_task_execution_failed", error=str(e), task_id=task.task_id + except Exception as exc: + logger.error( + "hook_task_execution_failed", error=str(exc), task_id=task.task_id ) async def _execute_hook(self, hook: Hook, context: HookContext) -> None: - """Execute a single hook with proper async/sync handling. - - Args: - hook: The hook to execute - context: The context to pass to the hook - """ result = hook(context) - if asyncio.iscoroutine(result): + if inspect.isawaitable(result): await result - # If result is None, it was a sync hook and we're done + + # Backwards compatibility helper used by HookManager.shutdown() + async def shutdown(self) -> None: + await self.stop() diff --git a/ccproxy/core/request_context.py b/ccproxy/core/request_context.py index 2771990e..302da1f0 100644 --- a/ccproxy/core/request_context.py +++ b/ccproxy/core/request_context.py @@ -15,7 +15,6 @@ from __future__ import annotations -import asyncio import json import time import uuid @@ -28,6 +27,7 @@ import structlog +from ccproxy.core.async_runtime import create_lock from ccproxy.core.logging import TraceBoundLogger, get_logger @@ -401,7 +401,7 @@ class ContextTracker: def __init__(self) -> None: self._active_contexts: dict[str, RequestContext] = {} - self._lock = asyncio.Lock() + self._lock = create_lock() async def add_context(self, context: RequestContext) -> None: """Add an active request context.""" diff --git a/ccproxy/http/pool.py b/ccproxy/http/pool.py index 97002bcc..47ff729f 100644 --- a/ccproxy/http/pool.py +++ b/ccproxy/http/pool.py @@ -5,7 +5,6 @@ Implements Phase 2.3 of the refactoring plan. """ -import asyncio from typing import Any from urllib.parse import urlparse @@ -14,6 +13,7 @@ from ccproxy.config.settings import Settings from ccproxy.config.utils import HTTP_STREAMING_TIMEOUT +from ccproxy.core.async_runtime import create_lock from ccproxy.http.client import HTTPClientFactory @@ -43,7 +43,7 @@ def __init__( self.hook_manager = hook_manager self._pools: dict[str, httpx.AsyncClient] = {} self._shared_client: httpx.AsyncClient | None = None - self._lock = asyncio.Lock() + self._lock = create_lock() logger.trace("http_pool_manager_initialized", category="lifecycle") diff --git a/ccproxy/plugins/access_log/writer.py b/ccproxy/plugins/access_log/writer.py index 87f422c1..55451431 100644 --- a/ccproxy/plugins/access_log/writer.py +++ b/ccproxy/plugins/access_log/writer.py @@ -1,9 +1,20 @@ -import asyncio +import contextlib import time from pathlib import Path import aiofiles +from ccproxy.core.async_runtime import ( + CancelledError, + Task, + create_lock, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) from ccproxy.core.logging import get_plugin_logger @@ -16,7 +27,7 @@ class AccessLogWriter: Features: - Async file I/O for performance - Optional buffering to reduce I/O operations - - Thread-safe with asyncio.Lock + - Thread-safe with the runtime lock abstraction - Auto-creates parent directories """ @@ -38,8 +49,8 @@ def __init__( self.flush_interval = flush_interval self._buffer: list[str] = [] - self._lock = asyncio.Lock() - self._flush_task: asyncio.Task[None] | None = None + self._lock = create_lock() + self._flush_task: Task[None] | None = None self._last_flush = time.time() # Ensure parent directory exists @@ -92,11 +103,11 @@ def _schedule_flush(self) -> None: if self._flush_task and not self._flush_task.done(): return # Already scheduled - self._flush_task = asyncio.create_task(self._auto_flush()) + self._flush_task = runtime_create_task(self._auto_flush()) async def _auto_flush(self) -> None: """Automatically flush the buffer after the flush interval.""" - await asyncio.sleep(self.flush_interval) + await runtime_sleep(self.flush_interval) async with self._lock: await self._flush() @@ -107,3 +118,5 @@ async def close(self) -> None: if self._flush_task and not self._flush_task.done(): self._flush_task.cancel() + with contextlib.suppress(CancelledError): + await self._flush_task diff --git a/ccproxy/plugins/analytics/ingest.py b/ccproxy/plugins/analytics/ingest.py index 4f7f8a17..9cc8d067 100644 --- a/ccproxy/plugins/analytics/ingest.py +++ b/ccproxy/plugins/analytics/ingest.py @@ -1,12 +1,13 @@ from __future__ import annotations -import asyncio import time from datetime import datetime from typing import Any from sqlmodel import Session +from ccproxy.core.async_runtime import to_thread as runtime_to_thread + from .models import AccessLog @@ -74,7 +75,7 @@ async def ingest(self, log_data: dict[str, Any]) -> bool: try: # Execute the DB write in a thread to avoid blocking the event loop - return await asyncio.to_thread(self._insert_sync, row) + return await runtime_to_thread(self._insert_sync, row) except Exception: return False diff --git a/ccproxy/plugins/claude_api/detection_service.py b/ccproxy/plugins/claude_api/detection_service.py index 74292159..92c2f429 100644 --- a/ccproxy/plugins/claude_api/detection_service.py +++ b/ccproxy/plugins/claude_api/detection_service.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import json import os import socket @@ -14,6 +13,24 @@ from ccproxy.config.settings import Settings from ccproxy.config.utils import get_ccproxy_cache_dir +from ccproxy.core.async_runtime import ( + FIRST_COMPLETED, + PIPE, + CancelledError, + create_event, +) +from ccproxy.core.async_runtime import ( + create_subprocess_exec as runtime_create_subprocess_exec, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + wait as runtime_wait, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.logging import get_plugin_logger from ccproxy.models.detection import DetectedHeaders, DetectedPrompts from ccproxy.services.cli_detection import CLIDetectionService @@ -265,7 +282,7 @@ async def capture_handler(request: Request) -> Response: config = Config(temp_app, host="127.0.0.1", port=port, log_level="error") server = Server(config) - server_ready = asyncio.Event() + server_ready = create_event() @temp_app.on_event("startup") async def signal_server_ready() -> None: @@ -273,14 +290,14 @@ async def signal_server_ready() -> None: server_ready.set() - server_task = asyncio.create_task(server.serve()) - ready_task = asyncio.create_task(server_ready.wait()) + server_task = runtime_create_task(server.serve()) + ready_task = runtime_create_task(server_ready.wait()) try: - done, _pending = await asyncio.wait( + done, _pending = await runtime_wait( {ready_task, server_task}, timeout=5, - return_when=asyncio.FIRST_COMPLETED, + return_when=FIRST_COMPLETED, ) if ready_task in done: await ready_task @@ -318,16 +335,16 @@ async def signal_server_ready() -> None: cmd = cli_info["command"] + ["test"] - process = await asyncio.create_subprocess_exec( + process = await runtime_create_subprocess_exec( *cmd, env=env, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=PIPE, + stderr=PIPE, cwd=str(cwd_path), ) try: - await asyncio.wait_for(process.wait(), timeout=30) + await runtime_wait_for(process.wait(), timeout=30) except TimeoutError: process.kill() await process.wait() @@ -337,7 +354,7 @@ async def signal_server_ready() -> None: finally: if not ready_task.done(): ready_task.cancel() - with suppress(asyncio.CancelledError): + with suppress(CancelledError): await ready_task server.should_exit = True diff --git a/ccproxy/plugins/claude_sdk/adapter.py b/ccproxy/plugins/claude_sdk/adapter.py index 75f7988f..ed6326f1 100644 --- a/ccproxy/plugins/claude_sdk/adapter.py +++ b/ccproxy/plugins/claude_sdk/adapter.py @@ -1,6 +1,5 @@ """Claude SDK adapter implementation using delegation pattern.""" -import asyncio import json import uuid from collections.abc import AsyncIterator @@ -12,6 +11,7 @@ from starlette.responses import Response, StreamingResponse from ccproxy.config.utils import OPENAI_CHAT_COMPLETIONS_PATH +from ccproxy.core.async_runtime import CancelledError from ccproxy.core.logging import get_plugin_logger from ccproxy.core.request_context import RequestContext from ccproxy.llms.streaming import OpenAIStreamProcessor @@ -377,7 +377,7 @@ async def adapted_stream_generator() -> AsyncIterator[bytes]: request_context=request_context, ): yield sse_chunk - except asyncio.CancelledError as exc: + except CancelledError as exc: logger.warning( "streaming_cancelled", error=str(exc), @@ -482,7 +482,7 @@ async def stream_generator() -> AsyncIterator[bytes]: yield chunk else: yield str(chunk).encode() - except asyncio.CancelledError as exc: + except CancelledError as exc: logger.warning( "streaming_cancelled", error=str(exc), @@ -599,7 +599,7 @@ async def stream_generator() -> AsyncIterator[bytes]: category="http", ) raise HTTPException(status_code=502, detail=f"HTTP error: {e}") from e - except asyncio.CancelledError as e: + except CancelledError as e: logger.warning( "request_cancelled", error=str(e), diff --git a/ccproxy/plugins/claude_sdk/client.py b/ccproxy/plugins/claude_sdk/client.py index d9fabd47..2d23e882 100644 --- a/ccproxy/plugins/claude_sdk/client.py +++ b/ccproxy/plugins/claude_sdk/client.py @@ -1,12 +1,21 @@ """Claude SDK client wrapper for handling core Claude Code SDK interactions.""" -import asyncio import contextlib from collections.abc import AsyncIterator from typing import Any, TypeVar, cast from pydantic import BaseModel +from ccproxy.core.async_runtime import ( + Task, + create_task, +) +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_utils import patched_typing from ccproxy.core.errors import ClaudeProxyError, ServiceUnavailableError from ccproxy.core.logging import get_plugin_logger @@ -541,11 +550,11 @@ async def _wait_for_first_chunk( logger.debug( "waiting_for_first_chunk", timeout=timeout_seconds, category="streaming" ) - first_message = await asyncio.wait_for( + first_message = await runtime_wait_for( anext(message_iterator), timeout=timeout_seconds ) return first_message, message_iterator - except TimeoutError: + except RuntimeTimeoutError: # Check if session pool is enabled - if so, let it handle the timeout has_session_pool = ( self._session_manager and await self._session_manager.has_session_pool() @@ -666,7 +675,7 @@ async def _create_drain_task( session_client: Any, request_id: str | None = None, session_id: str | None = None, - ) -> asyncio.Task[None]: + ) -> Task[None]: """Create a background task to drain remaining messages from stream. Args: @@ -717,7 +726,7 @@ async def drain_stream() -> None: session_client.has_active_stream = False session_client.active_stream_task = None - return asyncio.create_task(drain_stream()) + return create_task(drain_stream()) def _convert_message(self, message: Any, model_class: type[T]) -> T: """Convert SDK message to Pydantic model.""" diff --git a/ccproxy/plugins/claude_sdk/manager.py b/ccproxy/plugins/claude_sdk/manager.py index 25815cf3..efe3b489 100644 --- a/ccproxy/plugins/claude_sdk/manager.py +++ b/ccproxy/plugins/claude_sdk/manager.py @@ -7,7 +7,6 @@ from __future__ import annotations -import asyncio from collections.abc import Callable # Type alias for metrics factory function @@ -15,6 +14,7 @@ from claude_agent_sdk import ClaudeAgentOptions +from ccproxy.core.async_runtime import Lock, create_lock from ccproxy.core.errors import ClaudeProxyError from ccproxy.core.logging import get_plugin_logger @@ -47,7 +47,7 @@ def __init__( self.config = config self._session_pool: SessionPool | None = None - self._lock = asyncio.Lock() + self._lock: Lock = create_lock() self._metrics_factory = metrics_factory # Initialize session pool if enabled diff --git a/ccproxy/plugins/claude_sdk/message_queue.py b/ccproxy/plugins/claude_sdk/message_queue.py index 284ad0dc..46baf95b 100644 --- a/ccproxy/plugins/claude_sdk/message_queue.py +++ b/ccproxy/plugins/claude_sdk/message_queue.py @@ -2,15 +2,21 @@ from __future__ import annotations -import asyncio import contextlib import time import uuid from collections.abc import AsyncIterator from dataclasses import dataclass, field from enum import Enum -from typing import Any, TypeVar - +from typing import Any, TypeVar, cast + +from ccproxy.core.async_runtime import ( + Queue, + QueueEmpty, + QueueFull, + create_lock, + create_queue, +) from ccproxy.core.logging import get_plugin_logger @@ -48,7 +54,7 @@ def __init__(self, listener_id: str | None = None): listener_id: Optional ID for the listener, generated if not provided """ self.listener_id = listener_id or str(uuid.uuid4()) - self._queue: asyncio.Queue[QueueMessage] = asyncio.Queue() + self._queue: Queue[QueueMessage] = cast(Queue[QueueMessage], create_queue()) self._closed = False self._created_at = time.time() @@ -59,12 +65,13 @@ async def get_message(self) -> QueueMessage: The next queued message Raises: - asyncio.QueueEmpty: If queue is empty and closed + QueueEmpty: If queue is empty and closed """ if self._closed and self._queue.empty(): - raise asyncio.QueueEmpty("Listener is closed") + raise QueueEmpty("Listener is closed") - return await self._queue.get() + message = await self._queue.get() + return message async def put_message(self, message: QueueMessage) -> None: """Put a message into this listener's queue. @@ -79,7 +86,7 @@ def close(self) -> None: """Close the listener, preventing new messages.""" self._closed = True # Put a shutdown message to unblock any waiting consumers - with contextlib.suppress(asyncio.QueueFull): + with contextlib.suppress(QueueFull): self._queue.put_nowait(QueueMessage(type=MessageType.SHUTDOWN)) @property @@ -108,7 +115,7 @@ async def __aiter__(self) -> AsyncIterator[Any]: break else: yield message.data - except asyncio.QueueEmpty: + except QueueEmpty: break @@ -122,7 +129,7 @@ def __init__(self, max_listeners: int = 100): max_listeners: Maximum number of concurrent listeners """ self._listeners: dict[str, QueueListener] = {} - self._lock = asyncio.Lock() + self._lock = create_lock() self._max_listeners = max_listeners self._total_messages_received = 0 self._total_messages_delivered = 0 @@ -230,7 +237,7 @@ async def broadcast(self, message: Any) -> int: # Use put_nowait to avoid blocking listener._queue.put_nowait(queue_msg) delivered_count += 1 - except asyncio.QueueFull: + except QueueFull: logger.warning( "message_queue_listener_full", listener_id=listener_id, @@ -262,7 +269,7 @@ async def broadcast_error(self, error: Exception) -> None: for listener in self._listeners.values(): if not listener.is_closed: - with contextlib.suppress(asyncio.QueueFull): + with contextlib.suppress(QueueFull): listener._queue.put_nowait(queue_msg) logger.trace( @@ -278,7 +285,7 @@ async def broadcast_complete(self) -> None: for listener in self._listeners.values(): if not listener.is_closed: - with contextlib.suppress(asyncio.QueueFull): + with contextlib.suppress(QueueFull): listener._queue.put_nowait(queue_msg) logger.trace( @@ -293,7 +300,7 @@ async def broadcast_shutdown(self) -> None: for listener in self._listeners.values(): if not listener.is_closed: - with contextlib.suppress(asyncio.QueueFull): + with contextlib.suppress(QueueFull): listener._queue.put_nowait(queue_msg) logger.trace( diff --git a/ccproxy/plugins/claude_sdk/session_client.py b/ccproxy/plugins/claude_sdk/session_client.py index e26ad05f..60b197c9 100644 --- a/ccproxy/plugins/claude_sdk/session_client.py +++ b/ccproxy/plugins/claude_sdk/session_client.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import time from enum import Enum from typing import Any @@ -10,6 +9,23 @@ from claude_agent_sdk import ClaudeAgentOptions from pydantic import BaseModel +from ccproxy.core.async_runtime import ( + CancelledError as RuntimeCancelledError, +) +from ccproxy.core.async_runtime import ( + Task, + create_event, + create_lock, +) +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + loop_time as runtime_loop_time, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import create_managed_task from ccproxy.core.async_utils import patched_typing from ccproxy.core.logging import get_plugin_logger @@ -73,7 +89,7 @@ def __init__( # Session management self.status = SessionStatus.IDLE - self.lock = asyncio.Lock() # Prevent concurrent access + self.lock = create_lock() # Prevent concurrent access self.metrics = SessionMetrics(created_at=time.time(), last_used=time.time()) # Error handling @@ -82,17 +98,17 @@ def __init__( self.max_connection_attempts = 3 # Background connection task - self._connection_task: asyncio.Task[bool] | None = None + self._connection_task: Task[bool] | None = None # Active stream tracking - self.active_stream_task: asyncio.Task[None] | None = None + self.active_stream_task: Task[None] | None = None self.has_active_stream: bool = False self.active_stream_handle: Any = ( None # StreamHandle when using queue-based approach ) # Interrupt synchronization - self._interrupt_complete_event = asyncio.Event() + self._interrupt_complete_event = create_event() self._interrupt_complete_event.set() # Initially set (not interrupting) # Session reuse tracking @@ -142,7 +158,7 @@ async def connect(self) -> bool: error=str(e), exc_info=e, ) - except TimeoutError as e: + except RuntimeTimeoutError as e: self.status = SessionStatus.ERROR self.last_error = e self.metrics.error_count += 1 @@ -188,7 +204,7 @@ async def connect(self) -> bool: # This should never be reached, but mypy needs it return False - async def connect_background(self) -> asyncio.Task[bool]: + async def connect_background(self) -> Task[bool]: """Start connection in background without blocking. Returns: @@ -233,7 +249,7 @@ async def disconnect(self) -> None: try: await self.claude_client.disconnect() logger.debug("session_disconnected", session_id=self.session_id) - except TimeoutError as e: + except RuntimeTimeoutError as e: logger.warning( "session_disconnect_timeout", session_id=self.session_id, @@ -282,7 +298,7 @@ async def interrupt(self) -> None: ) # Set up a hard timeout for the entire interrupt operation - start_time = asyncio.get_event_loop().time() + start_time = runtime_loop_time() max_interrupt_time = 15.0 # Maximum 15 seconds for entire interrupt try: @@ -306,7 +322,7 @@ async def interrupt(self) -> None: ) # Clear the handle reference self.active_stream_handle = None - except asyncio.CancelledError as e: + except RuntimeCancelledError as e: logger.warning( "session_stream_handle_interrupt_cancelled", session_id=self.session_id, @@ -314,7 +330,7 @@ async def interrupt(self) -> None: exc_info=e, message="Stream handle interrupt was cancelled, continuing with SDK interrupt", ) - except TimeoutError as e: + except RuntimeTimeoutError as e: logger.warning( "session_stream_handle_interrupt_timeout", session_id=self.session_id, @@ -340,23 +356,23 @@ async def interrupt(self) -> None: try: # Call interrupt directly with timeout - avoid creating separate tasks - await asyncio.wait_for(self.claude_client.interrupt(), timeout=30.0) + await runtime_wait_for(self.claude_client.interrupt(), timeout=30.0) logger.debug( "session_interrupted_gracefully", session_id=self.session_id ) # Reset status after successful interrupt self.status = SessionStatus.DISCONNECTED - except TimeoutError: + except RuntimeTimeoutError: # Interrupt timed out logger.warning( "session_interrupt_sdk_timeout", session_id=self.session_id, message="SDK interrupt timed out after 30 seconds", ) - raise TimeoutError("Interrupt timed out") from None + raise RuntimeTimeoutError("Interrupt timed out") from None - except TimeoutError: + except RuntimeTimeoutError: logger.warning( "session_interrupt_timeout", session_id=self.session_id, @@ -366,7 +382,7 @@ async def interrupt(self) -> None: # Force disconnect if interrupt hangs await self._force_disconnect() - except asyncio.CancelledError as e: + except RuntimeCancelledError as e: logger.warning( "session_interrupt_cancelled", session_id=self.session_id, @@ -411,7 +427,7 @@ async def interrupt(self) -> None: ) finally: # Final safety check - ensure we don't hang forever - total_elapsed = asyncio.get_event_loop().time() - start_time + total_elapsed = runtime_loop_time() - start_time if total_elapsed > max_interrupt_time: logger.error( "session_interrupt_max_time_exceeded", @@ -443,11 +459,11 @@ async def _force_disconnect(self) -> None: # Try to drain any active stream first with timeout try: - await asyncio.wait_for( + await runtime_wait_for( self.drain_active_stream(), timeout=5.0, # 5 second timeout for draining in force disconnect ) - except TimeoutError: + except RuntimeTimeoutError: logger.warning( "session_force_drain_timeout", session_id=self.session_id, @@ -457,11 +473,11 @@ async def _force_disconnect(self) -> None: try: if self.claude_client: # Try to disconnect with timeout - await asyncio.wait_for( + await runtime_wait_for( self.claude_client.disconnect(), timeout=3.0, # 3 second timeout for disconnect ) - except TimeoutError as e: + except RuntimeTimeoutError as e: logger.warning( "session_force_disconnect_timeout", session_id=self.session_id, @@ -531,7 +547,7 @@ async def drain_active_stream(self) -> None: handle_id=self.active_stream_handle.handle_id, message="Stream drain timed out after 30 seconds", ) - except TimeoutError as e: + except RuntimeTimeoutError as e: logger.error( "session_stream_drain_timeout_via_handle", session_id=self.session_id, @@ -539,7 +555,7 @@ async def drain_active_stream(self) -> None: error=str(e), exc_info=e, ) - except asyncio.CancelledError as e: + except RuntimeCancelledError as e: logger.warning( "session_stream_drain_cancelled_via_handle", session_id=self.session_id, @@ -581,7 +597,7 @@ async def wait_for_interrupt_complete(self, timeout: float = 5.0) -> bool: True if interrupt completed within timeout, False if timed out """ try: - await asyncio.wait_for( + await runtime_wait_for( self._interrupt_complete_event.wait(), timeout=timeout ) logger.debug( @@ -590,7 +606,7 @@ async def wait_for_interrupt_complete(self, timeout: float = 5.0) -> bool: message="Interrupt completion event signaled", ) return True - except TimeoutError: + except RuntimeTimeoutError: logger.warning( "session_interrupt_wait_timeout", session_id=self.session_id, diff --git a/ccproxy/plugins/claude_sdk/session_pool.py b/ccproxy/plugins/claude_sdk/session_pool.py index 9224666b..d5855930 100644 --- a/ccproxy/plugins/claude_sdk/session_pool.py +++ b/ccproxy/plugins/claude_sdk/session_pool.py @@ -2,12 +2,30 @@ from __future__ import annotations -import asyncio import contextlib -from typing import TYPE_CHECKING, Any +from typing import Any from claude_agent_sdk import ClaudeAgentOptions +from ccproxy.core.async_runtime import ( + CancelledError as RuntimeCancelledError, +) +from ccproxy.core.async_runtime import ( + Task, + create_lock, +) +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + gather as runtime_gather, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import create_managed_task from ccproxy.core.errors import ClaudeProxyError, ServiceUnavailableError from ccproxy.core.logging import get_plugin_logger @@ -16,10 +34,6 @@ from .session_client import SessionClient, SessionStatus -if TYPE_CHECKING: - pass - - logger = get_plugin_logger() @@ -41,9 +55,9 @@ class SessionPool: def __init__(self, config: SessionPoolSettings | None = None): self.config = config or SessionPoolSettings() self.sessions: dict[str, SessionClient] = {} - self.cleanup_task: asyncio.Task[None] | None = None + self.cleanup_task: Task[None] | None = None self._shutdown = False - self._lock = asyncio.Lock() + self._lock = create_lock() async def start(self) -> None: """Start the session pool and cleanup task.""" @@ -69,7 +83,7 @@ async def stop(self) -> None: if self.cleanup_task: self.cleanup_task.cancel() - with contextlib.suppress(asyncio.CancelledError): + with contextlib.suppress(RuntimeCancelledError): await self.cleanup_task # Disconnect all active sessions @@ -79,7 +93,7 @@ async def stop(self) -> None: ] if disconnect_tasks: - await asyncio.gather(*disconnect_tasks, return_exceptions=True) + await runtime_gather(*disconnect_tasks, return_exceptions=True) self.sessions.clear() @@ -345,7 +359,7 @@ async def _handle_ongoing_timeout( old_handle_id=old_handle_id, note="Ongoing timeout stream was already completed", ) - except asyncio.CancelledError as e: + except RuntimeCancelledError as e: logger.warning( "session_pool_interrupt_ongoing_cancelled", session_id=session_id, @@ -354,7 +368,7 @@ async def _handle_ongoing_timeout( exc_info=e, note="Interrupt cancelled during ongoing timeout stream cleanup", ) - except TimeoutError as e: + except RuntimeTimeoutError as e: logger.warning( "session_pool_interrupt_ongoing_timeout", session_id=session_id, @@ -498,9 +512,9 @@ async def _cleanup_loop(self) -> None: """Background task to cleanup expired sessions.""" while not self._shutdown: try: - await asyncio.sleep(self.config.cleanup_interval) + await runtime_sleep(self.config.cleanup_interval) await self._cleanup_sessions() - except asyncio.CancelledError: + except RuntimeCancelledError: break except Exception as e: logger.error("session_cleanup_error", error=str(e), exc_info=e) @@ -537,14 +551,14 @@ async def _cleanup_sessions(self) -> None: # Try to interrupt stuck session before cleanup try: await session_client.interrupt() - except asyncio.CancelledError as e: + except RuntimeCancelledError as e: logger.warning( "session_stuck_interrupt_cancelled", session_id=session_id, error=str(e), exc_info=e, ) - except TimeoutError as e: + except RuntimeTimeoutError as e: logger.warning( "session_stuck_interrupt_timeout", session_id=session_id, @@ -594,19 +608,19 @@ async def interrupt_session(self, session_id: str) -> bool: try: # Interrupt the session with 30-second timeout (allows for longer SDK response times) - await asyncio.wait_for(session_client.interrupt(), timeout=30.0) + await runtime_wait_for(session_client.interrupt(), timeout=30.0) logger.debug("session_interrupted", session_id=session_id) # Remove the session to prevent reuse await self._remove_session(session_id) return True - except (TimeoutError, Exception) as e: + except (RuntimeTimeoutError, Exception) as e: logger.error( "session_interrupt_failed", session_id=session_id, error=str(e) - if not isinstance(e, TimeoutError) + if not isinstance(e, RuntimeTimeoutError) else "Timeout after 30s", ) # Always remove the session on failure @@ -635,14 +649,14 @@ async def interrupt_all_sessions(self) -> int: try: await session_client.interrupt() interrupted_count += 1 - except asyncio.CancelledError as e: + except RuntimeCancelledError as e: logger.warning( "session_interrupt_cancelled_during_all", session_id=session_id, error=str(e), exc_info=e, ) - except TimeoutError as e: + except RuntimeTimeoutError as e: logger.error( "session_interrupt_timeout_during_all", session_id=session_id, diff --git a/ccproxy/plugins/claude_sdk/stream_handle.py b/ccproxy/plugins/claude_sdk/stream_handle.py index 9ec23a02..0c572c85 100644 --- a/ccproxy/plugins/claude_sdk/stream_handle.py +++ b/ccproxy/plugins/claude_sdk/stream_handle.py @@ -2,12 +2,20 @@ from __future__ import annotations -import asyncio import time import uuid from collections.abc import AsyncIterator from typing import Any +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + create_lock, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import create_managed_task from ccproxy.core.logging import get_plugin_logger @@ -60,7 +68,7 @@ def __init__( # Worker management self._worker: StreamWorker | None = None - self._worker_lock = asyncio.Lock() + self._worker_lock = create_lock() self._listeners: dict[str, QueueListener] = {} self._created_at = time.time() self._first_listener_at: float | None = None @@ -74,49 +82,67 @@ def __init__( async def create_listener(self) -> AsyncIterator[Any]: """Create a new listener for this stream. - This method starts the worker on first listener and returns - an async iterator for consuming messages. - - Yields: - Messages from the stream + Ensures the worker exists, registers the listener before the worker + begins consuming messages, and then yields messages as they arrive. """ - # Start worker if needed - await self._ensure_worker_started() + async with self._worker_lock: + worker_was_created = False - if not self._worker: - raise RuntimeError("Failed to start stream worker") + if self._worker is None: + worker_id = f"{self.handle_id}-worker" + self._worker = StreamWorker( + worker_id=worker_id, + message_iterator=self._message_iterator, + session_id=self.session_id, + request_id=self.request_id, + session_client=self._session_client, + stream_handle=self, + ) + worker_was_created = True - # Create listener - queue = self._worker.get_message_queue() - listener = await queue.create_listener() - self._listeners[listener.listener_id] = listener + worker = self._worker + if worker is None: + raise RuntimeError("Failed to initialize stream worker") - if self._first_listener_at is None: - self._first_listener_at = time.time() + queue = worker.get_message_queue() + listener = await queue.create_listener() + self._listeners[listener.listener_id] = listener + + if self._first_listener_at is None: + self._first_listener_at = time.time() + + start_worker = worker.status == WorkerStatus.IDLE + + if worker_was_created: + logger.debug( + "stream_handle_worker_created", + handle_id=self.handle_id, + worker_id=worker.worker_id, + session_id=self.session_id, + category="streaming", + ) + + if start_worker: + await worker.start() logger.debug( "stream_handle_listener_created", handle_id=self.handle_id, listener_id=listener.listener_id, total_listeners=len(self._listeners), - worker_status=self._worker.status.value, + worker_status=worker.status.value, category="streaming", ) try: - # Yield messages from listener async for message in listener: yield message - except GeneratorExit: - # Client disconnected logger.debug( "stream_handle_listener_disconnected", handle_id=self.handle_id, listener_id=listener.listener_id, ) - - # Check if this will be the last listener after removal remaining_listeners = len(self._listeners) - 1 if remaining_listeners == 0 and self._session_client: logger.debug( @@ -125,42 +151,10 @@ async def create_listener(self) -> AsyncIterator[Any]: listener_id=listener.listener_id, message="Last listener disconnected, will trigger SDK interrupt in cleanup", ) - - raise - finally: - # Remove listener await self._remove_listener(listener.listener_id) - - # Check if we should trigger cleanup await self._check_cleanup() - async def _ensure_worker_started(self) -> None: - """Ensure the worker is started, creating it if needed.""" - async with self._worker_lock: - if self._worker is None: - # Create worker - worker_id = f"{self.handle_id}-worker" - self._worker = StreamWorker( - worker_id=worker_id, - message_iterator=self._message_iterator, - session_id=self.session_id, - request_id=self.request_id, - session_client=self._session_client, - stream_handle=self, # Pass self for message tracking - ) - - # Start worker - await self._worker.start() - - logger.debug( - "stream_handle_worker_created", - handle_id=self.handle_id, - worker_id=worker_id, - session_id=self.session_id, - category="streaming", - ) - async def _remove_listener(self, listener_id: str) -> None: """Remove a listener and clean it up. @@ -291,7 +285,7 @@ async def _safe_interrupt_with_timeout(self) -> None: message="Calling SDK interrupt to gracefully stop stream", ) - await asyncio.wait_for( + await runtime_wait_for( self._session_client.interrupt(), timeout=self._interrupt_timeout, # Configurable timeout for stream handle initiated interrupts ) @@ -318,7 +312,7 @@ async def _safe_interrupt_with_timeout(self) -> None: message="Worker stop failed but continuing", ) - except TimeoutError: + except RuntimeTimeoutError: logger.warning( "stream_handle_interrupt_timeout", handle_id=self.handle_id, diff --git a/ccproxy/plugins/claude_sdk/stream_worker.py b/ccproxy/plugins/claude_sdk/stream_worker.py index bb8b1c50..ecbdc47d 100644 --- a/ccproxy/plugins/claude_sdk/stream_worker.py +++ b/ccproxy/plugins/claude_sdk/stream_worker.py @@ -2,12 +2,27 @@ from __future__ import annotations -import asyncio import time from collections.abc import AsyncIterator from enum import Enum from typing import TYPE_CHECKING, Any +from ccproxy.core.async_runtime import ( + ALL_COMPLETED, + Task, +) +from ccproxy.core.async_runtime import ( + CancelledError as RuntimeCancelledError, +) +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + wait as runtime_wait, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import create_managed_task from ccproxy.core.logging import get_plugin_logger @@ -67,7 +82,7 @@ def __init__( # Worker state self.status = WorkerStatus.IDLE self._message_queue = MessageQueue() - self._worker_task: asyncio.Task[None] | None = None + self._worker_task: Task[None] | None = None self._started_at: float | None = None self._completed_at: float | None = None @@ -121,11 +136,10 @@ async def stop(self, timeout: float = 5.0) -> None: self._worker_task.cancel() try: - # Use asyncio.wait instead of wait_for to handle cancelled tasks properly - done, pending = await asyncio.wait( - [self._worker_task], + done, pending = await runtime_wait( + {self._worker_task}, timeout=timeout, - return_when=asyncio.ALL_COMPLETED, + return_when=ALL_COMPLETED, ) if pending: @@ -164,11 +178,11 @@ async def wait_for_completion(self, timeout: float | None = None) -> bool: try: if timeout: - await asyncio.wait_for(self._worker_task, timeout=timeout) + await runtime_wait_for(self._worker_task, timeout=timeout) else: await self._worker_task return True - except TimeoutError: + except RuntimeTimeoutError: return False def get_message_queue(self) -> MessageQueue: @@ -255,7 +269,7 @@ async def _run_worker(self) -> None: duration_seconds=time.time() - (self._started_at or 0), ) - except asyncio.CancelledError: + except RuntimeCancelledError: # Worker was cancelled self.status = WorkerStatus.INTERRUPTED logger.debug( diff --git a/ccproxy/plugins/claude_sdk/streaming.py b/ccproxy/plugins/claude_sdk/streaming.py index 0ad5420d..c9ad9fd5 100644 --- a/ccproxy/plugins/claude_sdk/streaming.py +++ b/ccproxy/plugins/claude_sdk/streaming.py @@ -4,6 +4,7 @@ from typing import Any from uuid import uuid4 +from ccproxy.core.async_runtime import create_task from ccproxy.core.logging import get_plugin_logger from ccproxy.core.request_context import RequestContext @@ -305,9 +306,7 @@ async def process_stream( } # Emit the hook asynchronously - import asyncio - - asyncio.create_task( + create_task( self.streaming_hook.emit_stream_end( request_id=str(request_id or ""), usage_metrics=usage_metrics, diff --git a/ccproxy/plugins/codex/detection_service.py b/ccproxy/plugins/codex/detection_service.py index ffcc5b34..5a1addce 100644 --- a/ccproxy/plugins/codex/detection_service.py +++ b/ccproxy/plugins/codex/detection_service.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import json import os import socket @@ -15,6 +14,24 @@ from ccproxy.config.settings import Settings from ccproxy.config.utils import get_ccproxy_cache_dir +from ccproxy.core.async_runtime import ( + FIRST_COMPLETED, + PIPE, + CancelledError, + create_event, +) +from ccproxy.core.async_runtime import ( + create_subprocess_exec as runtime_create_subprocess_exec, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + wait as runtime_wait, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.logging import get_plugin_logger from ccproxy.models.detection import DetectedHeaders, DetectedPrompts from ccproxy.services.cli_detection import CLIDetectionService @@ -313,7 +330,7 @@ async def capture_handler(request: Request) -> Response: config = Config(temp_app, host="127.0.0.1", port=port, log_level="error") server = Server(config) - server_ready = asyncio.Event() + server_ready = create_event() @temp_app.on_event("startup") async def signal_server_ready() -> None: @@ -322,14 +339,14 @@ async def signal_server_ready() -> None: server_ready.set() logger.debug("start", category="plugin") - server_task = asyncio.create_task(server.serve()) - ready_task = asyncio.create_task(server_ready.wait()) + server_task = runtime_create_task(server.serve()) + ready_task = runtime_create_task(server_ready.wait()) try: - done, _pending = await asyncio.wait( + done, _pending = await runtime_wait( {ready_task, server_task}, timeout=5, - return_when=asyncio.FIRST_COMPLETED, + return_when=FIRST_COMPLETED, ) if ready_task in done: await ready_task @@ -389,15 +406,15 @@ async def signal_server_ready() -> None: "test", ] - process = await asyncio.create_subprocess_exec( + process = await runtime_create_subprocess_exec( *cmd, env=env, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=PIPE, + stderr=PIPE, ) # Wait for process with timeout try: - await asyncio.wait_for(process.wait(), timeout=300) + await runtime_wait_for(process.wait(), timeout=300) except TimeoutError: process.kill() await process.wait() @@ -413,7 +430,7 @@ async def signal_server_ready() -> None: finally: if not ready_task.done(): ready_task.cancel() - with suppress(asyncio.CancelledError): + with suppress(CancelledError): await ready_task server.should_exit = True diff --git a/ccproxy/plugins/copilot/detection_service.py b/ccproxy/plugins/copilot/detection_service.py index 79f1e171..1b1a2aef 100644 --- a/ccproxy/plugins/copilot/detection_service.py +++ b/ccproxy/plugins/copilot/detection_service.py @@ -1,11 +1,16 @@ """GitHub CLI detection service for Copilot plugin.""" -import asyncio import shutil from datetime import datetime, timedelta from typing import TYPE_CHECKING, Any, cast from ccproxy.config.settings import Settings +from ccproxy.core.async_runtime import ( + PIPE, +) +from ccproxy.core.async_runtime import ( + create_subprocess_exec as runtime_create_subprocess_exec, +) from ccproxy.core.logging import get_plugin_logger from .models import CopilotCacheData, CopilotCliInfo @@ -59,11 +64,11 @@ async def initialize_detection(self) -> CopilotCacheData: if cli_available and cli_path: try: # Get CLI version - version_result = await asyncio.create_subprocess_exec( + version_result = await runtime_create_subprocess_exec( *cli_path, "--version", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=PIPE, + stderr=PIPE, ) stdout, stderr = await version_result.communicate() @@ -78,12 +83,12 @@ async def initialize_detection(self) -> CopilotCacheData: break # Check authentication status - auth_result = await asyncio.create_subprocess_exec( + auth_result = await runtime_create_subprocess_exec( *cli_path, "auth", "status", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=PIPE, + stderr=PIPE, ) stdout, stderr = await auth_result.communicate() diff --git a/ccproxy/plugins/copilot/oauth/client.py b/ccproxy/plugins/copilot/oauth/client.py index c2e0d4f6..69d522bd 100644 --- a/ccproxy/plugins/copilot/oauth/client.py +++ b/ccproxy/plugins/copilot/oauth/client.py @@ -1,6 +1,5 @@ """OAuth client implementation for GitHub Copilot with Device Code Flow.""" -import asyncio import time from typing import TYPE_CHECKING, Any @@ -8,6 +7,7 @@ from pydantic import SecretStr from ccproxy.auth.oauth.protocol import StandardProfileFields +from ccproxy.core.async_runtime import sleep as runtime_sleep from ccproxy.core.logging import get_plugin_logger from ..config import CopilotOAuthConfig @@ -160,7 +160,7 @@ async def poll_for_token( if time.time() - start_time > expires_in: raise TimeoutError("Device code has expired") - await asyncio.sleep(current_interval) + await runtime_sleep(current_interval) data = { "client_id": self.config.client_id, @@ -233,7 +233,7 @@ async def poll_for_token( exc_info=e, ) # Continue polling on HTTP errors - await asyncio.sleep(current_interval) + await runtime_sleep(current_interval) continue async def exchange_for_copilot_token( diff --git a/ccproxy/plugins/credential_balancer/manager.py b/ccproxy/plugins/credential_balancer/manager.py index 0b9414e9..97e3c0ce 100644 --- a/ccproxy/plugins/credential_balancer/manager.py +++ b/ccproxy/plugins/credential_balancer/manager.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import time import uuid from dataclasses import dataclass, field @@ -15,6 +14,7 @@ from ccproxy.auth.managers.token_snapshot import TokenSnapshot from ccproxy.auth.models.credentials import BaseCredentials from ccproxy.auth.oauth.protocol import StandardProfileFields +from ccproxy.core.async_runtime import create_lock from ccproxy.core.logging import TraceBoundLogger, get_plugin_logger from ccproxy.core.request_context import RequestContext @@ -41,7 +41,10 @@ class CredentialEntry: logger: TraceBoundLogger _failure_count: int = 0 _disabled_until: float | None = None - _lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False) + _lock: Any = field(init=False) + + def __post_init__(self) -> None: + self._lock = create_lock() @property def label(self) -> str: @@ -174,8 +177,8 @@ def __init__( self._entries = entries self._strategy = config.strategy self._failure_codes = set(config.failure_status_codes) - self._lock = asyncio.Lock() - self._state_lock = asyncio.Lock() + self._lock = create_lock() + self._state_lock = create_lock() self._request_states: dict[str, _RequestState] = {} self._active_index = 0 self._next_index = 0 diff --git a/ccproxy/plugins/docker/adapter.py b/ccproxy/plugins/docker/adapter.py index 243dceb1..9baecade 100644 --- a/ccproxy/plugins/docker/adapter.py +++ b/ccproxy/plugins/docker/adapter.py @@ -1,6 +1,5 @@ """Docker adapter for container operations.""" -import asyncio import os import shlex import subprocess @@ -10,6 +9,9 @@ from fastapi import Request from starlette.responses import Response, StreamingResponse +from ccproxy.core.async_runtime import ( + create_subprocess_exec as runtime_create_subprocess_exec, +) from ccproxy.core.logging import get_plugin_logger from ccproxy.services.adapters.base import BaseAdapter from ccproxy.streaming import DeferredStreaming @@ -49,11 +51,11 @@ def __init__(self, config: DockerConfig | None = None): async def _needs_sudo(self) -> bool: """Check if Docker requires sudo by testing docker info command.""" try: - process = await asyncio.create_subprocess_exec( + process = await runtime_create_subprocess_exec( "docker", "info", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) _, stderr = await process.communicate() if process.returncode == 0: @@ -74,10 +76,10 @@ async def is_available(self) -> bool: cmd_str = " ".join(docker_cmd) try: - process = await asyncio.create_subprocess_exec( + process = await runtime_create_subprocess_exec( *docker_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) stdout, stderr = await process.communicate() @@ -457,10 +459,10 @@ async def image_exists(self, image_name: str, image_tag: str = "latest") -> bool try: # Run Docker inspect command - process = await asyncio.create_subprocess_exec( + process = await runtime_create_subprocess_exec( *docker_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) _, stderr = await process.communicate() @@ -481,10 +483,10 @@ async def image_exists(self, image_name: str, image_tag: str = "latest") -> bool try: logger.debug("docker_image_check_permission_denied_using_sudo") sudo_cmd = ["sudo"] + docker_cmd - sudo_process = await asyncio.create_subprocess_exec( + sudo_process = await runtime_create_subprocess_exec( *sudo_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) await sudo_process.communicate() if sudo_process.returncode == 0: diff --git a/ccproxy/plugins/docker/stream_process.py b/ccproxy/plugins/docker/stream_process.py index 8f2c26b4..118a7fa9 100644 --- a/ccproxy/plugins/docker/stream_process.py +++ b/ccproxy/plugins/docker/stream_process.py @@ -22,9 +22,29 @@ async def process(self, line: str, stream_type: str) -> str: ``` """ -import asyncio import shlex -from typing import Any, Generic, TypeAlias, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeAlias, TypeVar, cast + +from ccproxy.core.async_runtime import ( + PIPE, +) +from ccproxy.core.async_runtime import ( + create_subprocess_exec as runtime_create_subprocess_exec, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) + + +if TYPE_CHECKING: + + class _ReadableStream(Protocol): + async def readline(self) -> bytes: + """Read a single line of bytes from the stream.""" + + StreamReader = _ReadableStream +else: # pragma: no cover - runtime uses concrete implementation from backend + StreamReader = Any T = TypeVar("T") # Type of processed output @@ -220,13 +240,13 @@ async def process(self, line: str, stream_type: str) -> str: cmd = shlex.split(cmd) # Start the async process with pipes for stdout and stderr - process = await asyncio.create_subprocess_exec( + process = await runtime_create_subprocess_exec( *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=PIPE, + stderr=PIPE, ) - async def stream_output(stream: asyncio.StreamReader, stream_type: str) -> list[T]: + async def stream_output(stream: "StreamReader", stream_type: str) -> list[T]: """Process output from a stream and capture results. Args: @@ -253,8 +273,8 @@ async def stream_output(stream: asyncio.StreamReader, stream_type: str) -> list[ if process.stdout is None or process.stderr is None: raise RuntimeError("Process stdout or stderr is None") - stdout_task = asyncio.create_task(stream_output(process.stdout, "stdout")) - stderr_task = asyncio.create_task(stream_output(process.stderr, "stderr")) + stdout_task = runtime_create_task(stream_output(process.stdout, "stdout")) + stderr_task = runtime_create_task(stream_output(process.stderr, "stderr")) # Wait for process to complete and collect output return_code = await process.wait() diff --git a/ccproxy/plugins/duckdb_storage/storage.py b/ccproxy/plugins/duckdb_storage/storage.py index 1dcd4b1a..64e023b4 100644 --- a/ccproxy/plugins/duckdb_storage/storage.py +++ b/ccproxy/plugins/duckdb_storage/storage.py @@ -7,8 +7,6 @@ from __future__ import annotations -import asyncio -import contextlib import time from collections.abc import Mapping, Sequence from datetime import datetime @@ -21,8 +19,9 @@ from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError from sqlmodel import Session, SQLModel, create_engine, func -from ccproxy.core.async_task_manager import create_managed_task +from ccproxy.core.async_runtime import to_thread as runtime_to_thread from ccproxy.core.logging import get_plugin_logger +from ccproxy.plugins.analytics.models import AccessLog, AccessLogPayload logger = get_plugin_logger(__name__) @@ -40,11 +39,6 @@ def __init__(self, database_path: str | Path = "data/metrics.duckdb"): self.database_path = Path(database_path) self._engine: Engine | None = None self._initialized: bool = False - self._write_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue() - self._background_worker_task: asyncio.Task[None] | None = None - self._shutdown_event = asyncio.Event() - # Sentinel to wake the background worker immediately on shutdown - self._sentinel: object = object() async def initialize(self) -> None: """Initialize the storage backend.""" @@ -61,13 +55,6 @@ async def initialize(self) -> None: # Create schema using SQLModel (synchronous in main thread) self._create_schema_sync() - # Start background worker for queue processing - self._background_worker_task = await create_managed_task( - self._background_worker(), - name="duckdb_background_worker", - creator="SimpleDuckDBStorage", - ) - self._initialized = True logger.debug( "simple_duckdb_initialized", database_path=str(self.database_path) @@ -148,124 +135,19 @@ async def store_request(self, data: Mapping[str, Any]) -> bool: return False try: - # Add to queue for background processing - await self._write_queue.put(dict(data)) - return True - except asyncio.QueueFull as e: - logger.error( - "queue_store_full_error", - error=str(e), - request_id=data.get("request_id"), - exc_info=e, - ) - return False + payload = dict(data) + if str(self.database_path) == ":memory:": + return self._store_request_sync(payload) + return await runtime_to_thread(self._store_request_sync, payload) except Exception as e: logger.error( - "queue_store_error", + "simple_duckdb_store_async_error", error=str(e), request_id=data.get("request_id"), exc_info=e, ) return False - async def _background_worker(self) -> None: - """Background worker to process queued write operations sequentially.""" - logger.debug("duckdb_background_worker_started") - - while not self._shutdown_event.is_set(): - try: - # Wait for either a queue item or shutdown with timeout - try: - data = await asyncio.wait_for(self._write_queue.get(), timeout=1.0) - except TimeoutError: - continue # Check shutdown event and continue - - # We successfully got an item, so we need to mark it done - try: - # If we receive a sentinel item, break out quickly on shutdown - if data is self._sentinel: - self._write_queue.task_done() - break - success = self._store_request_sync(data) - if success: - logger.debug( - "queue_processed_successfully", - request_id=data.get("request_id"), - ) - except SQLAlchemyError as e: - logger.error( - "background_worker_db_error", - error=str(e), - request_id=data.get("request_id"), - exc_info=e, - ) - except Exception as e: - logger.error( - "background_worker_error", - error=str(e), - request_id=data.get("request_id"), - exc_info=e, - ) - - # Always mark the task as done for regular items, regardless of success/failure - if data is not self._sentinel: - self._write_queue.task_done() - - except asyncio.CancelledError as e: - logger.info("background_worker_cancelled", exc_info=e) - break - except Exception as e: - logger.error( - "background_worker_unexpected_error", - error=str(e), - exc_info=e, - ) - # Continue processing other items - - # Process any remaining items in the queue during shutdown - logger.debug("processing_remaining_queue_items_on_shutdown") - while not self._write_queue.empty(): - try: - # Get remaining items without timeout during shutdown - data = self._write_queue.get_nowait() - - # Process the queued write operation synchronously - try: - success = self._store_request_sync(data) - if success: - logger.debug( - "shutdown_queue_processed_successfully", - request_id=data.get("request_id"), - ) - except SQLAlchemyError as e: - logger.error( - "shutdown_background_worker_db_error", - error=str(e), - request_id=data.get("request_id"), - exc_info=e, - ) - except Exception as e: - logger.error( - "shutdown_background_worker_error", - error=str(e), - request_id=data.get("request_id"), - exc_info=e, - ) - # Note: No task_done() call needed for get_nowait() items - - except asyncio.QueueEmpty: - # No more items to process - break - except Exception as e: - logger.error( - "shutdown_background_worker_unexpected_error", - error=str(e), - exc_info=e, - ) - # Continue processing other items - - logger.debug("duckdb_background_worker_stopped") - def _store_request_sync(self, data: dict[str, Any]) -> bool: """Synchronous version of store_request for thread pool execution.""" try: @@ -477,36 +359,7 @@ async def store(self, metric: dict[str, Any]) -> bool: return await self.store_batch([metric]) async def close(self) -> None: - """Close the database connection and stop background worker.""" - # Signal shutdown to background worker - self._shutdown_event.set() - - # Wake up background worker immediately if it's waiting on queue.get() - with contextlib.suppress(Exception): - self._write_queue.put_nowait(self._sentinel) # type: ignore[arg-type] - - # Wait for background worker to finish - if self._background_worker_task: - try: - await asyncio.wait_for(self._background_worker_task, timeout=5.0) - except TimeoutError: - logger.warning("background_worker_shutdown_timeout") - self._background_worker_task.cancel() - except asyncio.CancelledError: - logger.info("background_worker_shutdown_cancelled") - except Exception as e: - logger.error( - "background_worker_shutdown_error", error=str(e), exc_info=e - ) - - # Process remaining items in queue (with timeout) - try: - await asyncio.wait_for(self._write_queue.join(), timeout=2.0) - except TimeoutError: - logger.warning( - "queue_drain_timeout", remaining_items=self._write_queue.qsize() - ) - + """Close the database connection.""" if self._engine: try: self._engine.dispose() @@ -538,7 +391,7 @@ async def health_check(self) -> dict[str, Any]: try: if self._engine: # Run the synchronous database operation in a thread pool - access_log_count = await asyncio.to_thread(self._health_check_sync) + access_log_count = await runtime_to_thread(self._health_check_sync) return { "status": "healthy", @@ -588,7 +441,7 @@ async def reset_data(self) -> bool: try: # Run the reset operation in a thread pool - return await asyncio.to_thread(self._reset_data_sync) + return await runtime_to_thread(self._reset_data_sync) except SQLAlchemyError as e: logger.error("simple_duckdb_reset_db_error", error=str(e), exc_info=e) return False @@ -625,9 +478,6 @@ async def wait_for_queue_processing(self, timeout: float = 5.0) -> None: timeout: Maximum time to wait in seconds Raises: - asyncio.TimeoutError: If processing doesn't complete within timeout + RuntimeTimeoutError: If processing doesn't complete within timeout """ - if not self._initialized or self._shutdown_event.is_set(): - return - - await asyncio.wait_for(self._write_queue.join(), timeout=timeout) + return None diff --git a/ccproxy/plugins/oauth_claude/storage.py b/ccproxy/plugins/oauth_claude/storage.py index 14c7ab7f..7352d6fb 100644 --- a/ccproxy/plugins/oauth_claude/storage.py +++ b/ccproxy/plugins/oauth_claude/storage.py @@ -1,12 +1,12 @@ """Token storage for Claude OAuth plugin.""" -import asyncio import json import tempfile from pathlib import Path from typing import Any, cast from ccproxy.auth.storage.base import BaseJsonStorage +from ccproxy.core.async_runtime import to_thread as runtime_to_thread from ccproxy.core.logging import get_plugin_logger from .models import ClaudeCredentials, ClaudeProfileInfo @@ -132,7 +132,7 @@ def write_file() -> None: # Atomic rename tmp_path.replace(self.file_path) - await asyncio.to_thread(write_file) + await runtime_to_thread(write_file) async def _read_json(self) -> dict[str, Any] | None: """Read JSON data from file. @@ -147,7 +147,7 @@ def read_file() -> dict[str, Any]: with self.file_path.open("r") as f: return cast(dict[str, Any], json.load(f)) - return cast(dict[str, Any], await asyncio.to_thread(read_file)) + return cast(dict[str, Any], await runtime_to_thread(read_file)) async def save_profile(self, profile_data: dict[str, Any]) -> bool: """Save Claude profile data. diff --git a/ccproxy/plugins/permissions/handlers/cli.py b/ccproxy/plugins/permissions/handlers/cli.py index 71c7219a..774c1136 100644 --- a/ccproxy/plugins/permissions/handlers/cli.py +++ b/ccproxy/plugins/permissions/handlers/cli.py @@ -1,11 +1,12 @@ """CLI command for handling confirmation requests via SSE stream.""" -import asyncio +from __future__ import annotations + import contextlib import json import logging from collections.abc import AsyncIterator -from typing import Any +from typing import TYPE_CHECKING, Any import httpx import typer @@ -13,6 +14,18 @@ import ccproxy.core.logging from ccproxy.config.settings import Settings +from ccproxy.core.async_runtime import ( + CancelledError, +) +from ccproxy.core.async_runtime import ( + run as runtime_run, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import create_managed_task from ccproxy.core.logging import get_plugin_logger @@ -21,6 +34,10 @@ from .terminal import TerminalPermissionHandler as TextualPermissionHandler +if TYPE_CHECKING: + from asyncio import Task + + logger = get_plugin_logger() app = typer.Typer( @@ -51,11 +68,11 @@ def __init__( self.auth_token = auth_token self.auto_reconnect = auto_reconnect - self._ongoing_requests: dict[str, asyncio.Task[bool]] = {} + self._ongoing_requests: dict[str, Task[bool]] = {} self._resolved_requests: dict[str, tuple[bool, str]] = {} self._resolved_by_us: set[str] = set() - async def __aenter__(self) -> "SSEConfirmationHandler": + async def __aenter__(self) -> SSEConfirmationHandler: """Async context manager entry.""" headers = {} if self.auth_token: @@ -198,8 +215,8 @@ async def _handle_permission_resolved(self, data: dict[str, Any]) -> None: task.cancel() - with contextlib.suppress(TimeoutError, asyncio.CancelledError): - await asyncio.wait_for(task, timeout=0.1) + with contextlib.suppress(TimeoutError, CancelledError): + await runtime_wait_for(task, timeout=0.1) logger.info( "permission_cancelled_by_other_handler", @@ -236,11 +253,11 @@ async def _handle_permission_with_cancellation( await self.send_response(request.id, allowed) - await asyncio.sleep(0.5) + await runtime_sleep(0.5) return allowed - except asyncio.CancelledError: + except CancelledError: logger.info( "permission_cancelled", request_id=request.id, @@ -383,7 +400,7 @@ async def run(self) -> None: # Reset retry count and reconnect retry_count = 0 print("Connection closed. Reconnecting...") - await asyncio.sleep(1.0) # Brief pause before reconnecting + await runtime_sleep(1.0) # Brief pause before reconnecting continue else: print("Connection closed. Exiting (auto-reconnect disabled).") @@ -421,7 +438,7 @@ async def run(self) -> None: f"Connection failed (attempt {retry_count}/{self.max_retries}). Retrying in {delay}s..." ) - await asyncio.sleep(delay) + await runtime_sleep(delay) continue except Exception as e: @@ -565,7 +582,7 @@ async def run_handler() -> None: # Run the async handler try: - asyncio.run(run_handler()) + runtime_run(run_handler()) except KeyboardInterrupt: logger.info("permission_handler_stopped") except Exception as e: diff --git a/ccproxy/plugins/permissions/handlers/terminal.py b/ccproxy/plugins/permissions/handlers/terminal.py index 53cdefc1..c60a9d04 100644 --- a/ccproxy/plugins/permissions/handlers/terminal.py +++ b/ccproxy/plugins/permissions/handlers/terminal.py @@ -2,12 +2,25 @@ from __future__ import annotations -import asyncio import contextlib import time from dataclasses import dataclass -from typing import TYPE_CHECKING - +from typing import TYPE_CHECKING, cast + +from ccproxy.core.async_runtime import ( + CancelledError, + Future, + Queue, + Task, + create_future, + create_queue, +) +from ccproxy.core.async_runtime import ( + InvalidStateError as RuntimeInvalidStateError, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) from ccproxy.core.async_task_manager import ( create_fire_and_forget_task, create_managed_task, @@ -77,7 +90,7 @@ class PendingRequest: """Represents a pending confirmation request with its response future.""" request: PermissionRequest - future: asyncio.Future[bool] + future: Future[bool] cancelled: bool = False @@ -181,7 +194,7 @@ async def _show_result(self, allowed: bool, message: str) -> None: dialog.styles.border = ("solid", "red") # Give user time to see the result - await asyncio.sleep(1.5) + await runtime_sleep(1.5) self.dismiss(allowed) def action_confirm(self) -> None: @@ -362,7 +375,7 @@ async def _show_result(self, allowed: bool, message: str) -> None: dialog.styles.border = ("solid", "red") # Give user time to see the result - await asyncio.sleep(1.5) + await runtime_sleep(1.5) self.exit(allowed) def action_confirm(self) -> None: @@ -404,24 +417,22 @@ class TerminalPermissionHandler: def __init__(self) -> None: """Initialize the terminal confirmation handler.""" - self._request_queue: ( - asyncio.Queue[tuple[PermissionRequest, asyncio.Future[bool]]] | None - ) = None + self._request_queue: Queue[tuple[PermissionRequest, Future[bool]]] | None = None self._cancelled_requests: set[str] = set() - self._processing_task: asyncio.Task[None] | None = None + self._processing_task: Task[None] | None = None self._active_apps: dict[str, ConfirmationApp] = {} def _get_request_queue( self, - ) -> asyncio.Queue[tuple[PermissionRequest, asyncio.Future[bool]]]: + ) -> Queue[tuple[PermissionRequest, Future[bool]]]: """Lazily initialize and return the request queue.""" if self._request_queue is None: - self._request_queue = asyncio.Queue() + self._request_queue = cast( + Queue[tuple[PermissionRequest, Future[bool]]], create_queue() + ) return self._request_queue - def _safe_set_future_result( - self, future: asyncio.Future[bool], result: bool - ) -> bool: + def _safe_set_future_result(self, future: Future[bool], result: bool) -> bool: """Safely set a future result, handling already cancelled futures. Args: @@ -436,12 +447,12 @@ def _safe_set_future_result( try: future.set_result(result) return True - except asyncio.InvalidStateError: + except RuntimeInvalidStateError: # Future was already resolved or cancelled return False def _safe_set_future_exception( - self, future: asyncio.Future[bool], exception: BaseException + self, future: Future[bool], exception: BaseException ) -> bool: """Safely set a future exception, handling already cancelled futures. @@ -457,7 +468,7 @@ def _safe_set_future_exception( try: future.set_exception(exception) return True - except asyncio.InvalidStateError: + except RuntimeInvalidStateError: # Future was already resolved or cancelled return False @@ -474,13 +485,13 @@ async def _process_queue(self) -> None: # Process the request await self._process_single_request(request, future) - except asyncio.CancelledError: + except CancelledError: break except Exception as e: logger.error("queue_processing_error", error=str(e), exc_info=e) def _is_request_processable( - self, request: PermissionRequest, future: asyncio.Future[bool] + self, request: PermissionRequest, future: Future[bool] ) -> bool: """Check if a request can be processed.""" # Check if cancelled before processing @@ -497,7 +508,7 @@ def _is_request_processable( return True async def _process_single_request( - self, request: PermissionRequest, future: asyncio.Future[bool] + self, request: PermissionRequest, future: Future[bool] ) -> None: """Process a single permission request.""" app = None @@ -558,7 +569,7 @@ async def _create_processing_task(self) -> None: async def _queue_and_wait_for_result(self, request: PermissionRequest) -> bool: """Queue a request and wait for its result.""" - future: asyncio.Future[bool] = asyncio.Future() + future: Future[bool] = create_future() await self._get_request_queue().put((request, future)) return await future @@ -669,7 +680,7 @@ async def shutdown(self) -> None: """Shutdown the handler and cleanup resources.""" if self._processing_task and not self._processing_task.done(): self._processing_task.cancel() - with contextlib.suppress(asyncio.CancelledError): + with contextlib.suppress(CancelledError): await self._processing_task self._processing_task = None diff --git a/ccproxy/plugins/permissions/models.py b/ccproxy/plugins/permissions/models.py index eab4ce74..6e347147 100644 --- a/ccproxy/plugins/permissions/models.py +++ b/ccproxy/plugins/permissions/models.py @@ -1,6 +1,5 @@ """Pydantic models for permission system.""" -import asyncio import uuid from datetime import UTC, datetime from enum import Enum @@ -8,6 +7,8 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr +from ccproxy.core.async_runtime import create_event + class PermissionStatus(Enum): """Status of a permission request.""" @@ -48,7 +49,7 @@ class PermissionRequest(BaseModel): resolved_at: datetime | None = None # Private attribute for event-driven waiting - _resolved_event: asyncio.Event = PrivateAttr(default_factory=asyncio.Event) + _resolved_event: Any = PrivateAttr(default_factory=create_event) def is_expired(self) -> bool: """Check if the request has expired.""" diff --git a/ccproxy/plugins/permissions/routes.py b/ccproxy/plugins/permissions/routes.py index b7049a82..bd8d2805 100644 --- a/ccproxy/plugins/permissions/routes.py +++ b/ccproxy/plugins/permissions/routes.py @@ -1,6 +1,5 @@ """API routes for permission request handling via SSE and REST.""" -import asyncio import json from collections.abc import AsyncGenerator from typing import TYPE_CHECKING, Any @@ -14,6 +13,12 @@ from ccproxy.api.dependencies import OptionalSettingsDep from ccproxy.auth.dependencies import ConditionalAuthDep +from ccproxy.core.async_runtime import ( + CancelledError, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.errors import ( PermissionAlreadyResolvedError, PermissionNotFoundError, @@ -89,7 +94,7 @@ async def event_generator( while not await request.is_disconnected(): try: - event_data = await asyncio.wait_for(queue.get(), timeout=30.0) + event_data = await runtime_wait_for(queue.get(), timeout=30.0) yield { "event": event_data.get("type", "message"), @@ -102,7 +107,7 @@ async def event_generator( "data": json.dumps({"message": "keepalive"}), } - except asyncio.CancelledError: + except CancelledError: pass finally: await service.unsubscribe_from_events(queue) diff --git a/ccproxy/plugins/permissions/service.py b/ccproxy/plugins/permissions/service.py index bcd4d181..0a6e0b24 100644 --- a/ccproxy/plugins/permissions/service.py +++ b/ccproxy/plugins/permissions/service.py @@ -1,10 +1,26 @@ """Permission service for handling permission requests without UI dependencies.""" -import asyncio import contextlib from datetime import UTC, datetime, timedelta -from typing import TYPE_CHECKING, Any - +from typing import TYPE_CHECKING, Any, cast + +from ccproxy.core.async_runtime import ( + CancelledError, + Queue, + QueueFull, + Task, + create_lock, + create_queue, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import AsyncTaskManager, create_managed_task from ccproxy.core.errors import ( PermissionNotFoundError, @@ -32,10 +48,10 @@ class PermissionService: def __init__(self, timeout_seconds: int = 30): self._timeout_seconds = timeout_seconds self._requests: dict[str, PermissionRequest] = {} - self._expiry_task: asyncio.Task[None] | None = None + self._expiry_task: Task[None] | None = None self._shutdown = False - self._event_queues: list[asyncio.Queue[dict[str, Any]]] = [] - self._lock = asyncio.Lock() + self._event_queues: list[Queue[dict[str, Any]]] = [] + self._lock = create_lock() async def start( self, @@ -64,7 +80,7 @@ async def start( "permission_service_task_manager_unavailable", error=str(exc), ) - self._expiry_task = asyncio.create_task( + self._expiry_task = runtime_create_task( self._expiry_checker(), name="permission_expiry_checker" ) @@ -74,7 +90,7 @@ async def stop(self) -> None: self._shutdown = True if self._expiry_task: self._expiry_task.cancel() - with contextlib.suppress(asyncio.CancelledError): + with contextlib.suppress(CancelledError): await self._expiry_task self._expiry_task = None logger.debug("permission_service_stopped") @@ -212,7 +228,7 @@ async def resolve(self, request_id: str, allowed: bool) -> bool: async def _expiry_checker(self) -> None: while not self._shutdown: try: - await asyncio.sleep(self._get_expiry_poll_interval()) + await runtime_sleep(self._get_expiry_poll_interval()) now = datetime.now(UTC) expired_ids = [] @@ -247,7 +263,7 @@ async def _expiry_checker(self) -> None: count=len(expired_ids), ) - except asyncio.CancelledError: + except CancelledError: break except Exception as e: logger.error( @@ -294,20 +310,18 @@ def _should_fallback_to_unmanaged_task(exc: RuntimeError) -> bool: ) ) - async def subscribe_to_events(self) -> asyncio.Queue[dict[str, Any]]: + async def subscribe_to_events(self) -> Queue[dict[str, Any]]: """Subscribe to permission events. Returns: An async queue that will receive events """ - queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue() + queue: Queue[dict[str, Any]] = cast(Queue[dict[str, Any]], create_queue()) async with self._lock: self._event_queues.append(queue) return queue - async def unsubscribe_from_events( - self, queue: asyncio.Queue[dict[str, Any]] - ) -> None: + async def unsubscribe_from_events(self, queue: Queue[dict[str, Any]]) -> None: """Unsubscribe from permission events. Args: @@ -330,7 +344,7 @@ async def _emit_event(self, event: dict[str, Any]) -> None: return for queue in queues: - with contextlib.suppress(asyncio.QueueFull): + with contextlib.suppress(QueueFull): queue.put_nowait(event) async def get_pending_requests(self) -> list[PermissionRequest]: @@ -365,7 +379,7 @@ async def wait_for_permission( The final status of the permission request Raises: - asyncio.TimeoutError: If timeout is reached before resolution + TimeoutError: If timeout is reached before resolution PermissionNotFoundError: If request ID is not found """ async with self._lock: @@ -381,7 +395,7 @@ async def wait_for_permission( try: # Efficiently wait for the event to be set - await asyncio.wait_for( + await runtime_wait_for( request._resolved_event.wait(), timeout=timeout_seconds ) except TimeoutError as e: diff --git a/ccproxy/plugins/permissions/ui/terminal_permission_handler.py b/ccproxy/plugins/permissions/ui/terminal_permission_handler.py index 9ed7214a..7dd7bf92 100644 --- a/ccproxy/plugins/permissions/ui/terminal_permission_handler.py +++ b/ccproxy/plugins/permissions/ui/terminal_permission_handler.py @@ -2,12 +2,32 @@ from __future__ import annotations -import asyncio import contextlib import time from dataclasses import dataclass from typing import TYPE_CHECKING +from ccproxy.core.async_runtime import ( + CancelledError as RuntimeCancelledError, +) +from ccproxy.core.async_runtime import ( + Future, + Queue, + Task, + create_queue, +) +from ccproxy.core.async_runtime import ( + InvalidStateError as RuntimeInvalidStateError, +) +from ccproxy.core.async_runtime import ( + create_future as runtime_create_future, +) +from ccproxy.core.async_runtime import ( + create_task as runtime_create_task, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) from ccproxy.core.logging import get_plugin_logger from .. import PermissionRequest @@ -73,7 +93,7 @@ class PendingRequest: """Represents a pending confirmation request with its response future.""" request: PermissionRequest - future: asyncio.Future[bool] + future: Future[bool] cancelled: bool = False @@ -177,7 +197,7 @@ async def _show_result(self, allowed: bool, message: str) -> None: dialog.styles.border = ("solid", "red") # Give user time to see the result - await asyncio.sleep(1.5) + await runtime_sleep(1.5) self.dismiss(allowed) def action_confirm(self) -> None: @@ -358,7 +378,7 @@ async def _show_result(self, allowed: bool, message: str) -> None: dialog.styles.border = ("solid", "red") # Give user time to see the result - await asyncio.sleep(1.5) + await runtime_sleep(1.5) self.exit(allowed) def action_confirm(self) -> None: @@ -400,24 +420,20 @@ class TerminalPermissionHandler: def __init__(self) -> None: """Initialize the terminal confirmation handler.""" - self._request_queue: ( - asyncio.Queue[tuple[PermissionRequest, asyncio.Future[bool]]] | None - ) = None + self._request_queue: Queue[tuple[PermissionRequest, Future[bool]]] | None = None self._cancelled_requests: set[str] = set() - self._processing_task: asyncio.Task[None] | None = None + self._processing_task: Task[None] | None = None self._active_apps: dict[str, ConfirmationApp] = {} def _get_request_queue( self, - ) -> asyncio.Queue[tuple[PermissionRequest, asyncio.Future[bool]]]: + ) -> Queue[tuple[PermissionRequest, Future[bool]]]: """Lazily initialize and return the request queue.""" if self._request_queue is None: - self._request_queue = asyncio.Queue() + self._request_queue = create_queue() return self._request_queue - def _safe_set_future_result( - self, future: asyncio.Future[bool], result: bool - ) -> bool: + def _safe_set_future_result(self, future: Future[bool], result: bool) -> bool: """Safely set a future result, handling already cancelled futures. Args: @@ -432,12 +448,12 @@ def _safe_set_future_result( try: future.set_result(result) return True - except asyncio.InvalidStateError: + except RuntimeInvalidStateError: # Future was already resolved or cancelled return False def _safe_set_future_exception( - self, future: asyncio.Future[bool], exception: BaseException + self, future: Future[bool], exception: BaseException ) -> bool: """Safely set a future exception, handling already cancelled futures. @@ -453,7 +469,7 @@ def _safe_set_future_exception( try: future.set_exception(exception) return True - except asyncio.InvalidStateError: + except RuntimeInvalidStateError: # Future was already resolved or cancelled return False @@ -470,13 +486,13 @@ async def _process_queue(self) -> None: # Process the request await self._process_single_request(request, future) - except asyncio.CancelledError: + except RuntimeCancelledError: break except Exception as e: logger.error("queue_processing_error", error=str(e), exc_info=True) def _is_request_processable( - self, request: PermissionRequest, future: asyncio.Future[bool] + self, request: PermissionRequest, future: Future[bool] ) -> bool: """Check if a request can be processed.""" # Check if cancelled before processing @@ -493,7 +509,7 @@ def _is_request_processable( return True async def _process_single_request( - self, request: PermissionRequest, future: asyncio.Future[bool] + self, request: PermissionRequest, future: Future[bool] ) -> None: """Process a single permission request.""" app = None @@ -532,11 +548,11 @@ async def _process_single_request( def _ensure_processing_task_running(self) -> None: """Ensure the processing task is running.""" if self._processing_task is None or self._processing_task.done(): - self._processing_task = asyncio.create_task(self._process_queue()) + self._processing_task = runtime_create_task(self._process_queue()) async def _queue_and_wait_for_result(self, request: PermissionRequest) -> bool: """Queue a request and wait for its result.""" - future: asyncio.Future[bool] = asyncio.Future() + future: Future[bool] = runtime_create_future() await self._get_request_queue().put((request, future)) return await future @@ -605,7 +621,7 @@ def cancel_confirmation(self, request_id: str, reason: str = "cancelled") -> Non if request_id in self._active_apps: app = self._active_apps[request_id] # Schedule the cancellation feedback asynchronously - asyncio.create_task(self._cancel_active_dialog(app, reason)) + runtime_create_task(self._cancel_active_dialog(app, reason)) async def _cancel_active_dialog(self, app: ConfirmationApp, reason: str) -> None: """Cancel an active dialog with visual feedback. @@ -643,7 +659,7 @@ async def shutdown(self) -> None: """Shutdown the handler and cleanup resources.""" if self._processing_task and not self._processing_task.done(): self._processing_task.cancel() - with contextlib.suppress(asyncio.CancelledError): + with contextlib.suppress(RuntimeCancelledError): await self._processing_task self._processing_task = None diff --git a/ccproxy/plugins/pricing/tasks.py b/ccproxy/plugins/pricing/tasks.py index f9413097..41ea9eb8 100644 --- a/ccproxy/plugins/pricing/tasks.py +++ b/ccproxy/plugins/pricing/tasks.py @@ -1,12 +1,24 @@ """Pricing plugin scheduled tasks.""" -import asyncio import contextlib import random import time from abc import ABC, abstractmethod from typing import Any +from ccproxy.core.async_runtime import ( + CancelledError as RuntimeCancelledError, +) +from ccproxy.core.async_runtime import ( + Task, + create_event, +) +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import create_managed_task from ccproxy.core.logging import get_plugin_logger @@ -49,8 +61,8 @@ def __init__( self.jitter_factor = min(1.0, max(0.0, jitter_factor)) # Task state - self._task: asyncio.Task[None] | None = None - self._stop_event = asyncio.Event() + self._task: Task[None] | None = None + self._stop_event = create_event() self._consecutive_failures = 0 self._last_success_time: float | None = None self._next_run_time: float | None = None @@ -156,9 +168,9 @@ async def _task_loop(self) -> None: # Wait for next execution or stop event try: - await asyncio.wait_for(self._stop_event.wait(), timeout=delay) + await runtime_wait_for(self._stop_event.wait(), timeout=delay) break # Stop event was set - except TimeoutError: + except RuntimeTimeoutError: continue # Time to run again finally: @@ -195,14 +207,14 @@ async def stop(self, timeout: float = 10.0) -> None: # Wait for task to complete try: - await asyncio.wait_for(self._task, timeout=timeout) - except TimeoutError: + await runtime_wait_for(self._task, timeout=timeout) + except RuntimeTimeoutError: logger.warning( "scheduled_task_stop_timeout", task_name=self.name, timeout=timeout ) if not self._task.done(): self._task.cancel() - with contextlib.suppress(asyncio.CancelledError): + with contextlib.suppress(RuntimeCancelledError): await self._task self._task = None diff --git a/ccproxy/scheduler/core.py b/ccproxy/scheduler/core.py index 1e229e95..aa6d1929 100644 --- a/ccproxy/scheduler/core.py +++ b/ccproxy/scheduler/core.py @@ -1,10 +1,23 @@ """Core scheduler for managing periodic tasks.""" -import asyncio from typing import Any import structlog +from ccproxy.core.async_runtime import ( + Semaphore, + create_semaphore, +) +from ccproxy.core.async_runtime import ( + TimeoutError as RuntimeTimeoutError, +) +from ccproxy.core.async_runtime import ( + gather as runtime_gather, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) + from .errors import ( SchedulerError, SchedulerShutdownError, @@ -49,7 +62,7 @@ def __init__( self._running = False self._tasks: dict[str, BaseScheduledTask] = {} - self._semaphore: asyncio.Semaphore | None = None + self._semaphore: Semaphore | None = None async def start(self) -> None: """Start the scheduler and all enabled tasks.""" @@ -58,7 +71,7 @@ async def start(self) -> None: return self._running = True - self._semaphore = asyncio.Semaphore(self.max_concurrent_tasks) + self._semaphore = create_semaphore(self.max_concurrent_tasks) logger.debug( "scheduler_starting", @@ -103,12 +116,15 @@ async def stop(self) -> None: if stop_tasks: try: # Wait for all tasks to stop gracefully - await asyncio.wait_for( - asyncio.gather(*stop_tasks, return_exceptions=True), + await runtime_wait_for( + runtime_gather( + *stop_tasks, + return_exceptions=True, + ), timeout=self.graceful_shutdown_timeout, ) logger.debug("scheduler_stopped_gracefully") - except TimeoutError: + except RuntimeTimeoutError: logger.warning( "scheduler_shutdown_timeout", timeout=self.graceful_shutdown_timeout, diff --git a/ccproxy/scheduler/tasks.py b/ccproxy/scheduler/tasks.py index d5a94e57..ac4bf8f9 100644 --- a/ccproxy/scheduler/tasks.py +++ b/ccproxy/scheduler/tasks.py @@ -1,6 +1,5 @@ """Base scheduled task classes and task implementations.""" -import asyncio import random import time from abc import ABC, abstractmethod @@ -10,6 +9,18 @@ import structlog from packaging import version as pkg_version +from ccproxy.core.async_runtime import ( + CancelledError, + Event, + Task, + create_event, +) +from ccproxy.core.async_runtime import ( + sleep as runtime_sleep, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.core.async_task_manager import create_managed_task from ccproxy.scheduler.errors import SchedulerError from ccproxy.utils.version_checker import ( @@ -66,8 +77,8 @@ def __init__( self._consecutive_failures = 0 self._last_run_time: float = 0 self._running = False - self._task: asyncio.Task[Any] | None = None - self._stop_complete: asyncio.Event | None = None + self._task: Task[Any] | None = None + self._stop_complete: Event | None = None @abstractmethod async def run(self) -> bool: @@ -129,7 +140,7 @@ async def start(self) -> None: return self._running = True - self._stop_complete = asyncio.Event() + self._stop_complete = create_event() logger.debug("task_starting", task_name=self.name) try: @@ -175,7 +186,7 @@ async def stop(self) -> None: try: # Wait for the task to complete cancellation await self._task - except asyncio.CancelledError: + except CancelledError: # Expected when task is cancelled pass except Exception as e: @@ -192,7 +203,7 @@ async def stop(self) -> None: # Wait for the completion event to be signaled if self._stop_complete is not None: try: - await asyncio.wait_for(self._stop_complete.wait(), timeout=1.0) + await runtime_wait_for(self._stop_complete.wait(), timeout=1.0) except TimeoutError: logger.warning( "task_stop_completion_timeout", @@ -262,9 +273,9 @@ async def _run_loop(self) -> None: ) # Wait for next execution or cancellation - await asyncio.sleep(delay) + await runtime_sleep(delay) - except asyncio.CancelledError: + except CancelledError: logger.debug("task_cancelled", task_name=self.name) break except TimeoutError as e: @@ -279,7 +290,7 @@ async def _run_loop(self) -> None: ) # Use backoff delay for exceptions too backoff_delay = self.calculate_next_delay() - await asyncio.sleep(backoff_delay) + await runtime_sleep(backoff_delay) except SchedulerError as e: self._consecutive_failures += 1 logger.error( @@ -292,7 +303,7 @@ async def _run_loop(self) -> None: ) # Use backoff delay for exceptions too backoff_delay = self.calculate_next_delay() - await asyncio.sleep(backoff_delay) + await runtime_sleep(backoff_delay) except Exception as e: self._consecutive_failures += 1 logger.error( @@ -305,7 +316,7 @@ async def _run_loop(self) -> None: ) # Use backoff delay for exceptions too backoff_delay = self.calculate_next_delay() - await asyncio.sleep(backoff_delay) + await runtime_sleep(backoff_delay) # Signal that the task has completed if self._stop_complete is not None: diff --git a/ccproxy/services/cli_detection.py b/ccproxy/services/cli_detection.py index ef7c49dd..58518300 100644 --- a/ccproxy/services/cli_detection.py +++ b/ccproxy/services/cli_detection.py @@ -5,7 +5,6 @@ It eliminates duplicate CLI detection logic by consolidating common patterns. """ -import asyncio import json import re from typing import Any, NamedTuple @@ -14,6 +13,18 @@ from ccproxy.config.settings import Settings from ccproxy.config.utils import get_ccproxy_cache_dir +from ccproxy.core.async_runtime import ( + PIPE, +) +from ccproxy.core.async_runtime import ( + create_subprocess_exec as runtime_create_subprocess_exec, +) +from ccproxy.core.async_runtime import ( + gather as runtime_gather, +) +from ccproxy.core.async_runtime import ( + wait_for as runtime_wait_for, +) from ccproxy.utils.binary_resolver import BinaryResolver, CLIInfo from ccproxy.utils.caching import TTLCache @@ -211,13 +222,13 @@ async def _get_cli_version( cmd = cli_command + [version_flag] # Run command with timeout - process = await asyncio.create_subprocess_exec( + process = await runtime_create_subprocess_exec( *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stdout=PIPE, + stderr=PIPE, ) - stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0) + stdout, stderr = await runtime_wait_for(process.communicate(), timeout=5.0) version = None if process.returncode == 0 and stdout: @@ -406,7 +417,7 @@ async def detect_multiple( self.detect_cli(binary_name, package_name) for binary_name, package_name in binaries ] - results = await asyncio.gather(*tasks, return_exceptions=True) + results = await runtime_gather(*tasks, return_exceptions=True) detected: dict[str, CLIDetectionResult] = {} for (binary_name, _), result in zip(binaries, results, strict=False): diff --git a/ccproxy/services/mocking/mock_handler.py b/ccproxy/services/mocking/mock_handler.py index 8cdd1afb..6424f863 100644 --- a/ccproxy/services/mocking/mock_handler.py +++ b/ccproxy/services/mocking/mock_handler.py @@ -1,6 +1,5 @@ """Mock response handler for bypass mode.""" -import asyncio import json import random from collections.abc import AsyncGenerator @@ -9,6 +8,7 @@ import structlog from fastapi.responses import StreamingResponse +from ccproxy.core.async_runtime import sleep as runtime_sleep from ccproxy.core.request_context import RequestContext from ccproxy.services.adapters.format_adapter import DictFormatAdapter from ccproxy.services.adapters.simple_converters import ( @@ -105,7 +105,7 @@ async def generate_standard_response( """ # Simulate latency latency = random.uniform(*self.latency_range) - await asyncio.sleep(latency) + await runtime_sleep(latency) # Check if we should simulate an error if self.should_simulate_error(): @@ -211,7 +211,7 @@ async def stream_generator() -> AsyncGenerator[bytes, None]: if i + chunk_size < len(words): chunk_text += " " - await asyncio.sleep(0.05) # Simulate token generation delay + await runtime_sleep(0.05) # Simulate token generation delay if is_openai_format: chunk_event = { diff --git a/ccproxy/testing/endpoints/cli.py b/ccproxy/testing/endpoints/cli.py index a26f5760..3722f90f 100644 --- a/ccproxy/testing/endpoints/cli.py +++ b/ccproxy/testing/endpoints/cli.py @@ -3,13 +3,14 @@ from __future__ import annotations import argparse -import asyncio import json import logging import sys import structlog +from ccproxy.core.async_runtime import run as runtime_run + from .config import list_available_tests from .console import colored_header, colored_info from .runner import run_endpoint_tests_async @@ -191,7 +192,7 @@ def main(argv: list[str] | None = None) -> None: print(colored_info("→ Structured output mode enabled")) try: - summary = asyncio.run( + summary = runtime_run( run_endpoint_tests_async(base_url=args.base, tests=args.tests) ) except ValueError as exc: diff --git a/ccproxy/testing/endpoints/runner.py b/ccproxy/testing/endpoints/runner.py index e82d5537..f2306c2a 100644 --- a/ccproxy/testing/endpoints/runner.py +++ b/ccproxy/testing/endpoints/runner.py @@ -3,7 +3,6 @@ from __future__ import annotations import ast -import asyncio import copy import json import re @@ -13,6 +12,8 @@ import httpx import structlog +from ccproxy.core.async_runtime import current_task +from ccproxy.core.async_runtime import run as runtime_run from ccproxy.llms.models.openai import ResponseMessage, ResponseObject from ccproxy.llms.streaming.accumulators import StreamAccumulator @@ -1879,18 +1880,13 @@ def run_endpoint_tests( ) -> EndpointTestRunSummary: """Convenience wrapper to run endpoint tests from synchronous code.""" - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = None - - if loop and loop.is_running(): + if current_task() is not None: raise RuntimeError( "run_endpoint_tests() cannot be called while an event loop is running; " "use await run_endpoint_tests_async(...) instead" ) - return asyncio.run(run_endpoint_tests_async(base_url=base_url, tests=tests)) + return runtime_run(run_endpoint_tests_async(base_url=base_url, tests=tests)) __all__ = [ diff --git a/docs/OAUTH_PLUGIN_ARCHITECTURE.md b/docs/OAUTH_PLUGIN_ARCHITECTURE.md new file mode 100644 index 00000000..40f6f915 --- /dev/null +++ b/docs/OAUTH_PLUGIN_ARCHITECTURE.md @@ -0,0 +1,52 @@ +# OAuth Plugin Architecture + +This document describes how CCProxy structures OAuth-capable plugins and the shared conventions you should follow when adding new providers. + +## Core Concepts +- **Token Storage** – Each provider ships a storage backend that persists OAuth credentials (usually JSON files under `~/.config/ccproxy`). +- **Token Manager** – A subclass of `BaseEnhancedTokenManager` responsible for refreshing tokens, exposing `TokenSnapshot` data, and integrating with the CLI. +- **Plugin Runtime** – The plugin factory wires the manager and storage into a `ProviderPluginRuntime` so adapters receive authenticated HTTP clients via dependency injection. +- **Hook Integration** – OAuth managers emit hook events (`HookEvent.OAUTH_TOKEN_REQUEST`, `HookEvent.OAUTH_TOKEN_RESPONSE`, etc.) for observability and analytics. + +## File Layout +``` +ccproxy/plugins// +├── manager.py # Token manager implementation +├── storage.py # Credential persistence helpers +├── plugin.py # Factory + manifest wiring +├── adapter.py # HTTP adapter using the manager +├── routes.py # FastAPI routers (if applicable) +├── schemas.py # Pydantic models for OAuth payloads +└── README.md # Provider-specific notes +``` + +## OAuth Flow +1. **CLI Initiation** – `ccproxy auth login ` looks up the plugin manifest and resolves its `OAuthFlow` descriptor. +2. **Device / Browser Grant** – The CLI opens the provider authorization URL (PKCE, device-code, or standard authorization code) and polls for completion using `httpx`. +3. **Credential Storage** – The resulting tokens are serialized via the plugin storage class. Secrets are masked when echoed back to the user. +4. **Runtime Refresh** – On startup the token manager loads credentials, validates expiry, and schedules refresh jobs through the async task manager. +5. **Snapshot Reporting** – Managers expose `TokenSnapshot` for `/auth status`, enabling consistent diagnostics across providers. + +## Implementing a New OAuth Plugin +1. **Define Settings** – Create a `Config` model with the provider-specific endpoints and scopes. Register defaults in the plugin manifest. +2. **Implement Storage** – Subclass `BaseJSONTokenStorage` (or equivalent) to read/write credential files. Handle migration of legacy formats in `migrate()`. +3. **Implement Manager** – Extend `BaseEnhancedTokenManager` and implement `_build_token_snapshot()` plus provider-specific refresh logic. Use `AsyncHTTPClientFactory` from the container for outbound requests. +4. **Expose Routes / CLI** – If the provider needs webhook callbacks or additional CLI commands, add them via `manifest.routes` and `manifest.cli_commands`. +5. **Register Plugin** – Export `factory = ()` at module scope so entry-point discovery works. + +## Security Recommendations +- Store tokens with `0600` permissions and avoid embedding secrets in configuration files committed to source control. +- Use PKCE (code verifier + challenge) whenever the provider supports it. +- Rotate refresh tokens when the provider issues new ones; do not assume they are long lived. +- Emit structured log events without leaking raw tokens. The `TokenSnapshot` helper masks sensitive fields by default. + +## Testing Checklist +- Unit-test storage migrations to ensure existing users keep working after upgrades. +- Mock OAuth endpoints with `pytest-httpx` and cover success, refresh, and error paths. +- Run `make test-plugins` with the plugin enabled to verify end-to-end behaviour. +- Exercise CLI flows manually using sandbox credentials before releasing a new provider version. + +## Troubleshooting +- Use `ccproxy auth status --provider ` to inspect stored credentials and expiry timestamps. +- Enable verbose logging: `LOGGING__LEVEL=debug uv run ccproxy serve`. +- If refresh attempts fail repeatedly, delete the credential file and redo `ccproxy auth login` to obtain a clean grant. diff --git a/docs/anyio.txt b/docs/anyio.txt new file mode 100644 index 00000000..5e51389d --- /dev/null +++ b/docs/anyio.txt @@ -0,0 +1,8950 @@ +================ +CODE SNIPPETS +================ +TITLE: Pass Backend Options to AnyIO Run +DESCRIPTION: Illustrates how to pass backend-specific options to `anyio.run()` using the `backend_options` argument. Examples include enabling debug mode for asyncio and configuring interrupt handling for Trio. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/basics.rst#_snippet_5 + +LANGUAGE: python +CODE: +``` +run(main, backend="asyncio", backend_options={"debug": True}) +run( + main, + backend="trio", + backend_options={"restrict_keyboard_interrupt_to_checkpoints": True} +) +``` + +-------------------------------- + +TITLE: Install AnyIO +DESCRIPTION: Installs the AnyIO library using pip. This is the basic installation command. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/basics.rst#_snippet_0 + +LANGUAGE: bash +CODE: +``` +pip install anyio +``` + +-------------------------------- + +TITLE: Install AnyIO with Trio Backend +DESCRIPTION: Installs AnyIO along with the Trio backend as an extra dependency. This allows using Trio as the underlying asynchronous framework. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/basics.rst#_snippet_1 + +LANGUAGE: bash +CODE: +``` +pip install anyio[trio] +``` + +-------------------------------- + +TITLE: Run AnyIO Application +DESCRIPTION: Starts an AnyIO application by running the given async function. It handles the setup and teardown of the AnyIO runtime and the event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +anyio.run(async_function, backend='asyncio') +``` + +-------------------------------- + +TITLE: Install Pre-commit Hooks +DESCRIPTION: This command installs the pre-commit hooks for the AnyIO repository. Activating these hooks ensures that code style and quality checks are performed locally before committing changes. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contributing.rst#_snippet_2 + +LANGUAGE: Shell +CODE: +``` +pre-commit install +``` + +-------------------------------- + +TITLE: Start Task and Wait for Initialization in Python +DESCRIPTION: Shows how to start a task and wait for its initialization signal using `TaskGroup.start()` and `TaskStatus`. This is useful for services that need to be ready before dependent tasks proceed. The `task_status.started()` call is mandatory for the spawned task. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tasks.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import ( + TASK_STATUS_IGNORED, + create_task_group, + connect_tcp, + create_tcp_listener, + run, +) +from anyio.abc import TaskStatus + + +async def handler(stream): + ... + + +async def start_some_service( + port: int, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED +): + async with await create_tcp_listener( + local_host="127.0.0.1", local_port=port + ) as listener: + task_status.started() + await listener.serve(handler) + + +async def main(): + async with create_task_group() as tg: + await tg.start(start_some_service, 5000) + async with await connect_tcp("127.0.0.1", 5000) as stream: + ... + + +run(main) +``` + +-------------------------------- + +TITLE: Start TLS Listener with AnyIO +DESCRIPTION: Sets up a TLS listener on a specified port and handles incoming connections using a provided handler function. Requires a TLS context. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +from anyio import create_tcp_listener, run +from anyio.streams.tls import TLSListener + +async def handle(client, server): + # Handle client connection + pass + +async def main(): + # Assuming 'context' is a pre-configured SSL context + listener = TLSListener(await create_tcp_listener(local_port=1234), context) + await listener.serve(handle) + +run(main) +``` + +-------------------------------- + +TITLE: Run Async Program with AnyIO (Default Backend) +DESCRIPTION: A simple AnyIO program that prints 'Hello, world!' and runs on the default backend (usually asyncio). It demonstrates the basic structure of an async function and how to execute it using `anyio.run()`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/basics.rst#_snippet_2 + +LANGUAGE: python +CODE: +``` +from anyio import run + + +async def main(): + print('Hello, world!') + +run(main) +``` + +-------------------------------- + +TITLE: Run Async Program with Specific Backend (AnyIO) +DESCRIPTION: Shows how to explicitly specify the backend (e.g., 'trio' or 'asyncio') when running an async program with `anyio.run()`. This provides control over which asynchronous framework is used. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/basics.rst#_snippet_3 + +LANGUAGE: python +CODE: +``` +run(main, backend='trio') +run(main, backend='asyncio') +``` + +-------------------------------- + +TITLE: TaskGroup Start Method +DESCRIPTION: Adds the `TaskGroup.start()` method and the corresponding `BlockingPortal.start_task()` method. These allow for more explicit control over task initiation within task groups. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_163 + +LANGUAGE: Python +CODE: +``` +anyio.TaskGroup.start() +``` + +LANGUAGE: Python +CODE: +``` +anyio.BlockingPortal.start_task() +``` + +-------------------------------- + +TITLE: TLS Server Setup with SSL Context +DESCRIPTION: Demonstrates setting up a basic TLS server in Python using AnyIO and the `ssl` module. It includes creating an SSL context, loading a certificate and key, and handling client connections. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +import ssl + +from anyio import create_tcp_listener, run +from anyio.streams.tls import TLSListener + + +async def handle(client): + async with client: + name = await client.receive() + await client.send(b'Hello, %s\n' % name) + + +async def main(): + # Create a context for the purpose of authenticating clients + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + + # Load the server certificate and private key + context.load_cert_chain(certfile='cert.pem', keyfile='key.pem') +``` + +-------------------------------- + +TITLE: Run Async Program using Native Trio Backend +DESCRIPTION: Demonstrates running an AnyIO-compatible async function directly using the native `trio.run()` function. It also shows how to identify the current async library using `sniffio.current_async_library()`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/basics.rst#_snippet_4 + +LANGUAGE: python +CODE: +``` +import sniffio +import trio +from anyio import sleep + + +async def main(): + print('Hello') + await sleep(1) + print("I'm running on", sniffio.current_async_library()) + +trio.run(main) +``` + +-------------------------------- + +TITLE: AnyIO CapacityLimiter Example +DESCRIPTION: Illustrates the use of AnyIO's CapacityLimiter to control concurrent access to a limited resource. It shows how to initialize a limiter with a specific token count and how tasks acquire and release tokens. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/synchronization.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +from anyio import CapacityLimiter, create_task_group, sleep, run + + +async def use_resource(tasknum, limiter): + async with limiter: + print(f"Task number {tasknum} is now working with the shared resource") + await sleep(1) + + +async def main(): + limiter = CapacityLimiter(2) + async with create_task_group() as tg: + for num in range(10): + tg.start_soon(use_resource, num, limiter) + +run(main) +``` + +-------------------------------- + +TITLE: Start Task and Wait for Readiness (AnyIO) +DESCRIPTION: Demonstrates using `BlockingPortal.start_task` to start a task and wait for it to signal readiness by calling `task_status.started()`. This method returns both a future for the task's result and the initial start value provided by the task. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +from anyio import sleep, TASK_STATUS_IGNORED +from anyio.from_thread import start_blocking_portal + + +async def service_task(*, task_status=TASK_STATUS_IGNORED): + task_status.started('STARTED') + await sleep(1) + return 'DONE' + + +with start_blocking_portal() as portal: + future, start_value = portal.start_task(service_task) + print('Task has started with value', start_value) + + return_value = future.result() + print('Task has finished with return value', return_value) +``` + +-------------------------------- + +TITLE: File Read/Write Stream Example +DESCRIPTION: Shows how to use FileReadStream and FileWriteStream to read from and write to files. It demonstrates writing a byte string to a file and then reading it back, printing the content. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +from anyio import run +from anyio.streams.file import FileReadStream, FileWriteStream + + +async def main(): + path = '/tmp/testfile' + async with await FileWriteStream.from_path(path) as stream: + await stream.send(b'Hello, World!') + + async with await FileReadStream.from_path(path) as stream: + async for chunk in stream: + print(chunk.decode(), end='') + + print() + +run(main) +``` + +-------------------------------- + +TITLE: AnyIO Task Group Resource Cleanup on Cancellation +DESCRIPTION: Demonstrates that a newly created task in AnyIO, even if cancelled immediately, gets an opportunity to react to the cancellation. This example shows a resource's __aexit__ method being called, printing 'closed'. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +import asyncio + +import anyio + + +class Resource: + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + # Here would be the code that cleanly closes the resource + print("closed") + + +async def handle_resource(resource): + async with resource: + ... + + +async def main(): + async with anyio.create_task_group() as tg: + tg.start_soon(handle_resource, Resource()) + tg.cancel_scope.cancel() + + +# Output: "closed" +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: AnyIO Condition Example +DESCRIPTION: Demonstrates the usage of AnyIO's Condition primitive for synchronizing tasks. It shows how tasks can wait for a condition to be met and how to notify one or all waiting tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/synchronization.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +from anyio import Condition, create_task_group, sleep, run + + +async def listen(tasknum, condition): + async with condition: + await condition.wait() + print('Woke up task number', tasknum) + + +async def main(): + condition = Condition() + async with create_task_group() as tg: + for tasknum in range(6): + tg.start_soon(listen, tasknum, condition) + + await sleep(1) + async with condition: + condition.notify(1) + + await sleep(1) + async with condition: + condition.notify(2) + + await sleep(1) + async with condition: + condition.notify_all() + +run(main) +``` + +-------------------------------- + +TITLE: Buffered Byte Receive Stream Example +DESCRIPTION: Demonstrates the usage of BufferedByteReceiveStream for receiving data in chunks. It shows how to receive a specific number of bytes and how to receive data until a delimiter is encountered. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from anyio import run +from anyio.streams.buffered import BufferedByteReceiveStream +from anyio.streams.memory import create_memory_object_stream + + +async def main(): + send, receive = create_memory_object_stream[bytes](4) + buffered = BufferedByteReceiveStream(receive) + for part in b'hel', b'lo, ', b'wo', b'rld!': + await send.send(part) + + result = await buffered.receive_exactly(8) + print(repr(result)) + + result = await buffered.receive_until(b'!', 10) + print(repr(result)) + +run(main) +``` + +-------------------------------- + +TITLE: Text Stream Encoding/Decoding Example +DESCRIPTION: Illustrates how TextReceiveStream and TextSendStream can be used to seamlessly send and receive text data by handling the encoding and decoding of strings to bytes. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +from anyio import run, create_memory_object_stream +from anyio.streams.text import TextReceiveStream, TextSendStream + + +async def main(): + bytes_send, bytes_receive = create_memory_object_stream[bytes](1) + text_send = TextSendStream(bytes_send) + await text_send.send('åäö') + result = await bytes_receive.receive() + print(repr(result)) + + text_receive = TextReceiveStream(bytes_receive) + await bytes_send.send(result) + result = await text_receive.receive() + print(repr(result)) + +run(main) +``` + +-------------------------------- + +TITLE: Low-Level Temporary File and Directory Creation Asynchronously +DESCRIPTION: Provides an example of using lower-level asynchronous functions for temporary file and directory creation. It demonstrates mkstemp, mkdtemp, and gettempdir, highlighting that manual cleanup is required for mkstemp and mkdtemp. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tempfile.rst#_snippet_4 + +LANGUAGE: python +CODE: +``` +from anyio import mkstemp, mkdtemp, gettempdir, run +import os + +async def main(): + fd, path = await mkstemp(suffix=".txt", prefix="mkstemp_", text=True) + print(f"Created temp file: {path}") + + temp_dir = await mkdtemp(prefix="mkdtemp_") + print(f"Created temp dir: {temp_dir}") + + print(f"Default temp dir: {await gettempdir()}") + + os.remove(path) + +run(main) +``` + +-------------------------------- + +TITLE: Handle KeyboardInterrupt and SystemExit with AnyIO in Python +DESCRIPTION: This example shows how to handle Ctrl+C (KeyboardInterrupt) and termination signals (SystemExit) by installing a custom signal handler using `anyio.open_signal_receiver`. It cancels the task group's scope upon receiving a signal. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/signals.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +import signal + +from anyio import open_signal_receiver, create_task_group, run +from anyio.abc import CancelScope + + +async def signal_handler(scope: CancelScope): + with open_signal_receiver(signal.SIGINT, signal.SIGTERM) as signals: + async for signum in signals: + if signum == signal.SIGINT: + print('Ctrl+C pressed!') + else: + print('Terminated!') + + scope.cancel() + return + + +async def main(): + async with create_task_group() as tg: + tg.start_soon(signal_handler, tg.cancel_scope) + ... # proceed with starting the actual application logic + +run(main) +``` + +-------------------------------- + +TITLE: AnyIO Event Notification Example +DESCRIPTION: Demonstrates how to use AnyIO Events to notify tasks. An event is set by one task and waited upon by another, facilitating communication. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/synchronization.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import Event, create_task_group, run + + +async def notify(event): + event.set() + + +async def main(): + event = Event() + async with create_task_group() as tg: + tg.start_soon(notify, event) + await event.wait() + print('Received notification!') + +run(main) +``` + +-------------------------------- + +TITLE: Asyncio Task Cancellation Example +DESCRIPTION: Demonstrates a common issue with asyncio's edge cancellation where catching BaseException prevents proper task cancellation, leading to hangs. This example shows how a task catching CancelledError can continue execution unexpectedly. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +import asyncio + + +async def sleeper(): + try: + await asyncio.sleep(1) + except BaseException: + pass # the first cancellation is caught here + + # This call will never return unless the task is cancelled again + await asyncio.sleep(float("inf")) + +async def main(): + async with asyncio.TaskGroup() as tg: + task = tg.create_task(sleeper()) + await asyncio.sleep(0) # ensure that the task reaches the first sleep() + task.cancel() + + print("done") + +# Execution hangs +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: Asyncio TaskGroup Start Soon Consistency +DESCRIPTION: Ensures `TaskGroup.start_soon()` calls the target function immediately before starting the task for consistency across backends, and avoids coroutine wrappers on Python 3.8+. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_175 + +LANGUAGE: Python +CODE: +``` +asyncio.TaskGroup.start_soon() +``` + +-------------------------------- + +TITLE: BlockingPortal Start Task Soon +DESCRIPTION: Introduces the `name` argument to `BlockingPortal.start_task_soon()`, formerly `spawn_task()`. This allows naming tasks for better debugging and identification. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_164 + +LANGUAGE: Python +CODE: +``` +anyio.BlockingPortal.start_task_soon(name=...) +``` + +-------------------------------- + +TITLE: Open Process Handle for Interaction (Python) +DESCRIPTION: Launches a subprocess and provides a handle for more granular control over its input, output, and lifecycle. This example streams the stdout of the 'ps' command. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/subprocesses.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from anyio import open_process, run +from anyio.streams.text import TextReceiveStream + + +async def main(): + async with await open_process(['ps']) as process: + async for text in TextReceiveStream(process.stdout): + print(text) + +run(main) +``` + +-------------------------------- + +TITLE: AnyIO Lock Exclusive Access Example +DESCRIPTION: Shows how to use AnyIO Locks for ensuring exclusive access to a shared resource. Only one task can acquire the lock and access the resource at any given time. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/synchronization.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from anyio import Lock, create_task_group, sleep, run + + +async def use_resource(tasknum, lock): + async with lock: + print('Task number', tasknum, 'is now working with the shared resource') + await sleep(1) + + +async def main(): + lock = Lock() + async with create_task_group() as tg: + for num in range(4): + tg.start_soon(use_resource, num, lock) + +run(main) +``` + +-------------------------------- + +TITLE: Get All Available AnyIO Backends +DESCRIPTION: Retrieves a list of all available backend names that AnyIO can use to run asynchronous code. This is useful for selecting a specific backend or for introspection. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +anyio.get_all_backends() +``` + +-------------------------------- + +TITLE: Start Blocking Portal for Thread Communication +DESCRIPTION: Initiates a blocking portal, which is a mechanism for safely communicating between synchronous threads and the asynchronous event loop. It allows threads to run async code. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_28 + +LANGUAGE: Python +CODE: +``` +portal = anyio.from_thread.start_blocking_portal() +``` + +-------------------------------- + +TITLE: Fix RuntimeError: called 'started' twice on the same task status +DESCRIPTION: Corrects a 'RuntimeError: called 'started' twice on the same task status' that occurred when cancelling a task in a TaskGroup created with 'start()' before the first checkpoint after calling 'task_status.started()'. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_62 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def task_with_early_cancel(task_status: anyio.abc.TaskStatus): + try: + task_status.started() + # Simulate work before a potential cancellation point + await anyio.sleep(0.01) + # If cancelled here, the RuntimeError might occur if not handled properly + await anyio.sleep(10) + except anyio.get_cancelled_exc_class(): + print("Task was cancelled.") + raise + +async def run_taskgroup_with_start(): + async with anyio.create_task_group() as tg: + # Using start() method which is sensitive to cancellation timing + task_status = tg.start_soon(task_with_early_cancel) + await anyio.sleep(0.001) # Ensure task_status.started() is called + tg.cancel_scope.cancel() + # The fix ensures this cancellation doesn't lead to the RuntimeError + +# anyio.run(run_taskgroup_with_start) +``` + +-------------------------------- + +TITLE: Fix TaskGroup.start_soon() Type Annotation for Awaitables +DESCRIPTION: Corrects the type annotation for `TaskGroup.start_soon()` to properly accept any awaitables, ensuring type safety when starting asynchronous tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_96 + +LANGUAGE: python +CODE: +``` +Fixed the type annotation of ``TaskGroup.start_soon()`` to accept any awaitables + (already in v3.7.0 but was missing from 4.0.0rc1) +``` + +-------------------------------- + +TITLE: AnyIO Task Cancellation with Multiple Exceptions +DESCRIPTION: Demonstrates how AnyIO's cancel scopes deliver cancellation exceptions multiple times within a task, unlike asyncio's single delivery. This example shows a task that catches the first cancellation and then encounters another upon a subsequent await. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +import asyncio + +import anyio + + +async def sleeper(): + try: + await asyncio.sleep(1) + except BaseException: + pass # the first cancellation is caught here + + # This will raise another CancelledError + await asyncio.sleep(float("inf")) + +async def main(): + async with anyio.create_task_group() as tg: + tg.start_soon(sleeper) + await asyncio.sleep(0) # ensure that the task reaches the first sleep() + tg.cancel_scope.cancel() + + print("done") + +# Output: "done" +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: Get Temporary Directory Path +DESCRIPTION: Returns the path to the system's default temporary directory. This is useful for creating temporary files or directories in a standard location. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_36 + +LANGUAGE: Python +CODE: +``` +temp_dir = anyio.gettempdir() +temp_dir_bytes = anyio.gettempdirb() +``` + +-------------------------------- + +TITLE: Send UDP Packet with Prepended Data +DESCRIPTION: Creates a UDP socket, listens for incoming packets, and sends a response back to the sender with 'Hello, ' prepended to the received data. This example demonstrates basic UDP communication. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +import socket + +from anyio import create_udp_socket, run + + +async def main(): + async with await create_udp_socket( + family=socket.AF_INET, local_port=1234 + ) as udp: + async for packet, (host, port) in udp: + await udp.sendto(b'Hello, ' + packet, host, port) + +run(main) +``` + +-------------------------------- + +TITLE: Fix move_on_after and fail_after in AnyIO +DESCRIPTION: Version 1.4.0 resolves an issue where move_on_after() and fail_after() could get stuck on curio under certain circumstances. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_219 + +LANGUAGE: Python +CODE: +``` +Fixed move_on_after() and fail_after() getting stuck on curio in some circumstances +``` + +-------------------------------- + +TITLE: Safe Task Group Usage in Async Context Managers +DESCRIPTION: Provides an example of a generally safe pattern for using task groups within asynchronous context managers, ensuring that the host task remains active throughout the enclosed operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +from contextlib import asynccontextmanager + + +# Okay in most cases! +@asynccontextmanager +async def some_context_manager(): + async with create_task_group() as tg: + tg.start_soon(foo) + yield +``` + +-------------------------------- + +TITLE: Network Address Information +DESCRIPTION: Provides functions to get address information for network sockets and to get the hostname and service name from socket addresses. These functions are essential for network programming. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_69 + +LANGUAGE: Python +CODE: +``` +anyio.getaddrinfo +anyio.getnameinfo +``` + +-------------------------------- + +TITLE: Get Remote Address using Typed Attributes +DESCRIPTION: Demonstrates how to retrieve the remote IP address of a TCP connection using AnyIO's typed attributes. It connects to a host and port, then uses the `extra` method with `SocketAttribute.remote_address` to get the address. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/typedattrs.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import connect_tcp +from anyio.abc import SocketAttribute + + +async def connect(host, port, tls: bool): + stream = await connect_tcp(host, port, tls=tls) + print('Connected to', stream.extra(SocketAttribute.remote_address)) +``` + +-------------------------------- + +TITLE: Send UNIX Datagram Packet with Prepended Data +DESCRIPTION: Creates a UNIX datagram socket, listens for incoming packets on a specified local path, and sends a response back to the sender with 'Hello, ' prepended. This mirrors the UDP example but uses file system paths. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_8 + +LANGUAGE: Python +CODE: +``` +from anyio import create_unix_datagram_socket, run + + +async def main(): + async with await create_unix_datagram_socket( + local_path='/tmp/mysock' + ) as unix_dg: + async for packet, path in unix_dg: + await unix_dg.sendto(b'Hello, ' + packet, path) + +run(main) +``` + +-------------------------------- + +TITLE: AnyIO Semaphore Resource Limiting Example +DESCRIPTION: Illustrates using AnyIO Semaphores to limit concurrent access to a shared resource. A semaphore with an initial value of 2 allows only two tasks to access the resource at a time. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/synchronization.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import Semaphore, create_task_group, sleep, run + + +async def use_resource(tasknum, semaphore): + async with semaphore: + print(f"Task number {tasknum} is now working with the shared resource") + await sleep(1) + + +async def main(): + semaphore = Semaphore(2) + async with create_task_group() as tg: + for num in range(10): + tg.start_soon(use_resource, num, semaphore) + +run(main) +``` + +-------------------------------- + +TITLE: Fix TaskGroup.start() Cancellation Issues on Asyncio +DESCRIPTION: Addresses two bugs related to TaskGroup.start() on asyncio: erroneous RuntimeError when cancelling a task before checkpoint, and cancellation of the entire group if a start() call is cancelled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_63 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def task_that_might_be_cancelled(task_status: anyio.abc.TaskStatus): + task_status.started() + await anyio.sleep(10) # Keep task alive + +async def test_taskgroup_start_cancellation(): + async with anyio.create_task_group() as tg: + # Scenario 1: Cancel task before checkpoint + task_status = tg.start_soon(task_that_might_be_cancelled) + await anyio.sleep(0.001) # Ensure task_status.started() is called + tg.cancel_scope.cancel() # This should not raise RuntimeError + + # Scenario 2: Cancel the start() call itself (conceptual) + # This scenario is harder to directly simulate as start() is synchronous. + # The fix ensures that if the task spawned by start() is cancelled, + # it doesn't cascade to cancel the whole group incorrectly. + +# anyio.run(test_taskgroup_start_cancellation) +``` + +-------------------------------- + +TITLE: Start Blocking Portal for External Threads (AnyIO) +DESCRIPTION: Demonstrates how to initiate a blocking portal to allow external threads to interact with an AnyIO event loop. `anyio.from_thread.start_blocking_portal` is used to create a portal, which can then be used to call methods on the event loop from other threads. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +from anyio.from_thread import start_blocking_portal + + +with start_blocking_portal(backend='trio') as portal: + portal.call(...) +``` + +-------------------------------- + +TITLE: Asyncio Resource Management with TaskGroup Cancellation +DESCRIPTION: Demonstrates a scenario where a task responsible for resource management is cancelled before it can properly close the resource. This highlights a problem in asyncio's cancellation handling when tasks are cancelled before they start. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +import asyncio + + +class Resource: + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + # Here would be the code that cleanly closes the resource + print("closed") + + +async def handle_resource(resource): + async with resource: + ... + + +async def main(): + async with asyncio.TaskGroup() as tg: + task = tg.create_task(handle_resource(Resource())) + task.cancel() + + +# No output +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: Get Default Interpreter Limiter +DESCRIPTION: Retrieves the default limiter that controls the maximum number of subinterpreters that can be active concurrently. This helps manage resource consumption. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_22 + +LANGUAGE: Python +CODE: +``` +limiter = anyio.to_interpreter.current_default_interpreter_limiter() +``` + +-------------------------------- + +TITLE: Fix BlockingPortal Tasks Not Notifying Sync Listeners +DESCRIPTION: Corrects an issue where tasks started from `BlockingPortal` did not notify synchronous listeners (e.g., `concurrent.futures.wait()`) when they were cancelled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_93 + +LANGUAGE: python +CODE: +``` +Fixed tasks started from ``BlockingPortal`` not notifying synchronous listeners + (``concurrent.futures.wait()``) when they're cancelled +``` + +-------------------------------- + +TITLE: Build AnyIO Documentation with Tox +DESCRIPTION: This command uses tox to build the project's documentation. The generated HTML documentation will be available in the 'build' directory. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contributing.rst#_snippet_1 + +LANGUAGE: Shell +CODE: +``` +tox -e docs +``` + +-------------------------------- + +TITLE: Get Current Effective Deadline +DESCRIPTION: Retrieves the deadline of the innermost cancel scope that is currently active. This allows tasks to be aware of any imposed time limits. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_14 + +LANGUAGE: Python +CODE: +``` +deadline = anyio.current_effective_deadline() +``` + +-------------------------------- + +TITLE: Get Current Time in AnyIO +DESCRIPTION: Returns the current time according to the AnyIO event loop's clock. This is useful for timing operations or for logging. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +anyio.current_time() +``` + +-------------------------------- + +TITLE: Get Default Process Limiter +DESCRIPTION: Retrieves the default limiter that controls the maximum number of worker processes that can be used. This is essential for managing system resources when using multiprocessing. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_24 + +LANGUAGE: Python +CODE: +``` +limiter = anyio.to_process.current_default_process_limiter() +``` + +-------------------------------- + +TITLE: Get Current Task Function +DESCRIPTION: Introduces the `anyio.get_current_task()` function. This function returns information about the currently executing task, providing a way to introspect the task's context. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_234 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def inspect_current_task(): + current_task = anyio.get_current_task() + print(f"Current Task ID: {current_task.id}") +``` + +-------------------------------- + +TITLE: Run AnyIO Tests with Tox +DESCRIPTION: This snippet demonstrates how to use tox to run the AnyIO test suite, including pre-commit checks. It also shows how to run checks on all environments in parallel. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contributing.rst#_snippet_0 + +LANGUAGE: Shell +CODE: +``` +tox +tox -p +``` + +-------------------------------- + +TITLE: Get AnyIO Cancelled Exception Class +DESCRIPTION: Returns the specific exception class that AnyIO uses to signal cancellation. This allows for precise exception handling when dealing with cancelled operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +anyio.get_cancelled_exc_class() +``` + +-------------------------------- + +TITLE: Add Keyword Arguments for run_process and open_process +DESCRIPTION: Extends 'run_process()' and 'open_process()' with support for additional keyword arguments: 'startupinfo', 'creationflags', 'pass_fds', 'user', 'group', 'extra_groups', and 'umask'. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_49 + +LANGUAGE: Python +CODE: +``` +import anyio +import sys + +async def run_process_with_extra_args(): + # Example using startupinfo (Windows specific) + startupinfo = None + if sys.platform == "win32": + startupinfo = anyio.subprocess.STARTUPINFO() + startupinfo.dwFlags |= anyio.subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = anyio.subprocess.SW_HIDE + + try: + # Example with user and group (Unix-like systems) + await anyio.run_process("ls", + user="nobody", + group="nogroup", + startupinfo=startupinfo) + + # Example with creationflags (Windows specific) + # await anyio.run_process("cmd.exe", + # creationflags=anyio.subprocess.CREATE_NEW_PROCESS_GROUP) + + except Exception as e: + print(f"Error running process: {e}") + +async def open_process_with_extra_args(): + try: + process = await anyio.open_process("cat", + user="nobody", + group="nogroup") + await process.wait() + except Exception as e: + print(f"Error opening process: {e}") +``` + +-------------------------------- + +TITLE: Run async generator fixtures in single task for pytest plugin +DESCRIPTION: Modifies the pytest plugin to execute both setup and teardown phases of asynchronous generator fixtures within a single task. This enables use cases like cancel scopes and task groups that span the yield point. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_118 + +LANGUAGE: Python +CODE: +``` +# pytest plugin configuration or implementation detail +# Ensures proper lifecycle management for async fixtures. +``` + +-------------------------------- + +TITLE: Run Sync Function in Worker Interpreter with AnyIO +DESCRIPTION: Demonstrates how to execute a CPU-intensive function in a separate worker interpreter using AnyIO's `to_interpreter.run_sync`. This is suitable for parallelizing Python code that heavily utilizes the CPU and does not rely on releasing the GIL. The example shows importing necessary modules, defining an async main function, calling `run_sync` with the target function and its arguments, and printing the result. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/subinterpreters.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +import time + +from anyio import run, to_interpreter + +from yourothermodule import cpu_intensive_function + +async def main(): + result = await to_interpreter.run_sync( + cpu_intensive_function, 'Hello, ', 'world!' + ) + print(result) + +run(main) +``` + +-------------------------------- + +TITLE: Get Cancelled Exception Class +DESCRIPTION: Replaces the direct use of `CancelledError` with `anyio.get_cancelled_exc_class()`. This function returns the appropriate cancellation exception class for the current backend, ensuring consistent cancellation handling. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_235 + +LANGUAGE: Python +CODE: +``` +import anyio + +try: + # Some operation that might be cancelled + pass +except anyio.get_cancelled_exc_class(): + print("Operation was cancelled") +``` + +-------------------------------- + +TITLE: Abstract Network Client with Connectables +DESCRIPTION: Demonstrates creating a network client that abstracts the connection mechanism using AnyIO's Connectable interfaces. It shows how to handle different connection types like host/port or UNIX socket paths. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_10 + +LANGUAGE: Python +CODE: +``` +from os import PathLike +from ssl import SSLContext + +from anyio.abc import ByteStreamConnectable, as_connectable + + +class MyNetworkClient: + def __init__( + self, + connectable: ByteStreamConnectable | tuple[str, int] | str | PathLike[str], + tls: bool | SSLContext = False + ): + self.connectable = as_connectable(connectable, tls) + + async def __aenter__(self): + # Connect to the remote and enter the stream's context manager + self._stream = await self.connectable.connect() + await self._stream.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + # Exit the stream's context manager, thus disconnecting it + await self._stream.__aexit__(exc_type, exc_val, exc_tb) +``` + +-------------------------------- + +TITLE: Asyncio Cancellation and Lost Results Example +DESCRIPTION: Illustrates how cancelling an asyncio task that is about to resume with a value can lead to the loss of that result. The CancelledError is raised upon resumption, potentially skipping the intended data processing. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +import asyncio + + +async def receive(f): + print(await f) + await asyncio.sleep(1) + print("The task will be cancelled before this is printed") + + +async def main(): + f = asyncio.get_running_loop().create_future() + task = asyncio.create_task(receive(f)) + await asyncio.sleep(0) # make sure the task has started + f.set_result("hello") + task.cancel() + + # The "hello" result is lost due to the cancellation + try: + await task + except asyncio.CancelledError: + pass + + +# No output +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: Add current_time() Function +DESCRIPTION: Introduces the `anyio.current_time()` function, which returns the current time according to the AnyIO event loop. This provides a consistent way to get the current time across different backends. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_261 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def get_current_time(): + now = anyio.current_time() + print(f"Current time: {now}") +``` + +-------------------------------- + +TITLE: Create TemporaryDirectory Asynchronously +DESCRIPTION: Demonstrates the asynchronous creation of a temporary directory. It prints the path of the created directory, which is automatically cleaned up upon exiting the 'async with' block. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tempfile.rst#_snippet_3 + +LANGUAGE: python +CODE: +``` +from anyio import TemporaryDirectory, run + +async def main(): + async with TemporaryDirectory() as temp_dir: + print(f"Temporary directory path: {temp_dir}") + +run(main) +``` + +-------------------------------- + +TITLE: Get Current Default Thread Limiter +DESCRIPTION: Introduces the `anyio.current_default_thread_limiter()` function. This function provides access to the currently active default thread limiter, which can be used for managing thread pool execution. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_225 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def use_thread_limiter(): + thread_limiter = anyio.current_default_thread_limiter() + # Use the limiter to run tasks in threads +``` + +-------------------------------- + +TITLE: Add start_new_session to run_process() and open_process() +DESCRIPTION: Introduces the 'start_new_session' keyword argument to run_process() and open_process(), allowing for greater control over process creation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_119 + +LANGUAGE: Python +CODE: +``` +from anyio import run_process + +# Example usage: +# run_process('my_script.py', start_new_session=True) +``` + +-------------------------------- + +TITLE: Get Default Thread Limiter for Worker Threads +DESCRIPTION: Retrieves the default limiter that controls the maximum number of worker threads that can be used for running synchronous functions. This helps manage resource usage. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_20 + +LANGUAGE: Python +CODE: +``` +limiter = anyio.to_thread.current_default_thread_limiter() +``` + +-------------------------------- + +TITLE: Create and Use TemporaryFile Asynchronously +DESCRIPTION: Demonstrates the creation and usage of an asynchronous TemporaryFile. It writes content, seeks to the beginning, and reads the content back. The file is automatically deleted upon closure. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tempfile.rst#_snippet_0 + +LANGUAGE: python +CODE: +``` +from anyio import TemporaryFile, run + +async def main(): + async with TemporaryFile(mode="w+") as f: + await f.write("Temporary file content") + await f.seek(0) + print(await f.read()) # Output: Temporary file content + +run(main) +``` + +-------------------------------- + +TITLE: AnyIO ResourceGuard Usage +DESCRIPTION: Demonstrates how to use AnyIO's ResourceGuard to ensure exclusive access to a resource. It shows how to initialize a ResourceGuard within a class and use it as a context manager to prevent concurrent access. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/synchronization.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +from anyio import ResourceGuard, BusyResourceError + +class Resource: + def __init__(self): + self._guard = ResourceGuard() + + async def do_something() -> None: + with self._guard: + ... + +# Example of how it would be used: +# async def main(): +# resource = Resource() +# try: +# await resource.do_something() +# except BusyResourceError: +# print("Resource is busy") +``` + +-------------------------------- + +TITLE: Create and Manage Tasks with Task Groups in Python +DESCRIPTION: Demonstrates how to create multiple concurrent tasks using `create_task_group` and `start_soon`. Tasks run concurrently, and the task group ensures all tasks are finished before exiting the context. If any task raises an exception, others are cancelled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tasks.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import sleep, create_task_group, run + + +async def sometask(num: int) -> None: + print('Task', num, 'running') + await sleep(1) + print('Task', num, 'finished') + + +async def main() -> None: + async with create_task_group() as tg: + for num in range(5): + tg.start_soon(sometask, num) + + print('All tasks finished!') + +run(main) +``` + +-------------------------------- + +TITLE: Test Echo Server with Free TCP Port +DESCRIPTION: An asynchronous test function demonstrating how to use the 'free_tcp_port' fixture to set up an echo server and test a client connection. It creates a TCP listener on the provided free port, handles client connections, and sends/receives data. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_7 + +LANGUAGE: python +CODE: +``` +from anyio import connect_tcp, create_task_group, create_tcp_listener +from anyio.abc import SocketStream + + +async def test_echo(free_tcp_port: int) -> None: + async def handle(client_stream: SocketStream) -> None: + async with client_stream: + payload = await client_stream.receive() + await client_stream.send(payload[::-1]) + + async with ( + await create_tcp_listener(local_port=free_tcp_port) as listener, + create_task_group() as tg + ): + tg.start_soon(listener.serve, handle) + + async with await connect_tcp("127.0.0.1", free_tcp_port) as stream: + await stream.send(b"hello") + assert await stream.receive() == b"olleh" + + tg.cancel_scope.cancel() +``` + +-------------------------------- + +TITLE: Git Workflow for Pull Requests +DESCRIPTION: This sequence of git commands outlines the process for creating a new branch, making changes, committing them with a message that references an issue if applicable, and pushing the changes to a forked repository. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contributing.rst#_snippet_3 + +LANGUAGE: Shell +CODE: +``` +git clone git@github.com/yourusername/anyio +git checkout -b myfixname +git push +``` + +-------------------------------- + +TITLE: Instantiate Event directly in AnyIO 3 +DESCRIPTION: Shows the migration from using `create_event()` to directly instantiating `Event` in AnyIO 3. This simplifies the creation of synchronization primitives. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_10 + +LANGUAGE: Python +CODE: +``` +from anyio import Event + +async def main(): + event = Event() +``` + +-------------------------------- + +TITLE: Pytest Fixture for Setting Up and Tearing Down a Server +DESCRIPTION: An asynchronous pytest fixture that sets up a server, yields control, and then shuts down the server. It depends on the 'anyio_backend' fixture. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_6 + +LANGUAGE: python +CODE: +``` +@pytest.fixture(scope='module') +async def server(anyio_backend): + server = await setup_server() + yield + await server.shutdown() +``` + +-------------------------------- + +TITLE: Create TCP Listener +DESCRIPTION: Creates a TCP listener that binds to a specified host and port, ready to accept incoming connections. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_66 + +LANGUAGE: Python +CODE: +``` +listener = await anyio.create_tcp_listener(local_address=('127.0.0.1', 8000)) +``` + +-------------------------------- + +TITLE: Receive SIGTERM and SIGHUP Signals in Python +DESCRIPTION: This snippet demonstrates how to use `anyio.open_signal_receiver` to listen for SIGTERM and SIGHUP signals. It shows how to gracefully shut down on SIGTERM and reload configuration on SIGHUP. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/signals.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +import signal + +from anyio import open_signal_receiver, run + + +async def main(): + with open_signal_receiver(signal.SIGTERM, signal.SIGHUP) as signals: + async for signum in signals: + if signum == signal.SIGTERM: + return + elif signum == signal.SIGHUP: + print('Reloading configuration') + +run(main) +``` + +-------------------------------- + +TITLE: Add env and cwd to run_process() and open_process() +DESCRIPTION: Introduces ``env`` and ``cwd`` keyword arguments to the ``run_process()`` and ``open_process()`` functions, allowing for environment variable and current working directory configuration. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_151 + +LANGUAGE: Python +CODE: +``` +Added ``env`` and ``cwd`` keyword arguments to ``run_process()`` and ``open_process()``. +``` + +-------------------------------- + +TITLE: Fix to_process.run_sync() Initialization Failure +DESCRIPTION: Resolves an issue where 'to_process.run_sync()' failed to initialize correctly if '__main__.__file__' pointed to a file in a nonexistent directory. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_52 + +LANGUAGE: Python +CODE: +``` +import anyio +import os + +# This is a conceptual example. The actual fix is within AnyIO's implementation. +# To test this, one would need to simulate the condition described. + +def sync_function(): + print("Running sync function.") + +async def run_sync_in_process(): + # Simulate the condition where __main__.__file__ might be problematic + original_main_file = os.environ.get('__MAIN_FILE__') + os.environ['__MAIN_FILE__'] = '/nonexistent/path/__main__.py' + + try: + await anyio.to_process.run_sync(sync_function) + except Exception as e: + print(f"Caught expected error: {e}") + finally: + # Restore original value + if original_main_file is not None: + os.environ['__MAIN_FILE__'] = original_main_file + else: + del os.environ['__MAIN_FILE__'] + +# To run this, you would typically call: +# anyio.run(run_sync_in_process) +``` + +-------------------------------- + +TITLE: Create and Use Memory Object Stream (Python) +DESCRIPTION: Demonstrates creating a memory object stream for producer-consumer patterns. It shows how to send objects through the stream and receive them asynchronously, with tasks processing items from the stream. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import create_task_group, create_memory_object_stream, run +from anyio.streams.memory import MemoryObjectReceiveStream + + +async def process_items(receive_stream: MemoryObjectReceiveStream[str]) -> None: + async with receive_stream: + async for item in receive_stream: + print('received', item) + + +async def main(): + # The [str] specifies the type of the objects being passed through the + # memory object stream. This is a bit of trick, as create_memory_object_stream + # is actually a class masquerading as a function. + send_stream, receive_stream = create_memory_object_stream[str]() + async with create_task_group() as tg: + tg.start_soon(process_items, receive_stream) + async with send_stream: + for num in range(10): + await send_stream.send(f'number {num}') + +run(main) +``` + +-------------------------------- + +TITLE: Use start_blocking_portal as context manager in AnyIO 3 +DESCRIPTION: Demonstrates the required usage of `start_blocking_portal` as a context manager in AnyIO 3. This change affects how blocking operations are managed within AnyIO. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_9 + +LANGUAGE: Python +CODE: +``` +from anyio import sleep +from anyio.from_thread import start_blocking_portal + +with start_blocking_portal() as portal: + portal.call(sleep, 1) +``` + +-------------------------------- + +TITLE: Open Signal Receiver (AnyIO) +DESCRIPTION: Provides an alternative mechanism for handling signals using a context manager. This allows for more flexible signal handling compared to asyncio's add_signal_handler. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_16 + +LANGUAGE: Python +CODE: +``` +async with anyio.open_signal_receiver(signalnums: Iterable[int]) as signals: +``` + +-------------------------------- + +TITLE: Create Unix Domain Socket Listener +DESCRIPTION: Creates a listener for Unix domain sockets, binding to a specified file path. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_67 + +LANGUAGE: Python +CODE: +``` +listener = await anyio.create_unix_listener('/tmp/my_server.sock') +``` + +-------------------------------- + +TITLE: Test Echo Server Using Ephemeral Port Discovery +DESCRIPTION: An asynchronous test function that demonstrates setting up an echo server and testing a client connection without explicitly using a free port fixture. It creates a TCP listener, retrieves the assigned ephemeral port, and then connects a client to it. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_8 + +LANGUAGE: python +CODE: +``` +from anyio import connect_tcp, create_task_group, create_tcp_listener +from anyio.abc import SocketAttribute, SocketStream + +async def test_echo() -> None: + async def handle(client_stream: SocketStream) -> None: + async with client_stream: + payload = await client_stream.receive() + await client_stream.send(payload[::-1]) + + async with ( + await create_tcp_listener(local_host="127.0.0.1") as listener, + create_task_group() as tg + ): + tg.start_soon(listener.serve, handle) + port = listener.extra(SocketAttribute.local_port) + + async with await connect_tcp("127.0.0.1", port) as stream: + await stream.send(b"hello") + assert await stream.receive() == b"olleh" + + tg.cancel_scope.cancel() +``` + +-------------------------------- + +TITLE: Temporary Directory Context Manager +DESCRIPTION: Provides a context manager for temporary directories, ensuring they are created and automatically removed along with their contents upon exiting the context. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_39 + +LANGUAGE: Python +CODE: +``` +async with await anyio.TemporaryDirectory() as temp_dir: + print(f'Using temp dir: {temp_dir}') +``` + +-------------------------------- + +TITLE: Task Information and Management +DESCRIPTION: Includes utilities for inspecting and managing tasks, such as retrieving task information, listing running tasks, and waiting for tasks to become blocked. Also includes a pytest plugin for managing free ports. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_77 + +LANGUAGE: Python +CODE: +``` +anyio.TaskInfo +anyio.pytest_plugin.FreePortFactory +anyio.get_current_task +anyio.get_running_tasks +anyio.wait_all_tasks_blocked +``` + +-------------------------------- + +TITLE: Handle Specific Exceptions in AnyIO TaskGroup +DESCRIPTION: Demonstrates how to use the `catch` context manager within an AnyIO task group to handle specific exceptions like `ValueError` and `KeyError`. Each exception type can be associated with a dedicated handler function. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tasks.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +import anyio +from anyio import create_task_group, catch + +def handle_valueerror(excgroup: ExceptionGroup) -> None: + # handle each ValueError + pass + +def handle_keyerror(excgroup: ExceptionGroup) -> None: + # handle each KeyError + pass + +async def some_task(): + pass + +async def another_task(): + pass + +async def main(): + with catch({ + ValueError: handle_valueerror, + KeyError: handle_keyerror + }): + async with create_task_group() as tg: + tg.start_soon(some_task) + tg.start_soon(another_task) +``` + +-------------------------------- + +TITLE: Configure AnyIO Backend in conftest.py (Python) +DESCRIPTION: Customize the AnyIO backend and its options for tests by defining the `anyio_backend` fixture in your `conftest.py` file. This allows specifying a default backend or multiple backends with specific configurations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_2 + +LANGUAGE: python +CODE: +``` +import pytest + + +@pytest.fixture +def anyio_backend(): + return 'asyncio' +``` + +LANGUAGE: python +CODE: +``` +import pytest + + +@pytest.fixture(params=[ + pytest.param(('asyncio', {'use_uvloop': True}), id='asyncio+uvloop'), + pytest.param(('asyncio', {'use_uvloop': False}), id='asyncio'), + pytest.param(('trio', {'restrict_keyboard_interrupt_to_checkpoints': True}), id='trio') +]) +def anyio_backend(request): + return request.param +``` + +LANGUAGE: python +CODE: +``` +import pytest + + +@pytest.mark.parametrize('anyio_backend', ['asyncio']) +async def test_on_asyncio_only(anyio_backend): + ... +``` + +-------------------------------- + +TITLE: Context Manager Mix-in +DESCRIPTION: A mix-in class that provides the standard Python context manager protocol (`__enter__`, `__exit__`) for classes. It simplifies the creation of context managers. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_40 + +LANGUAGE: Python +CODE: +``` +class MyContextManager(anyio.ContextManagerMixin): + def __enter__(self): + return self + def __exit__(self, exc_type, exc_val, exc_tb): + pass +``` + +-------------------------------- + +TITLE: Fix TaskGroup.start() Cancellation Responsiveness on Asyncio +DESCRIPTION: Addresses an issue where `TaskGroup.start()` on the asyncio backend did not properly respond to external cancellation requests. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_92 + +LANGUAGE: python +CODE: +``` +Fixed ``TaskGroup.start()`` on asyncio not responding to cancellation from the outside +``` + +-------------------------------- + +TITLE: Using Custom Event Loop Factory with AnyIO +DESCRIPTION: Shows how to configure AnyIO's `run` function to use a custom event loop factory, such as one provided by `uvloop`. This replaces the older method of specifying an event loop policy, aligning with AnyIO 4's approach. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +anyio.run(main, backend_options={"loop_factory": uvloop.new_event_loop}) +``` + +-------------------------------- + +TITLE: Subclassing ContextManagerMixin with Superclass Call +DESCRIPTION: Demonstrates how a subclass can inherit from and call the implementation of a parent class that uses ContextManagerMixin. The `with super().__contextmanager__():` syntax ensures the parent's context logic is executed. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contextmanagers.rst#_snippet_3 + +LANGUAGE: python +CODE: +``` +from collections.abc import Generator +from contextlib import contextmanager +from typing import Self + +from anyio import ContextManagerMixin + +class SuperclassContextManager(ContextManagerMixin): + @contextmanager + def __contextmanager__(self) -> Generator[Self]: + print("superclass entered") + try: + yield self + finally: + print("superclass exited") + + +class SubclassContextManager(SuperclassContextManager): + @contextmanager + def __contextmanager__(self) -> Generator[Self]: + print("subclass entered") + try: + with super().__contextmanager__(): + yield self + finally: + +``` + +-------------------------------- + +TITLE: Create UNIX Socket Listener (AnyIO) +DESCRIPTION: Creates a listener for UNIX domain sockets using a file path and serves incoming client connections. This is used for local IPC on UNIX-like systems. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +from anyio import create_unix_listener, run + + +async def handle(client): + async with client: + name = await client.receive(1024) + await client.send(b'Hello, %s\n' % name) + + +async def main(): + listener = await create_unix_listener('/tmp/mysock') + await listener.serve(handle) + +run(main) +``` + +-------------------------------- + +TITLE: Connect via TCP +DESCRIPTION: Establishes a TCP connection to a specified host and port. Returns a byte stream for communication. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_64 + +LANGUAGE: Python +CODE: +``` +stream = await anyio.connect_tcp('example.com', 80) +``` + +-------------------------------- + +TITLE: Temporary File Context Manager +DESCRIPTION: Provides a context manager for temporary files, ensuring they are properly created, used, and deleted. Supports both named and unnamed temporary files. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_37 + +LANGUAGE: Python +CODE: +``` +async with await anyio.TemporaryFile() as temp_file: + await temp_file.write(b'content') +async with await anyio.NamedTemporaryFile() as named_temp_file: + await named_temp_file.write(b'named content') +``` + +-------------------------------- + +TITLE: Create a Temporary Directory Asynchronously +DESCRIPTION: Creates a temporary directory asynchronously. The directory and its contents are automatically removed when the returned context manager exits. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_35 + +LANGUAGE: Python +CODE: +``` +async with await anyio.mkdtemp() as temp_dir_path: + print(f'Created temporary directory: {temp_dir_path}') +``` + +-------------------------------- + +TITLE: Process Management +DESCRIPTION: Provides functions to run and open subprocesses, allowing for external command execution and interaction. It also defines the abstract base class for processes. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_73 + +LANGUAGE: Python +CODE: +``` +anyio.run_process +anyio.open_process +anyio.abc.Process +``` + +-------------------------------- + +TITLE: Asynchronous Context Manager Mix-in +DESCRIPTION: A mix-in class that provides the asynchronous context manager protocol (`__aenter__`, `__aexit__`) for classes. It simplifies the creation of asynchronous context managers. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_41 + +LANGUAGE: Python +CODE: +``` +class MyAsyncContextManager(anyio.AsyncContextManagerMixin): + async def __aenter__(self): + return self + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass +``` + +-------------------------------- + +TITLE: Create and Use SpooledTemporaryFile Asynchronously +DESCRIPTION: Shows how to asynchronously create and use a SpooledTemporaryFile. This is useful for small data kept in memory. It writes content, seeks, and reads it back. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tempfile.rst#_snippet_2 + +LANGUAGE: python +CODE: +``` +from anyio import SpooledTemporaryFile, run + +async def main(): + async with SpooledTemporaryFile(max_size=1024, mode="w+") as f: + await f.write("Spooled temp file content") + await f.seek(0) + print(await f.read()) + +run(main) +``` + +-------------------------------- + +TITLE: Compatibility Helper for Collapsing Exception Groups +DESCRIPTION: Provides a Python context manager to maintain compatibility between AnyIO 3 and AnyIO 4 regarding exception handling. This helper unwraps single-exception `BaseExceptionGroup` instances, allowing older code expecting direct exceptions to function correctly. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +import sys +from contextlib import contextmanager +from typing import Generator + +has_exceptiongroups = True +if sys.version_info < (3, 11): + try: + from exceptiongroup import BaseExceptionGroup + except ImportError: + has_exceptiongroups = False + + +@contextmanager +def collapse_excgroups() -> Generator[None, None, None]: + try: + yield + except BaseException as exc: + if has_exceptiongroups: + while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1: + exc = exc.exceptions[0] + + raise exc +``` + +-------------------------------- + +TITLE: Create TCP Listener (AnyIO) +DESCRIPTION: Creates a TCP listener on a specified local port and serves incoming client connections. It uses `create_tcp_listener` and the `serve` method from AnyIO. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import create_tcp_listener, run + + +async def handle(client): + async with client: + name = await client.receive(1024) + await client.send(b'Hello, %s\n' % name) + + +async def main(): + listener = await create_tcp_listener(local_port=1234) + await listener.serve(handle) + +run(main) +``` + +-------------------------------- + +TITLE: Create a Task Group +DESCRIPTION: Creates a task group, which is a context manager for spawning and managing concurrent asynchronous tasks. All tasks within a group are guaranteed to finish before the group exits. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_16 + +LANGUAGE: Python +CODE: +``` +async with anyio.create_task_group() as tg: + await tg.spawn(some_async_function) + await tg.spawn(another_async_function) +``` + +-------------------------------- + +TITLE: Write Bytes to File Asynchronously with AnyIO Path +DESCRIPTION: Demonstrates creating a file and writing binary content to it asynchronously using AnyIO's `Path` class. The `write_bytes` operation is awaited as it performs disk I/O in a worker thread. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/fileio.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +from anyio import Path, run + + +async def main(): + path = Path('/foo/bar') + await path.write_bytes(b'hello, world') + +run(main) +``` + +-------------------------------- + +TITLE: Add anyio.Path.is_junction() and anyio.Path.walk() Methods +DESCRIPTION: New methods `anyio.Path.is_junction()` and `anyio.Path.walk()` have been added to the `anyio.Path` object, providing enhanced file system path manipulation capabilities. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_85 + +LANGUAGE: python +CODE: +``` +Added the ``anyio.Path.is_junction()`` and ``anyio.Path.walk()`` methods +``` + +-------------------------------- + +TITLE: Implement Async Context Manager with Nested Task Group +DESCRIPTION: This Python code demonstrates how to create a custom async context manager using `AsyncContextManagerMixin`. It shows the manual implementation of the `__asynccontextmanager__` method, which yields the context manager instance after launching tasks within an `anyio.create_task_group`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +from __future__ import annotations + +from collections.abc import AsyncGenerator +from typing import Self + +from anyio import AsyncContextManagerMixin, create_task_group + + +class MyAsyncContextManager(AsyncContextManagerMixin): + @asynccontextmanager + async def __asynccontextmanager__(self) -> AsyncGenerator[Self]: + async with create_task_group() as tg: + ... # launch tasks + yield self +``` + +-------------------------------- + +TITLE: Synchronous Context Manager with ContextManagerMixin +DESCRIPTION: Demonstrates how to create a synchronous context manager using AnyIO's ContextManagerMixin. It utilizes the @contextmanager decorator on the __contextmanager__ method to define the context entry and exit logic. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contextmanagers.rst#_snippet_0 + +LANGUAGE: python +CODE: +``` +from collections.abc import Generator +from contextlib import contextmanager +from typing import Self + +from anyio import ContextManagerMixin + +class MyContextManager(ContextManagerMixin): + @contextmanager + def __contextmanager__(self) -> Generator[Self]: + print("entering context") + yield self + print("exiting context") +``` + +-------------------------------- + +TITLE: Connect to TLS Server with AnyIO +DESCRIPTION: Establishes a TLS connection to a localhost server on port 1234. It demonstrates loading custom CA certificates for verification and sending/receiving data. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_8 + +LANGUAGE: Python +CODE: +``` +import ssl +from anyio import connect_tcp, run + +async def main(): + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + context.load_verify_locations(cafile='cert.pem') + + async with await connect_tcp('localhost', 1234, ssl_context=context) as client: + await client.send(b'Client\n') + response = await client.receive() + print(response) + +run(main) +``` + +-------------------------------- + +TITLE: Cross-compatible Event creation for AnyIO 2 and 3 +DESCRIPTION: Provides a method for creating `Event` objects that works with both AnyIO 2 and AnyIO 3, using a try-except block to handle the different import mechanisms. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_11 + +LANGUAGE: Python +CODE: +``` +try: + from anyio import Event + create_event = Event +except ImportError: + from anyio.abc import Event + create_event = lambda: Event() +``` + +-------------------------------- + +TITLE: Add stdin argument to anyio.run_process() +DESCRIPTION: Enhances `anyio.run_process()` by adding a `stdin` argument, similar to `anyio.open_process()`, `asyncio.create_subprocess_…()`, `trio.run_process()`, and `subprocess.run()`. This allows providing standard input to the subprocess. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_15 + +LANGUAGE: Python +CODE: +``` +import anyio + +# async def run_with_stdin(): +# result = await anyio.run_process(['cat'], stdin=b'hello') +# print(result.stdout) + +``` + +-------------------------------- + +TITLE: Use maybe_async for Event.set in AnyIO 3 +DESCRIPTION: Demonstrates how to use `maybe_async` to handle the change where `Event.set()` is no longer awaitable in AnyIO 3. This ensures compatibility with older versions while adapting to the new API. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +from anyio.abc import Event +from anyio import maybe_async + + +async def foo(event: Event): + await maybe_async(event.set()) + ... +``` + +-------------------------------- + +TITLE: Connect via Unix Domain Socket +DESCRIPTION: Establishes a connection to a Unix domain socket. This is typically used for inter-process communication on Unix-like systems. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_65 + +LANGUAGE: Python +CODE: +``` +stream = await anyio.connect_unix('/tmp/mysocket.sock') +``` + +-------------------------------- + +TITLE: Share Blocking Portal with BlockingPortalProvider +DESCRIPTION: Demonstrates using `BlockingPortalProvider` to efficiently share a single blocking portal across multiple threads for on-demand asynchronous calls. This avoids the overhead of creating a new portal for each synchronous API call. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_10 + +LANGUAGE: Python +CODE: +``` +from anyio.from_thread import BlockingPortalProvider + +class MyAPI: + def __init__(self, async_obj) -> None: + self._async_obj = async_obj + self._portal_provider = BlockingPortalProvider() + + def do_stuff(self) -> None: + with self._portal_provider as portal: + portal.call(self._async_obj.do_async_stuff) +``` + +-------------------------------- + +TITLE: Use Asynchronous Fixtures with AnyIO (Python) +DESCRIPTION: Define asynchronous fixtures using `async def` and the `pytest.mark.anyio` marker or by depending on the `anyio_backend` fixture. This is useful for setting up and tearing down asynchronous resources for tests. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_3 + +LANGUAGE: python +CODE: +``` +import pytest + +pytestmark = pytest.mark.anyio + + +@pytest.fixture +async def server(): + server = await setup_server() + yield server + await server.shutdown() + + +async def test_server(server): + result = await server.do_something() + assert result == 'foo' +``` + +LANGUAGE: python +CODE: +``` +import pytest + + +@pytest.fixture(autouse=True) +async def server(anyio_backend): + server = await setup_server() + yield + await server.shutdown() + + +async def test_server(): + result = await client.do_something_on_the_server() + assert result == 'foo' +``` + +-------------------------------- + +TITLE: Read File Asynchronously with AnyIO +DESCRIPTION: Demonstrates how to open a file asynchronously using `anyio.open_file`, read its contents, and print them. This operation is performed in a worker thread to avoid blocking the event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/fileio.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import open_file, run + + +async def main(): + async with await open_file('/some/path/somewhere') as f: + contents = await f.read() + print(contents) + +run(main) +``` + +-------------------------------- + +TITLE: Create AnyIO Event +DESCRIPTION: Demonstrates how to create an AnyIO event. This function is part of the core AnyIO library and handles asynchronous event synchronization. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_12 + +LANGUAGE: Python +CODE: +``` +from anyio import create_event +from anyio.abc import Event + +async def foo() -> Event: + return create_event() +``` + +-------------------------------- + +TITLE: Signal Handling +DESCRIPTION: Enables the opening of signal receivers to handle operating system signals asynchronously. This is important for graceful shutdown and signal-based control flow. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_75 + +LANGUAGE: Python +CODE: +``` +anyio.open_signal_receiver +``` + +-------------------------------- + +TITLE: Add BlockingPortalProvider Class +DESCRIPTION: Introduces the 'BlockingPortalProvider' class, designed to simplify the creation of synchronous counterparts for asynchronous interfaces that would otherwise necessitate multiple blocking portals. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_59 + +LANGUAGE: Python +CODE: +``` +import anyio + +class MyAsyncService: + async def do_something(self): + await anyio.sleep(0.1) + return "Async result" + +# Using BlockingPortalProvider to create a sync interface +class MySyncService: + def __init__(self): + self._portal_provider = anyio.BlockingPortalProvider() + self._async_service = self._portal_provider.wrap_async(MyAsyncService()) + + def do_something_sync(self): + return self._async_service.do_something() + +# Example usage: +# sync_service = MySyncService() +# result = sync_service.do_something_sync() +# print(result) +``` + +-------------------------------- + +TITLE: Use maybe_async_cm for CancelScope in AnyIO 3 +DESCRIPTION: Shows how to use `maybe_async_cm` for context managers like `CancelScope` that have transitioned from async to regular context managers in AnyIO 3. This allows for compatible usage across AnyIO versions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +from anyio import CancelScope, maybe_async_cm + +async def foo(): + async with maybe_async_cm(CancelScope()) as scope: + ... +``` + +-------------------------------- + +TITLE: Convert to Connectable +DESCRIPTION: Converts various network address representations into a connectable object, which can then be used to establish connections. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_63 + +LANGUAGE: Python +CODE: +``` +connectable = anyio.as_connectable(('localhost', 8080)) +``` + +-------------------------------- + +TITLE: Handle Multiple Exceptions with except* in Python +DESCRIPTION: Illustrates how to catch multiple exceptions raised within a task group using the `except*` syntax available in Python 3.11+. This allows handling different exception types that might occur concurrently. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tasks.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from anyio import create_task_group + +try: + async with create_task_group() as tg: + tg.start_soon(some_task) + tg.start_soon(another_task) +except* ValueError as excgroup: + for exc in excgroup.exceptions: + ... # handle each ValueError +except* KeyError as excgroup: + for exc in excgroup.exceptions: + ... # handle each KeyError +``` + +-------------------------------- + +TITLE: Wrap Async Context Manager with Blocking Portal +DESCRIPTION: Demonstrates how to wrap an asynchronous context manager into a synchronous one using `BlockingPortal.wrap_async_context_manager`. This allows using async context managers within synchronous code blocks managed by AnyIO's blocking portal. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +from anyio.from_thread import start_blocking_portal + + +class AsyncContextManager: + async def __aenter__(self): + print('entering') + + async def __aexit__(self, exc_type, exc_val, exc_tb): + print('exiting with', exc_type) + + +async_cm = AsyncContextManager() +with start_blocking_portal() as portal, portal.wrap_async_context_manager(async_cm): + print('inside the context manager block') +``` + +-------------------------------- + +TITLE: Provide free TCP/UDP port fixtures for pytest +DESCRIPTION: Adds four new fixtures to the AnyIO pytest plugin for easily obtaining unused TCP and UDP ports. These fixtures (`free_tcp_port_factory`, `free_udp_port_factory`, `free_tcp_port`, `free_udp_port`) simplify network testing by providing reliable access to available ports. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_14 + +LANGUAGE: Python +CODE: +``` +pytest_plugins = ['anyio'] + +def test_with_free_tcp_port(free_tcp_port): + assert free_tcp_port > 0 + +def test_with_port_factory(free_tcp_port_factory): + port1 = free_tcp_port_factory() + port2 = free_tcp_port_factory() + assert port1 != port2 + +``` + +-------------------------------- + +TITLE: Creating Type Annotated Memory Object Streams (AnyIO 4) +DESCRIPTION: Demonstrates the updated syntax for creating type-annotated memory object streams in AnyIO 4. The `create_memory_object_stream` function is now a generic class, requiring type parameters to be specified using square brackets. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +send, receive = create_memory_object_stream[int](100) +``` + +-------------------------------- + +TITLE: Configure Pytest for AnyIO (TOML) +DESCRIPTION: Configure pytest to automatically detect and run AnyIO tests by setting `anyio_mode = "auto"` in the pytest configuration file. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_0 + +LANGUAGE: toml +CODE: +``` +[tool.pytest.ini_options] +anyio_mode = "auto" +``` + +-------------------------------- + +TITLE: OpenSSL Command for Self-Signed Certificate +DESCRIPTION: Provides the bash command to generate a self-signed X.509 certificate and a private key using OpenSSL, suitable for local development and testing TLS connections. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_5 + +LANGUAGE: Bash +CODE: +``` +openssl req -x509 -newkey rsa:2048 -subj '/CN=localhost' -keyout key.pem -out cert.pem -nodes -days 365 +``` + +-------------------------------- + +TITLE: Receive File Descriptor via UNIX Socket (AnyIO Client) +DESCRIPTION: A client that connects to a UNIX socket, receives a file descriptor, opens it using `os.fdopen`, and prints its content. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +import os + +from anyio import connect_unix, run + + +async def main(): + async with await connect_unix('/tmp/mysock') as client: + _, fds = await client.receive_fds(0, 1) + with os.fdopen(fds[0]) as file: + print(file.read()) + +run(main) +``` + +-------------------------------- + +TITLE: Add async support for temporary file handling +DESCRIPTION: Introduces asynchronous support for temporary file operations, allowing temporary files to be created and managed within an async context. This integrates seamlessly with AnyIO's asynchronous I/O model. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_13 + +LANGUAGE: Python +CODE: +``` +import anyio + +# async def manage_temp_file(): +# async with await anyio.tempfile.TemporaryFile() as tmp: +# await tmp.write(b'some data') +# await tmp.seek(0) +# content = await tmp.read() +# print(content) + +``` + +-------------------------------- + +TITLE: Text Stream Wrappers +DESCRIPTION: Provides stream wrappers for handling text data, including encoding and decoding. These simplify working with text over byte-oriented protocols. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_59 + +LANGUAGE: Python +CODE: +``` +text_receive_stream = anyio.streams.text.TextReceiveStream(byte_stream) +text_send_stream = anyio.streams.text.TextSendStream(byte_stream) +``` + +-------------------------------- + +TITLE: Create and Use NamedTemporaryFile Asynchronously +DESCRIPTION: Illustrates the asynchronous creation and use of a NamedTemporaryFile. It prints the file's name, writes content, seeks to the beginning, and reads the content. The file is deleted upon closure if delete=True. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tempfile.rst#_snippet_1 + +LANGUAGE: python +CODE: +``` +from anyio import NamedTemporaryFile, run + +async def main(): + async with NamedTemporaryFile(mode="w+", delete=True) as f: + print(f"Temporary file name: {f.name}") + await f.write("Named temp file content") + await f.seek(0) + print(await f.read()) + +run(main) +``` + +-------------------------------- + +TITLE: Wrap Synchronous File Object Asynchronously with AnyIO +DESCRIPTION: Illustrates how to wrap an existing synchronous file object into an asynchronous one using `anyio.wrap_file`. This allows using asynchronous iteration with files opened using the standard `open()` function. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/fileio.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from anyio import wrap_file, run + + +async def main(): + with open('/some/path/somewhere') as f: + async for line in wrap_file(f): + print(line, end='') + +run(main) +``` + +-------------------------------- + +TITLE: Create a Temporary File Asynchronously +DESCRIPTION: Creates a temporary file asynchronously, returning its path and an asynchronous file object. The file is automatically deleted when closed. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_34 + +LANGUAGE: Python +CODE: +``` +async with await anyio.mkstemp() as (file, path): + await file.write(b'temp data') +``` + +-------------------------------- + +TITLE: Instantiate Event and CapacityLimiter outside event loop +DESCRIPTION: Allows the Event and CapacityLimiter classes to be instantiated without requiring an active event loop thread, providing more flexibility in their usage. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_68 + +LANGUAGE: Python +CODE: +``` +from anyio import Event, CapacityLimiter + +event = Event() +limiter = CapacityLimiter(10) +``` + +-------------------------------- + +TITLE: BlockingPortal Spawn Task and Wrap Context Manager +DESCRIPTION: Adds `spawn_task()` and `wrap_async_context_manager()` methods to `BlockingPortal`. These provide utilities for managing tasks and context managers in a blocking manner. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_187 + +LANGUAGE: Python +CODE: +``` +anyio.BlockingPortal.spawn_task() +``` + +LANGUAGE: Python +CODE: +``` +anyio.BlockingPortal.wrap_async_context_manager() +``` + +-------------------------------- + +TITLE: Asynchronous Path Operations +DESCRIPTION: Provides an asynchronous interface for path manipulation and file system operations, similar to `pathlib.Path` but with async methods. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_33 + +LANGUAGE: Python +CODE: +``` +async_path = anyio.Path('my_directory') +await async_path.mkdir() +await async_path.unlink() +``` + +-------------------------------- + +TITLE: Update type annotations for run_process() and open_process() +DESCRIPTION: Updates type annotations for run_process() and open_process(). 'command' now accepts bytes and sequences of bytes, and 'stdin', 'stdout', 'stderr' accept file-like objects. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_117 + +LANGUAGE: Python +CODE: +``` +from typing import Sequence, BinaryIO +from anyio import run_process, open_process + +# Example: +# run_process(['ls', '-l'], stdin=open('input.txt', 'rb')) +# run_process(command=['echo', 'hello'], stdout=...) # command can be bytes +``` + +-------------------------------- + +TITLE: Catching Cancellation Exceptions with get_cancelled_exc_class +DESCRIPTION: Shows how to specifically catch the cancellation exception raised by AnyIO using get_cancelled_exc_class. It emphasizes the importance of re-raising the caught exception for correct application behavior. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +from anyio import get_cancelled_exc_class + + +async def do_something(): + try: + await run_async_stuff() + except get_cancelled_exc_class(): + # (perform cleanup) + raise +``` + +-------------------------------- + +TITLE: Asynchronous Context Manager with AsyncContextManagerMixin +DESCRIPTION: Illustrates the creation of an asynchronous context manager using AnyIO's AsyncContextManagerMixin. The @asynccontextmanager decorator is applied to the __asynccontextmanager__ method for defining asynchronous context entry and exit. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contextmanagers.rst#_snippet_1 + +LANGUAGE: python +CODE: +``` +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from typing import Self + +from anyio import AsyncContextManagerMixin + +class MyAsyncContextManager(AsyncContextManagerMixin): + @asynccontextmanager + async def __asynccontextmanager__(self) -> AsyncGenerator[Self]: + print("entering context") + yield self + print("exiting context") +``` + +-------------------------------- + +TITLE: Catching Specific Exceptions from Task Groups (Python 3.11+) +DESCRIPTION: Demonstrates how to catch specific exceptions like ValueError when they are raised from a task group in AnyIO 4 using the `except*` syntax, which is available in Python 3.11 and later. This syntax allows for handling multiple exceptions within an ExceptionGroup. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +try: + await function_using_a_taskgroup() +except* ValueError as excgrp: + # Note: excgrp is an ExceptionGroup now! + ... +``` + +-------------------------------- + +TITLE: Shielding Operations with CancelScope +DESCRIPTION: Demonstrates how to use CancelScope with shield=True to protect operations from cancellation. This is often combined with move_on_after or fail_after for controlled timeouts during cleanup. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +async with create_task_group() as tg: + with CancelScope(shield=True) as scope: + tg.start_soon(external_task) + tg.cancel_scope.cancel() + print('Started sleeping in the host task') + await sleep(1) + print('Finished sleeping in the host task') +``` + +LANGUAGE: Python +CODE: +``` +async def do_something(resource): + try: + ... + except BaseException: + # Here we wait 10 seconds for resource.aclose() to complete, + # but if the operation doesn't complete within that period, we move on + # and re-raise the caught exception anyway + with move_on_after(10, shield=True): + await resource.aclose() + + raise +``` + +-------------------------------- + +TITLE: Mark Tests with pytest.mark.anyio (Python) +DESCRIPTION: Mark individual asynchronous test functions or entire modules/classes with the `pytest.mark.anyio` marker to enable AnyIO's test runner. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_1 + +LANGUAGE: python +CODE: +``` +import pytest + +pytestmark = pytest.mark.anyio + +async def test_something(): + ... +``` + +LANGUAGE: python +CODE: +``` +import pytest + + +async def test_something(anyio_backend): + ... +``` + +-------------------------------- + +TITLE: Wrap existing sockets as AnyIO streams +DESCRIPTION: Provides class methods to wrap existing sockets into AnyIO stream objects. This allows seamless integration of pre-existing socket resources with AnyIO's asynchronous stream abstractions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio.streams.uds import UNIXSocketStream +from anyio.streams.socket import SocketStream, UDPSocket, ConnectedUDPSocket +from anyio.abc import SocketListener + +# Example usage: +# existing_socket = ... # obtain an existing socket +# stream = SocketStream.from_socket(existing_socket) +# unix_stream = UNIXSocketStream.from_socket(existing_socket) +# udp_socket = UDPSocket.from_socket(existing_socket) +# connected_udp_socket = ConnectedUDPSocket.from_socket(existing_socket) +# listener = SocketListener.from_socket(existing_socket) + +``` + +-------------------------------- + +TITLE: Iterate File Lines Asynchronously with AnyIO +DESCRIPTION: Shows how to asynchronously iterate over a file line by line using `anyio.open_file` and an `async for` loop. This is useful for processing large files without loading them entirely into memory. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/fileio.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import open_file, run + + +async def main(): + async with await open_file('/some/path/somewhere') as f: + async for line in f: + print(line, end='') + +run(main) +``` + +-------------------------------- + +TITLE: Multi Listener for Streams +DESCRIPTION: A listener that can manage multiple underlying listeners, allowing a single point of entry to accept connections from various sources. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_56 + +LANGUAGE: Python +CODE: +``` +multi_listener = anyio.streams.stapled.MultiListener([listener1, listener2]) +``` + +-------------------------------- + +TITLE: Improved Asynchronous Context Manager with Task Group +DESCRIPTION: Shows a corrected implementation of an asynchronous context manager that properly handles task group cleanup. This version uses `async with create_task_group()` within the `__asynccontextmanager__` to ensure the task group is exited even if errors occur during context entry. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contextmanagers.rst#_snippet_2 + +LANGUAGE: python +CODE: +``` +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from typing import Self + +from anyio import AsyncContextManagerMixin, create_task_group + +class MyBetterContextManager(AsyncContextManagerMixin): + @asynccontextmanager + async def __asynccontextmanager__(self) -> AsyncGenerator[Self]: + async with create_task_group() as task_group: + # Still crashes, but at least now the task group is exited + task_group.start_soon(self.my_background_func) + yield self + + async my_background_func(self, arg: int) -> None: + ... +``` + +-------------------------------- + +TITLE: Add Path copy and move methods +DESCRIPTION: Includes `copy()`, `copy_into()`, `move()`, and `move_into()` methods for the `anyio.Path` class, available in Python 3.14. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_23 + +LANGUAGE: Python +CODE: +``` +Added support for the ``copy()``, ``copy_into()``, ``move()`` and ``move_into()`` + methods in ``anyio.Path``, available in Python 3.14 +``` + +-------------------------------- + +TITLE: Connect to UNIX Socket (AnyIO) +DESCRIPTION: Connects to a UNIX domain socket using a file path, sends data, and receives a response. This is an alternative to TCP for local inter-process communication. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from anyio import connect_unix, run + + +async def main(): + async with await connect_unix('/tmp/mysock') as client: + await client.send(b'Client\n') + response = await client.receive(1024) + print(response) + +run(main) +``` + +-------------------------------- + +TITLE: Pytest Fixture for AnyIO Backend +DESCRIPTION: A pytest fixture that specifies the AnyIO backend to be used for testing, typically returning 'asyncio'. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_5 + +LANGUAGE: python +CODE: +``` +def anyio_backend(): + return 'asyncio' +``` + +-------------------------------- + +TITLE: Add Python 3.13 Path Methods to anyio.Path +DESCRIPTION: Provides support for 'from_uri()', 'full_match()', and 'parser' methods/properties in anyio.Path, which were newly added in Python 3.13. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_48 + +LANGUAGE: Python +CODE: +``` +import anyio +import sys + +async def use_new_path_methods(uri): + if sys.version_info >= (3, 13): + p = await anyio.Path.from_uri(uri) + print(f"Parsed path: {p}") + # Assuming 'parser' is a property or method that can be accessed + # print(f"Parser: {p.parser}") + # Assuming 'full_match' is a method + # if await p.full_match("/some/pattern"): + # print("Full match found.") + else: + print("Python 3.13 path methods are not available on this version.") + +# Example usage: +# await use_new_path_methods("file:///path/to/file") +``` + +-------------------------------- + +TITLE: AnyIO Task Cancellation Before Resuming +DESCRIPTION: Illustrates how a task in AnyIO can process the result of an await before being cancelled. This is shown by setting a future's result and then cancelling the task group, ensuring the 'hello' message is printed. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +import asyncio + +import anyio + + +async def receive(f): + print(await f) + await asyncio.sleep(1) + print("The task will be cancelled before this is printed") + + +async def main(): + f = asyncio.get_running_loop().create_future() + async with anyio.create_task_group() as tg: + tg.start_soon(receive, f) + await asyncio.sleep(0) # make sure the task has started + f.set_result("hello") + tg.cancel_scope.cancel() + + +# Output: "hello" +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: Python AsyncContextManagerMixin Subclassing +DESCRIPTION: This snippet demonstrates subclassing AnyIO's AsyncContextManagerMixin. It shows how to create nested asynchronous context managers, ensuring proper entry and exit messages are printed for both the superclass and subclass contexts. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/contextmanagers.rst#_snippet_4 + +LANGUAGE: python +CODE: +``` +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from typing import Self + +from anyio import AsyncContextManagerMixin + +class SuperclassContextManager(AsyncContextManagerMixin): + @asynccontextmanager + async def __asynccontextmanager__(self) -> AsyncGenerator[Self]: + print("superclass entered") + try: + yield self + finally: + print("superclass exited") + + +class SubclassContextManager(SuperclassContextManager): + @asynccontextmanager + async def __asynccontextmanager__(self) -> AsyncGenerator[Self]: + print("subclass entered") + try: + async with super().__asynccontextmanager__(): + yield self + finally: + print("subclass exited") +``` + +-------------------------------- + +TITLE: Improve Type Annotations for PathLike in run_process/open_process +DESCRIPTION: Enhances type annotations and support for 'PathLike' arguments in 'run_process()' and 'open_process()', allowing path-like objects similar to 'subprocess.Popen'. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_50 + +LANGUAGE: Python +CODE: +``` +import anyio +from pathlib import Path + +async def run_with_pathlike(): + path_obj = Path("/path/to/executable") + try: + # Using a Path object as an argument + await anyio.run_process(path_obj, "arg1") + except Exception as e: + print(f"Error: {e}") + +async def open_with_pathlike(): + path_obj = Path("/path/to/executable") + try: + # Using a Path object as an argument + process = await anyio.open_process(path_obj) + await process.wait() + except Exception as e: + print(f"Error: {e}") +``` + +-------------------------------- + +TITLE: Allow EventAdapter set() before binding +DESCRIPTION: Modifies `EventAdapter` to permit calling `set()` even before an async backend is bound to it. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_29 + +LANGUAGE: Python +CODE: +``` +Changed ``EventAdapter`` (an ``Event`` with no bound async backend) to allow ``set()`` + to work even before an async backend is bound to it + (`#819 `_) +``` + +-------------------------------- + +TITLE: Create context manager mix-ins for AnyIO +DESCRIPTION: Introduces `anyio.ContextManagerMixin` and `anyio.AsyncContextManagerMixin` to simplify the creation of classes that embed other context managers, such as cancel scopes or task groups. This aids in managing resource lifecycles within asynchronous operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +import anyio + +class MyAsyncContextManager(anyio.AsyncContextManagerMixin): + async def __aenter__(self): + # enter logic + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + # exit logic + pass + +# Example usage: +# async with MyAsyncContextManager(): +# pass + +``` + +-------------------------------- + +TITLE: Run One-Shot Command in Shell (Python) +DESCRIPTION: Executes an external command using the default shell. This is equivalent to `shell=True` in Python's `subprocess.run`. The command is provided as a string. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/subprocesses.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import run_process, run + + +async def main(): + result = await run_process('ps') + print(result.stdout.decode()) + +run(main) +``` + +-------------------------------- + +TITLE: Asyncio StreamWriter get_extra_info +DESCRIPTION: The get_extra_info method in asyncio's StreamWriter returns a dictionary, making type checking difficult and requiring manual key access for retrieving information like remote addresses. AnyIO offers a more type-safe approach. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_12 + +LANGUAGE: Python +CODE: +``` +writer.get_extra_info('socket') +``` + +-------------------------------- + +TITLE: Add Support for Hypothesis +DESCRIPTION: Adds built-in support for the Hypothesis library. This integration allows for property-based testing of AnyIO code, enabling the discovery of edge cases and potential bugs through randomized testing. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_236 + +LANGUAGE: Python +CODE: +``` +import pytest +import hypothesis +import anyio + +@hypothesis.given(data=hypothesis.strategies.integers()) +@pytest.mark.anyio +async def test_integer_processing(data): + # Test logic using Hypothesis and AnyIO +``` + +-------------------------------- + +TITLE: Memory Object Stream Implementations +DESCRIPTION: Provides concrete implementations for memory-based object streams, used for efficient in-process communication between tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_54 + +LANGUAGE: Python +CODE: +``` +send_stream = anyio.streams.memory.MemoryObjectSendStream() +receive_stream = anyio.streams.memory.MemoryObjectReceiveStream(send_stream) +``` + +-------------------------------- + +TITLE: Catching Specific Exceptions from Task Groups (with exceptiongroup backport) +DESCRIPTION: Shows how to handle specific exceptions raised from AnyIO 4 task groups using the `exceptiongroup` backport library for compatibility with Python versions older than 3.11. It utilizes the `catch` context manager to specify how different exception types should be handled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from exceptiongroup import ExceptionGroup, catch + +def handle_value_errors(excgrp: ExceptionGroup) -> None: + ... + +with catch({ValueError: handle_value_errors}): + await function_using_a_taskgroup() +``` + +-------------------------------- + +TITLE: Synchronization Primitives +DESCRIPTION: Offers various synchronization primitives for managing concurrent tasks, including Events, Locks, Conditions, Semaphores, and Capacity Limiters. Statistics classes are also provided for monitoring these primitives. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_74 + +LANGUAGE: Python +CODE: +``` +anyio.Event +anyio.Lock +anyio.Condition +anyio.Semaphore +anyio.CapacityLimiter +anyio.ResourceGuard +anyio.LockStatistics +anyio.EventStatistics +anyio.ConditionStatistics +anyio.CapacityLimiterStatistics +anyio.SemaphoreStatistics +``` + +-------------------------------- + +TITLE: Create Blocking Portal within Running Event Loop (AnyIO) +DESCRIPTION: Shows how to create a `BlockingPortal` instance when an event loop is already running. This allows handing off the portal to external threads, enabling them to interact with the active event loop. The portal is managed using an asynchronous context manager. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +from anyio import run +from anyio.from_thread import BlockingPortal + + +async def main(): + async with BlockingPortal() as portal: + # ...hand off the portal to external threads... + await portal.sleep_until_stopped() + +run(main) +``` + +-------------------------------- + +TITLE: Add wrap_file() Function +DESCRIPTION: Introduces the ``wrap_file()`` function, which allows wrapping existing files to be used as asynchronous file objects within AnyIO. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_137 + +LANGUAGE: Python +CODE: +``` +Added the ``wrap_file()`` function for wrapping existing files as asynchronous file + objects +``` + +-------------------------------- + +TITLE: Make TaskStatus a generic protocol +DESCRIPTION: The TaskStatus class is now a generic protocol, allowing it to be parametrized to specify the type of value passed to task_status.started(). + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_103 + +LANGUAGE: Python +CODE: +``` +from typing import Generic, TypeVar +from anyio.abc import TaskStatus + +T = TypeVar('T') + +class MyTaskStatus(TaskStatus[T]): + def started(self, value: T) -> None: + pass +``` + +-------------------------------- + +TITLE: Open an Asynchronous File +DESCRIPTION: Opens a file asynchronously, allowing read and write operations without blocking the event loop. This is the asynchronous equivalent of Python's built-in `open()`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_30 + +LANGUAGE: Python +CODE: +``` +async with await anyio.open_file('myfile.txt', mode='w') as f: + await f.write('Hello, world!') +``` + +-------------------------------- + +TITLE: Connect to TCP Socket (AnyIO) +DESCRIPTION: Establishes a connection to a remote TCP socket, sends data, and receives a response. It uses the `connect_tcp` function from the AnyIO library. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import connect_tcp, run + + +async def main(): + async with await connect_tcp('hostname', 1234) as client: + await client.send(b'Client\n') + response = await client.receive() + print(response) + +run(main) +``` + +-------------------------------- + +TITLE: No-Wait Acquire Methods +DESCRIPTION: Adds `acquire_nowait()` methods to `Lock`, `Condition`, and `Semaphore`. These allow attempting to acquire a resource without blocking, returning immediately if the resource is unavailable. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_167 + +LANGUAGE: Python +CODE: +``` +anyio.Lock.acquire_nowait() +``` + +LANGUAGE: Python +CODE: +``` +anyio.Condition.acquire_nowait() +``` + +LANGUAGE: Python +CODE: +``` +anyio.Semaphore.acquire_nowait() +``` + +-------------------------------- + +TITLE: Generate Self-Signed Certificates with trustme for TLS +DESCRIPTION: Provides pytest fixtures to generate a Certificate Authority (CA) and issue server/client certificates using the trustme library. These are useful for testing TLS services. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_9 + +LANGUAGE: Python +CODE: +``` +import ssl +import pytest +import trustme + +@pytest.fixture(scope='session') +def ca(): + return trustme.CA() + +@pytest.fixture(scope='session') +def server_context(ca): + server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ca.issue_cert('localhost').configure_cert(server_context) + return server_context + +@pytest.fixture(scope='session') +def client_context(ca): + client_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + ca.configure_trust(client_context) + return client_context +``` + +-------------------------------- + +TITLE: Synchronous Send with Memory Object Stream (Python) +DESCRIPTION: Illustrates how to perform a synchronous send operation on a memory object stream using the `send_nowait` method within a context manager. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio.streams.memory import MemoryObjectSendStream + + +def synchronous_callback(send_stream: MemoryObjectSendStream[str]) -> None: + with send_stream: + send_stream.send_nowait('hello') +``` + +-------------------------------- + +TITLE: Use anyio.getaddrinfo() for Name Lookups +DESCRIPTION: Switches the library's internal name resolution mechanism to utilize `anyio.getaddrinfo()`. This change aims to provide a more consistent and potentially non-blocking approach to resolving network addresses across different backends. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_222 + +LANGUAGE: Python +CODE: +``` +import anyio + +# Example of how anyio.getaddrinfo might be used internally +async def resolve_address(hostname, port): + addresses = await anyio.getaddrinfo(hostname, port) + return addresses +``` + +-------------------------------- + +TITLE: Iterate Directory Contents Asynchronously with AnyIO Path +DESCRIPTION: Shows how to asynchronously iterate through the contents of a directory using AnyIO's `Path.iterdir()`. It checks if each item is a file and reads its text content asynchronously. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/fileio.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +from anyio import Path, run + + +async def main(): + # Print the contents of every file (assumed to be text) in the directory /foo/bar + dir_path = Path('/foo/bar') + async for path in dir_path.iterdir(): + if await path.is_file(): + print(await path.read_text()) + print('---------------------') + +run(main) +``` + +-------------------------------- + +TITLE: Define an Asynchronous Resource Interface +DESCRIPTION: Defines the interface for objects that can be asynchronously opened and closed. Resources implementing this interface are guaranteed to have an `aclose` method. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_8 + +LANGUAGE: Python +CODE: +``` +class MyAsyncResource(anyio.abc.AsyncResource): + async def aclose(self): + pass +``` + +-------------------------------- + +TITLE: TLS Stream Wrappers +DESCRIPTION: Provides stream wrappers for handling Transport Layer Security (TLS) connections, enabling secure communication over networks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_61 + +LANGUAGE: Python +CODE: +``` +tls_stream = anyio.streams.tls.TLSStream(wrapped_stream, ssl_context) +tls_listener = anyio.streams.tls.TLSListener(listener, ssl_context) +``` + +-------------------------------- + +TITLE: TLSListener Parameters +DESCRIPTION: Adds `handshake_timeout` and `error_handler` parameters to `TLSListener`, allowing for more configuration and error handling during TLS handshake. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_188 + +LANGUAGE: Python +CODE: +``` +anyio.TLSListener(handshake_timeout=..., error_handler=...) +``` + +-------------------------------- + +TITLE: Send File Descriptor via UNIX Socket (AnyIO Server) +DESCRIPTION: A server that creates a UNIX socket listener, accepts connections, and sends the file descriptor of an opened file to the client. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +from pathlib import Path + +from anyio import create_unix_listener, run + + +async def handle(client): + async with client: + with path.open('r') as file: + await client.send_fds(b'this message is ignored', [file]) + + +async def main(): + listener = await create_unix_listener('/tmp/mysock') + await listener.serve(handle) + +run(main) + +path = Path('/tmp/examplefile') +path.write_text('Test file') +run(main) +``` + +-------------------------------- + +TITLE: Create UDP Socket +DESCRIPTION: Creates a UDP socket, optionally binding it to a local address. UDP is a connectionless protocol suitable for broadcasting or unreliable data transfer. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_68 + +LANGUAGE: Python +CODE: +``` +udp_socket = await anyio +``` + +-------------------------------- + +TITLE: Asyncio StreamWriter Write and Drain +DESCRIPTION: In asyncio, writing data to a stream involves two steps: calling the synchronous write() method to buffer data and then awaiting the drain() coroutine to ensure the data is actually sent. This two-step process is a design limitation addressed by AnyIO. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_10 + +LANGUAGE: Python +CODE: +``` +writer.write(data) +await writer.drain() +``` + +-------------------------------- + +TITLE: Listener Interface +DESCRIPTION: Defines the interface for network listeners, which accept incoming connections. This is the server-side component for network services. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_46 + +LANGUAGE: Python +CODE: +``` +async def accept_connections(listener: anyio.abc.Listener): + while True: + client_stream, _ = await listener.accept() + asyncio.create_task(handle_client(client_stream)) +``` + +-------------------------------- + +TITLE: AnyIO Host Name Resolution (IDNA 2008) +DESCRIPTION: This snippet explains that for both asyncio and curio backends in AnyIO 2.0.0, host name resolution now uses IDNA 2008 with UTS 46 compatibility mapping, similar to trio, ensuring consistent internationalized domain name handling. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_205 + +LANGUAGE: Python +CODE: +``` +Host name resolution now uses `IDNA 2008`_ (with UTS 46 compatibility mapping, just like trio) +``` + +-------------------------------- + +TITLE: AnyIO Stream End-of-Stream Handling +DESCRIPTION: Unlike asyncio streams that return empty bytes or None, AnyIO streams raise an EndOfStream exception when no more data is available for reading. This provides a more explicit way to handle the end of a stream. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_9 + +LANGUAGE: Python +CODE: +``` +raise EndOfStream +``` + +-------------------------------- + +TITLE: Any Stream Type Aliases +DESCRIPTION: Provides type aliases for various AnyIO stream types, simplifying type hinting and improving code readability. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_48 + +LANGUAGE: Python +CODE: +``` +from typing import Union + +StreamType = Union[anyio.abc.ByteStream, anyio.abc.ObjectStream] +``` + +-------------------------------- + +TITLE: Implement Timeout with move_on_after in AnyIO +DESCRIPTION: Shows how to use `move_on_after` as a context manager to limit the execution time of a block of code. If the timeout is reached, the context block is exited prematurely, and the `cancelled_caught` attribute of the scope indicates if the timeout occurred. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import create_task_group, move_on_after, sleep, run + + +async def main(): + async with create_task_group() as tg: + with move_on_after(1) as scope: + print('Starting sleep') + await sleep(2) + print('This should never be printed') + + # The cancelled_caught property will be True if timeout was reached + print('Exited cancel scope, cancelled =', scope.cancelled_caught) + +run(main) +``` + +-------------------------------- + +TITLE: Fix Async Context Manager __aexit__ Call (BlockingPortal) +DESCRIPTION: Corrects an issue where the ``__aexit__()`` method of an async context manager was not being called in ``BlockingPortal.wrap_async_context_manager()`` if the host task was cancelled. This fix was contributed by Jonathan Slenders. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_124 + +LANGUAGE: Python +CODE: +``` +Fixed async context manager's ``__aexit__()`` method not being called in + ``BlockingPortal.wrap_async_context_manager()`` if the host task is cancelled + (`#381 `_; PR by Jonathan Slenders) +``` + +-------------------------------- + +TITLE: Handle Multiple Exceptions with exceptiongroup.catch in Python +DESCRIPTION: Provides an alternative method for handling multiple exceptions from a task group in Python versions prior to 3.11, using the `catch` function from the `exceptiongroup` library. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tasks.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +from anyio import create_task_group +from exceptiongroup import catch + +def handle_valueerror(excgroup: ExceptionGroup) -> None: + for exc in excgroup.exceptions: + ... +``` + +-------------------------------- + +TITLE: Low-Level Operations +DESCRIPTION: Provides low-level concurrency control mechanisms like checkpoints and cancellation shields. These are useful for fine-grained control over task execution and cancellation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_76 + +LANGUAGE: Python +CODE: +``` +anyio.lowlevel.checkpoint +anyio.lowlevel.checkpoint_if_cancelled +anyio.lowlevel.cancel_shielded_checkpoint +anyio.lowlevel.RunVar +``` + +-------------------------------- + +TITLE: Async Fixtures with Higher Scopes (Python) +DESCRIPTION: For asynchronous fixtures with scopes other than 'function', you must define your own `anyio_backend` fixture because the default one is function-scoped. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/testing.rst#_snippet_4 + +LANGUAGE: python +CODE: +``` +@pytest.fixture(scope='module') +``` + +-------------------------------- + +TITLE: Fix open_signal_receiver yielding integers on trio +DESCRIPTION: Addresses an inconsistency on the trio backend where open_signal_receiver() sometimes yielded integers instead of signal.Signals instances. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_106 + +LANGUAGE: Python +CODE: +``` +import signal +from anyio import open_signal_receiver + +# async def handle_signals(): +# async for sig in open_signal_receiver(signal.SIGINT): +# # sig should now consistently be a signal.Signals instance +``` + +-------------------------------- + +TITLE: File Write Stream +DESCRIPTION: A stream wrapper for writing to files, offering asynchronous write operations and potential buffering. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_53 + +LANGUAGE: Python +CODE: +``` +file_write_stream = anyio.streams.file.FileWriteStream(file_object) +``` + +-------------------------------- + +TITLE: Shielded Cleanup in Cancelled Scope +DESCRIPTION: Illustrates how to perform asynchronous cleanup operations within a scope that is already cancelled. This is achieved by wrapping the cleanup code in a shielded CancelScope. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +async def do_something(): + try: + await run_async_stuff() + except get_cancelled_exc_class(): + with CancelScope(shield=True): + await some_cleanup_function() + + raise +``` + +-------------------------------- + +TITLE: Enable Test Suite Without IPv6 (AnyIO) +DESCRIPTION: Allows the AnyIO test suite to run successfully even when IPv6 support is not available or when using trio or pytest without plugin autoloading. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_127 + +LANGUAGE: Python +CODE: +``` +Enabled the test suite to run without IPv6 support, trio or pytest plugin autoloading +``` + +-------------------------------- + +TITLE: Parametrize Pytest Tests with AnyIO Backends +DESCRIPTION: Introduces the capability to parameterize regular pytest test functions against a selected list of AnyIO backends. This allows for more comprehensive testing by running tests with different AnyIO implementations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_223 + +LANGUAGE: Python +CODE: +``` +import pytest +import anyio + +@pytest.mark.anyio +@pytest.mark.parametrize("backend", anyio.list_backends()) +async def test_feature_with_backends(backend): + async with anyio.move_on_after(10): + # Test logic that uses the specified backend + pass +``` + +-------------------------------- + +TITLE: Create a Cancel Scope +DESCRIPTION: Creates a new cancel scope, which is a context manager that allows for structured cancellation of asynchronous operations. Scopes can be nested. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_15 + +LANGUAGE: Python +CODE: +``` +async with anyio.CancelScope() as scope: + scope.cancel() + await some_async_code() +``` + +-------------------------------- + +TITLE: Add wait_readable/writable support for ProactorEventLoop +DESCRIPTION: Provides support for `wait_readable()` and `wait_writable()` on `ProactorEventLoop`, which is the default on asyncio for Windows. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_30 + +LANGUAGE: Python +CODE: +``` +Added support for ``wait_readable()`` and ``wait_writable()`` on ``ProactorEventLoop`` + (used on asyncio + Windows by default) +``` + +-------------------------------- + +TITLE: Configure socket options for selector thread waker +DESCRIPTION: Configures `SO_RCVBUF`, `SO_SNDBUF`, and `TCP_NODELAY` on the selector thread waker socket pair to potentially improve the performance of `wait_readable()` and `wait_writable()` when using `ProactorEventLoop`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_24 + +LANGUAGE: Python +CODE: +``` +Configure ``SO_RCVBUF``, ``SO_SNDBUF`` and ``TCP_NODELAY`` on the selector + thread waker socket pair (this should improve the performance of ``wait_readable()`` + and ``wait_writable()`` when using the ``ProactorEventLoop`` + (`#836 `_; PR by @graingert) +``` + +-------------------------------- + +TITLE: Fix CapacityLimiter token adjustment wake-up on asyncio +DESCRIPTION: Resolves an edge case on the asyncio backend where adjusting the total tokens in a CapacityLimiter failed to wake up waiting tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_71 + +LANGUAGE: Python +CODE: +``` +async def acquire(self, num_tokens: int = 1) -> None: + """Acquire tokens from the limiter.""" + pass + +def set_total_tokens(self, total_tokens: int) -> None: + """Set the total number of tokens.""" + # ... implementation ensures waiting tasks are woken ... + pass +``` + +-------------------------------- + +TITLE: AnyIO SocketStream API Enhancements +DESCRIPTION: The `SocketStream` API has been extended with `getsockopt()` and `setsockopt()` methods, providing granular control over socket behavior and configuration. This facilitates advanced network programming. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_267 + +LANGUAGE: Python +CODE: +``` +from anyio.streams.stapled import SocketStream + +async def configure_socket(stream: SocketStream): + # Example: Get socket options + keepalive = await stream.getsockopt(SOL_SOCKET, SO_KEEPALIVE) + print(f"Keepalive enabled: {keepalive}") + + # Example: Set socket options + await stream.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) +``` + +-------------------------------- + +TITLE: AnyIO asyncio Backend Improvements +DESCRIPTION: This snippet details improvements to the AnyIO asyncio backend in version 2.0.0. It includes added support for `ProactorEventLoop` on Windows and the backend now uses `asyncio.run()` for proper shutdown of async generators and cancellation of leftover native tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_202 + +LANGUAGE: Python +CODE: +``` +Added support for `ProactorEventLoop`. This allows asyncio applications to use AnyIO on Windows even without using AnyIO as the entry point. +The asyncio backend now uses `asyncio.run()` behind the scenes which properly shuts down async generators and cancels any leftover native tasks +``` + +-------------------------------- + +TITLE: Add Asynchronous Path Class (AnyIO) +DESCRIPTION: Introduces a new asynchronous ``Path`` class to AnyIO, providing asynchronous file system path operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_136 + +LANGUAGE: Python +CODE: +``` +Added asynchronous ``Path`` class +``` + +-------------------------------- + +TITLE: Happy Eyeballs Algorithm for connect_tcp() +DESCRIPTION: Implements the Happy Eyeballs algorithm (RFC 6555) for `anyio.connect_tcp()`. This feature improves connection establishment reliability and speed by trying both IPv4 and IPv6 connections concurrently. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_227 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def connect_with_happy_eyeballs(host, port): + # The Happy Eyeballs logic is now integrated into connect_tcp + async with await anyio.connect_tcp(host, port) as stream: + pass +``` + +-------------------------------- + +TITLE: Fix setsockopt() Option Passing +DESCRIPTION: Corrects the way `setsockopt()` passes options to the underlying method. This ensures that socket options are set correctly, which is crucial for network configuration and behavior. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_262 + +LANGUAGE: Python +CODE: +``` +import anyio +import socket + +async def set_socket_options(sock: socket.socket): + # Example: Setting SO_REUSEADDR + await sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # The internal handling of this call is now fixed. +``` + +-------------------------------- + +TITLE: Wrap a File Object Asynchronously +DESCRIPTION: Wraps a synchronous file object to make it usable within an asynchronous context. This allows existing file handles to be used with AnyIO's async I/O operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_31 + +LANGUAGE: Python +CODE: +``` +sync_file = open('data.bin', 'rb') +async_file = await anyio.wrap_file(sync_file) +``` + +-------------------------------- + +TITLE: Stapled Byte Stream +DESCRIPTION: Combines multiple byte streams into a single logical stream, allowing seamless data transfer across different underlying connections. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_57 + +LANGUAGE: Python +CODE: +``` +stapled_stream = anyio.streams.stapled.StapledByteStream([stream1, stream2]) +``` + +-------------------------------- + +TITLE: Run One-Shot Command Directly (Python) +DESCRIPTION: Executes an external command directly without involving a shell. This is equivalent to `shell=False` in Python's `subprocess.run`. The command and its arguments are provided as a sequence of strings. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/subprocesses.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import run_process, run + + +async def main(): + result = await run_process(['ps']) + print(result.stdout.decode()) + +run(main) +``` + +-------------------------------- + +TITLE: Improve Lock and Semaphore Performance with fast_acquire +DESCRIPTION: Enhances the performance of anyio.Lock and anyio.Semaphore on asyncio by up to 50%%. Introduces the 'fast_acquire' parameter which boosts performance at the cost of safety, as acquire() will not yield control back if there is no contention. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_47 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def use_lock_fast_acquire(): + lock = anyio.Lock() + # Using fast_acquire for potential performance gains + async with lock.acquire_with_flags(anyio.LockFlags.NO_WAIT): + print("Lock acquired immediately.") + +async def use_semaphore_fast_acquire(): + semaphore = anyio.Semaphore(1) + # Using fast_acquire for potential performance gains + async with semaphore.acquire_with_flags(anyio.SemaphoreFlags.NO_WAIT): + print("Semaphore acquired immediately.") +``` + +-------------------------------- + +TITLE: Provide Typed Attributes +DESCRIPTION: An interface for objects that can provide a set of typed attributes. This allows other objects to access metadata in a structured way. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_11 + +LANGUAGE: Python +CODE: +``` +class MyProvider(anyio.TypedAttributeProvider): + def typed_attributes(self): + return anyio.TypedAttributeSet() +``` + +-------------------------------- + +TITLE: AsyncFile Generics Update +DESCRIPTION: Updates the generics in ``AsyncFile`` to ensure methods correctly return ``str`` or ``bytes`` based on the argument provided to ``open_file()``. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_140 + +LANGUAGE: Python +CODE: +``` +Changed the generics in ``AsyncFile`` so that the methods correctly return either + ``str`` or ``bytes`` based on the argument to ``open_file()`` +``` + +-------------------------------- + +TITLE: Add support for byte-based paths in Unix socket functions +DESCRIPTION: Enables the use of byte-based paths for various Unix socket operations, including connecting, creating listeners, and creating datagram sockets. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_67 + +LANGUAGE: Python +CODE: +``` +async def connect_unix(path: bytes, ...) -> None: + """Connect to a Unix socket.""" + pass + +def create_unix_listeners(path: bytes, ...) -> None: + """Create Unix domain socket listeners.""" + pass + +def create_unix_datagram_socket(path: bytes, ...) -> None: + """Create a Unix domain datagram socket.""" + pass + +def create_connected_unix_datagram_socket(path: bytes, ...) -> None: + """Create a connected Unix domain datagram socket.""" + pass +``` + +-------------------------------- + +TITLE: Suppress DeprecationWarning for TaskGroup.spawn in AnyIO 3 +DESCRIPTION: Illustrates how to suppress `DeprecationWarning` when using `TaskGroup.spawn()` in AnyIO 3 for code that needs to maintain compatibility with AnyIO 2. This is a temporary measure until AnyIO 4. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_8 + +LANGUAGE: Python +CODE: +``` +import warnings +from anyio import create_task_group + +async def foo(): + async with create_task_group() as tg: + with warnings.catch_warnings(): + await tg.spawn(otherfunc) +``` + +-------------------------------- + +TITLE: Asynchronous File Interface +DESCRIPTION: Defines the interface for asynchronous file objects, providing methods for reading, writing, and seeking without blocking the event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_32 + +LANGUAGE: Python +CODE: +``` +async def read_file_content(file: anyio.AsyncFile): + content = await file.read() + return content +``` + +-------------------------------- + +TITLE: Create Memory Object Stream +DESCRIPTION: Creates a pair of connected asynchronous streams that transfer arbitrary Python objects in memory. Useful for inter-task communication. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_42 + +LANGUAGE: Python +CODE: +``` +send_stream, receive_stream = anyio.create_memory_object_stream() +``` + +-------------------------------- + +TITLE: Provide Values for Custom Typed Attributes +DESCRIPTION: Illustrates how to provide values for custom typed attributes by implementing the `extra_attributes` property in a class inheriting from `TypedAttributeProvider`. This property returns a mapping of attributes to their provider functions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/typedattrs.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from collections.abc import Callable, Mapping + +from anyio import TypedAttributeProvider + + +class MyAttributeProvider(TypedAttributeProvider): + @property + def extra_attributes() -> Mapping[Any, Callable[[], Any]]: + return { + MyTypedAttribute.string_valued_attribute: lambda: 'my attribute value', + MyTypedAttribute.some_float_attribute: lambda: 6.492 + } +``` + +-------------------------------- + +TITLE: Add experimental subinterpreter support +DESCRIPTION: Introduces experimental support for running functions within subinterpreters, available on Python 3.13 and later. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_22 + +LANGUAGE: Python +CODE: +``` +Added **experimental** support for running functions in subinterpreters on Python + 3.13 and later +``` + +-------------------------------- + +TITLE: Connectable Objects +DESCRIPTION: Represents objects that can be connected to, such as TCP or UNIX domain sockets. These are used to establish network connections. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_72 + +LANGUAGE: Python +CODE: +``` +anyio.TCPConnectable +anyio.UNIXConnectable +``` + +-------------------------------- + +TITLE: Stapled Object Stream +DESCRIPTION: Combines multiple object streams into a single logical stream for unified object transfer. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_58 + +LANGUAGE: Python +CODE: +``` +stapled_stream = anyio.streams.stapled.StapledObjectStream([obj_stream1, obj_stream2]) +``` + +-------------------------------- + +TITLE: Resolve Host Names with anyio.getaddrinfo() +DESCRIPTION: Ensures that host names are resolved using `anyio.getaddrinfo()` before calling `socket.sendto()` to prevent blocking on synchronous name resolution. This improves network operation by avoiding blocking calls. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_221 + +LANGUAGE: Python +CODE: +``` +import anyio + +# Example usage within a function that sends data via UDP +async def send_udp_data(socket, data, address): + await socket.sendto(data, address) + +# The fix ensures that anyio.getaddrinfo() is used internally by UDPSocket.send() +``` + +-------------------------------- + +TITLE: Fix return annotation of __aexit__ +DESCRIPTION: Corrects the return annotation for `__aexit__` in async context managers. It clarifies that the method should return `bool` if it suppresses an exception, and `None` otherwise. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_6 + +LANGUAGE: Python +CODE: +``` +import anyio +from typing import Optional, Type, Any + +class MyContextManager: + async def __aenter__(self) -> Any: + return self + + async def __aexit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[Any]) -> Optional[bool]: + if exc_type is not None: + # Decide whether to suppress the exception + return True # Suppress exception + return None # Do not suppress exception + +``` + +-------------------------------- + +TITLE: Add ExceptionGroup.exceptions Documentation +DESCRIPTION: Includes the missing documentation for the ``exceptions`` attribute of the ``ExceptionGroup`` class. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_132 + +LANGUAGE: Python +CODE: +``` +Added missing documentation for the ``ExceptionGroup.exceptions`` attribute +``` + +-------------------------------- + +TITLE: Correct API Documentation in AnyIO +DESCRIPTION: Version 1.4.0 corrects the API documentation for Stream.receive_until(), clarifying that the delimiter is not included in the returned data. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_220 + +LANGUAGE: Python +CODE: +``` +Fixed API documentation on Stream.receive_until() which claimed that the delimiter will be included in the returned data when it really isn't +``` + +-------------------------------- + +TITLE: Task Group Interface +DESCRIPTION: Defines the interface for task groups, which are used to manage concurrent tasks. It includes methods for spawning tasks and waiting for their completion. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_17 + +LANGUAGE: Python +CODE: +``` +async def run_tasks(tg: anyio.abc.TaskGroup): + await tg.spawn(task1) + await tg.spawn(task2, arg1) +``` + +-------------------------------- + +TITLE: Fix anyio.AsyncFile readinto annotations +DESCRIPTION: Corrects the return type annotations for the `readinto()` and `readinto1()` methods within the `anyio.AsyncFile` class. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_32 + +LANGUAGE: Python +CODE: +``` +Fixed the return type annotations of ``readinto()`` and ``readinto1()`` methods in the + ``anyio.AsyncFile`` class + (`#825 `_) +``` + +-------------------------------- + +TITLE: Spawn Task from Worker Thread using BlockingPortal (AnyIO) +DESCRIPTION: Illustrates spawning background tasks from a worker thread using `BlockingPortal.start_task_soon`. This method returns a `concurrent.futures.Future` that can be used to track the task's completion and retrieve its result. Tasks can be cancelled by cancelling the returned future. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +from concurrent.futures import as_completed + +from anyio import sleep +from anyio.from_thread import start_blocking_portal + + +async def long_running_task(index): + await sleep(1) + print(f'Task {index} running...') + await sleep(index) + return f'Task {index} return value' + + +with start_blocking_portal() as portal: + futures = [portal.start_task_soon(long_running_task, i) for i in range(1, 5)] + for future in as_completed(futures): + print(future.result()) +``` + +-------------------------------- + +TITLE: Add UNIX Datagram Socket Creation Functions +DESCRIPTION: Introduces `create_unix_datagram_socket` and `create_connected_unix_datagram_socket` functions for creating UNIX domain datagram sockets, contributed by Jean Hominal. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_86 + +LANGUAGE: python +CODE: +``` +Added ``create_unix_datagram_socket`` and ``create_connected_unix_datagram_socket`` to + create UNIX datagram sockets (PR by Jean Hominal) +``` + +-------------------------------- + +TITLE: Shield Task from Cancellation in AnyIO +DESCRIPTION: Demonstrates how to shield a task from cancellation by creating a cancel scope with `shield=True`. This is useful for critical operations like resource cleanup, ensuring they complete even if the parent scope is cancelled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from anyio import CancelScope, create_task_group, sleep, run + + +async def external_task(): + print('Started sleeping in the external task') + await sleep(1) + print('This line should never be seen') + + +async def main(): + async with create_task_group() as tg: + with CancelScope(shield=True): + tg.start_soon(external_task) + await sleep(0.5) # Give external_task time to start + tg.cancel_scope.cancel() # Cancel the main task group + + print('Task group finished') + +run(main) +``` + +-------------------------------- + +TITLE: Fix Process aclose cleanup on cancellation +DESCRIPTION: Addresses issues where cancelling Process.aclose() or run_process() could lead to orphaned processes or unclosed standard streams, particularly on the Trio backend. Ensures necessary cleanup is performed. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_65 + +LANGUAGE: Python +CODE: +``` +def aclose(self) -> None: + """Cleanly shut down the process.""" + # ... implementation details ... + pass +``` + +-------------------------------- + +TITLE: Add wait_readable and wait_writable functions +DESCRIPTION: Introduces `wait_readable()` and `wait_writable()` functions that accept file descriptors or objects with a `.fileno()` method, deprecating the older `wait_socket_readable()` and `wait_socket_writable()`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_28 + +LANGUAGE: Python +CODE: +``` +Added the ``wait_readable()`` and ``wait_writable()`` functions which will accept + an object with a ``.fileno()`` method or an integer handle, and deprecated + their now obsolete versions (``wait_socket_readable()`` and + ``wait_socket_writable()``) (PR by @davidbrochart) +``` + +-------------------------------- + +TITLE: Testing Task Groups with Specific Exceptions (pytest) +DESCRIPTION: Illustrates how to adapt pytest tests that previously expected a specific exception from a task group in AnyIO 3. In AnyIO 4, task groups raise ExceptionGroups, so tests must now expect `ExceptionGroup` and then inspect its contents to verify the original exception type. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/migration.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +from exceptiongroup import ExceptionGroup + +with pytest.raises(ExceptionGroup) as exc: + await function_using_a_taskgroup() + +assert len(exc.value.exceptions) == 1 +assert isinstance(exc.value.exceptions[0], ValueError) +``` + +-------------------------------- + +TITLE: Create UNIX Listener +DESCRIPTION: Modifies `create_unix_listener()` to remove existing sockets at the specified path before creating a new listener. This prevents `OSError: Address already in use` exceptions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_162 + +LANGUAGE: Python +CODE: +``` +anyio.create_unix_listener() +``` + +-------------------------------- + +TITLE: Buffered Byte Stream +DESCRIPTION: A byte stream that provides buffering for both sending and receiving operations, optimizing I/O efficiency. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_50 + +LANGUAGE: Python +CODE: +``` +buffered_stream = anyio.streams.buffered.BufferedByteStream(underlying_stream) +``` + +-------------------------------- + +TITLE: Adapt Trio API changes for to_thread.run_sync +DESCRIPTION: Updates the `anyio.to_thread.run_sync` function to use the `abandon_on_cancel` keyword argument instead of `cancellable`, aligning with API changes in Trio v0.23. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_72 + +LANGUAGE: Python +CODE: +``` +async def run_sync(func: Callable[..., T], *args: Any, abandon_on_cancel: bool = False, **kwargs: Any) -> T: + """Run a synchronous function in a thread.""" + pass +``` + +-------------------------------- + +TITLE: Add sleep_forever() and sleep_until() Functions +DESCRIPTION: Introduces the ``sleep_forever()`` and ``sleep_until()`` functions for managing asynchronous sleep operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_153 + +LANGUAGE: Python +CODE: +``` +Added the ``sleep_forever()`` and ``sleep_until()`` functions +``` + +-------------------------------- + +TITLE: RunVar Class for Event Loop Scoping +DESCRIPTION: Introduces the `RunVar` class for scoping variables to the running event loop. This provides a mechanism for managing state specific to an event loop instance. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_179 + +LANGUAGE: Python +CODE: +``` +anyio.RunVar +``` + +-------------------------------- + +TITLE: Blocking Portal Interface +DESCRIPTION: Represents a portal for communication between synchronous threads and the AnyIO event loop. It allows threads to run asynchronous code and receive results. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_29 + +LANGUAGE: Python +CODE: +``` +portal = anyio.from_thread.BlockingPortal() +portal.run_sync_soon(async_task) +result = portal.call_soon(sync_func) +``` + +-------------------------------- + +TITLE: Run Sync Code in Process +DESCRIPTION: Introduces the `run_sync_in_process()` function for executing synchronous code within separate worker processes. This is useful for offloading blocking operations from the main asynchronous event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_159 + +LANGUAGE: Python +CODE: +``` +anyio.run_sync_in_process() +``` + +-------------------------------- + +TITLE: Fix anyio.to_thread.run_sync() reference holding +DESCRIPTION: Eliminates the unnecessary holding of references to context, function, arguments, and other items by `anyio.to_thread.run_sync()` on asyncio until the next work item. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_21 + +LANGUAGE: Python +CODE: +``` +Fixed ``anyio.to_thread.run_sync()`` needlessly holding on to references of the + context, function, arguments and others until the next work item on asyncio + (PR by @Wankupi) +``` + +-------------------------------- + +TITLE: Manage Typed Attributes +DESCRIPTION: Provides a set of typed attributes that can be associated with an object. This allows for organized and type-safe storage of metadata. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_10 + +LANGUAGE: Python +CODE: +``` +typed_attrs = anyio.TypedAttributeSet() +typed_attrs.update({'key': 'value'}) +value = typed_attrs.get('key') +``` + +-------------------------------- + +TITLE: Synchronization Primitive Statistics +DESCRIPTION: Introduces the `statistics()` method for various synchronization primitives, including `Event`, `Lock`, `Condition`, `Semaphore`, `CapacityLimiter`, `MemoryObjectReceiveStream`, and `MemoryObjectSendStream`. This method provides insights into the internal state of these primitives. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_168 + +LANGUAGE: Python +CODE: +``` +anyio.Event.statistics() +``` + +LANGUAGE: Python +CODE: +``` +anyio.Lock.statistics() +``` + +LANGUAGE: Python +CODE: +``` +anyio.Condition.statistics() +``` + +LANGUAGE: Python +CODE: +``` +anyio.Semaphore.statistics() +``` + +LANGUAGE: Python +CODE: +``` +anyio.CapacityLimiter.statistics() +``` + +LANGUAGE: Python +CODE: +``` +anyio.MemoryObjectReceiveStream.statistics() +``` + +LANGUAGE: Python +CODE: +``` +anyio.MemoryObjectSendStream.statistics() +``` + +-------------------------------- + +TITLE: Add Async Name Resolution in AnyIO +DESCRIPTION: Version 1.4.0 of AnyIO introduces asynchronous name resolution functions, getaddrinfo() and getnameinfo(), for network address translation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_216 + +LANGUAGE: Python +CODE: +``` +Added async name resolution functions (anyio.getaddrinfo() and anyio.getnameinfo()) +``` + +-------------------------------- + +TITLE: Text Stream Interface +DESCRIPTION: Defines the interface for streams that handle text data, abstracting away the underlying byte encoding and decoding. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_60 + +LANGUAGE: Python +CODE: +``` +async def send_text(stream: anyio.streams.text.TextSendStream, text: str): + await stream.send_text(text) +``` + +-------------------------------- + +TITLE: Relax Path Initializer Type (FileReadStream/FileWriteStream) +DESCRIPTION: The type for the ``path`` initializer argument in ``FileReadStream`` and ``FileWriteStream`` has been relaxed to accept any path-like object, including the new asynchronous ``Path`` class. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_138 + +LANGUAGE: Python +CODE: +``` +Relaxed the type of the ``path`` initializer argument to ``FileReadStream`` and + ``FileWriteStream`` so they accept any path-like object (including the new + asynchronous ``Path`` class) +``` + +-------------------------------- + +TITLE: Asyncio Test Runner Default (No uvloop) +DESCRIPTION: Configures the asyncio test runner to not use uvloop by default, aligning its behavior with ``anyio.run()``. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_133 + +LANGUAGE: Python +CODE: +``` +Changed the asyncio test runner not to use uvloop by default (to match the behavior of + ``anyio.run()``) +``` + +-------------------------------- + +TITLE: AnyIO Exceptions +DESCRIPTION: Defines a set of custom exceptions used throughout the AnyIO library to indicate various error conditions, such as broken resources, busy resources, connection failures, and incomplete reads. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_78 + +LANGUAGE: Python +CODE: +``` +anyio.BrokenResourceError +anyio.BrokenWorkerInterpreter +anyio.BrokenWorkerProcess +anyio.BusyResourceError +anyio.ClosedResourceError +anyio.ConnectionFailed +anyio.DelimiterNotFound +anyio.EndOfStream +anyio.IncompleteRead +anyio.TypedAttributeLookupError +anyio.WouldBlock +``` + +-------------------------------- + +TITLE: Asyncio Shielding: Orphaned Tasks and Unpropagated Exceptions +DESCRIPTION: Illustrates the issue with asyncio.shield where a shielded task can become orphaned if the host task is cancelled. This can lead to exceptions not propagating and tasks being garbage collected mid-execution. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +import asyncio +import gc + + +async def shielded_task(): + fut = asyncio.get_running_loop().create_future() + await fut + + +async def host_task(): + await asyncio.shield(shielded_task()) + + +async def main(): + async with asyncio.TaskGroup() as tg: + task = tg.create_task(host_task()) + await asyncio.sleep(0) # allow the host task to start + task.cancel() + await asyncio.sleep(0) # allow the cancellation to take effect on the host task + gc.collect() + +# Prints warning: Task was destroyed but it is pending! +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: Pin Trio to avoid ExceptionGroup incompatibility +DESCRIPTION: Pins the Trio dependency to versions less than 0.22 to prevent an AttributeError ('NonBaseMultiError' object has no attribute '_exceptions') caused by incompatibility with AnyIO's ExceptionGroup class. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_114 + +LANGUAGE: Python +CODE: +``` +# requirements.txt or setup.py: +# anyio<3.7.0 +# trio<0.22 +``` + +-------------------------------- + +TITLE: Maybe Async Functions +DESCRIPTION: Adds `maybe_async()` and `maybe_async_cm()` functions to facilitate forward compatibility with AnyIO 3. These functions allow writing code that can seamlessly work with both synchronous and asynchronous contexts. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_180 + +LANGUAGE: Python +CODE: +``` +anyio.maybe_async() +``` + +LANGUAGE: Python +CODE: +``` +anyio.maybe_async_cm() +``` + +-------------------------------- + +TITLE: Improvement: Running Synchronous Code from Threads +DESCRIPTION: Introduces the `run_sync_from_thread()` function to improve the process of running synchronous code, likely from within a different thread context. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_158 + +LANGUAGE: Python +CODE: +``` +import anyio + +def sync_function(): + print("Running synchronous function") + +async def main(): + # Example of running a synchronous function from a thread + await anyio.to_thread.run_sync(sync_function) + +# anyio.run(main) +``` + +-------------------------------- + +TITLE: Fix anyio.Path.copy() and copy_into() on Python 3.14.0a7 +DESCRIPTION: Corrects an issue where `anyio.Path.copy()` and `anyio.Path.copy_into()` failed on specific pre-release versions of Python 3.14. This ensures reliable file copying operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +import anyio + +# Example usage (assuming the fix is applied): +# async def copy_file(): +# source_path = anyio.Path('source.txt') +# dest_path = anyio.Path('destination.txt') +# await source_path.copy(dest_path) + +# dest_dir = anyio.Path('dest_dir') +# await source_path.copy_into(dest_dir) + +``` + +-------------------------------- + +TITLE: AnyIO Memory Object Stream Statistics +DESCRIPTION: The MemoryObjectReceiveStream in AnyIO provides a statistics method to gather information about the stream's state, including queued items, open streams, and waiting tasks. This aids in debugging and understanding stream behavior. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_8 + +LANGUAGE: Python +CODE: +``` +stream.statistics() +``` + +-------------------------------- + +TITLE: Run Synchronous Function in a Subinterpreter +DESCRIPTION: Executes a synchronous function within a separate Python subinterpreter. This provides process-like isolation without the overhead of full processes, useful for running untrusted code. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_21 + +LANGUAGE: Python +CODE: +``` +result = await anyio.to_interpreter.run_sync(sync_function, arg1, interpreter=my_interpreter) +``` + +-------------------------------- + +TITLE: Add info property to anyio.Path on Python 3.14 +DESCRIPTION: Introduces an `info` property to `anyio.Path` objects when running on Python 3.14. This property provides detailed file status information, similar to `os.stat()`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_16 + +LANGUAGE: Python +CODE: +``` +import anyio + +# async def get_file_info(): +# path = anyio.Path('my_file.txt') +# if hasattr(path, 'info'): # Check for Python 3.14+ +# file_info = await path.info() +# print(f"File size: {file_info.st_size}") + +``` + +-------------------------------- + +TITLE: Specify thread name in start_blocking_portal +DESCRIPTION: Allows specifying a custom thread name when using `start_blocking_portal()`. This helps in identifying and debugging threads created by AnyIO when running blocking code in a separate thread. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def main(): + def blocking_function(): + pass + await anyio.to_thread.start_blocking_portal(blocking_function, name='my-custom-thread') + +# anyio.run(main) + +``` + +-------------------------------- + +TITLE: Byte Stream Interfaces +DESCRIPTION: Defines interfaces for streams that transfer raw bytes. These are fundamental for network communication and file I/O. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_45 + +LANGUAGE: Python +CODE: +``` +async def read_bytes(stream: anyio.abc.ByteReceiveStream, nbytes: int): + return await stream.receive_bytes(nbytes) +``` + +-------------------------------- + +TITLE: Cancel Task Group in AnyIO +DESCRIPTION: Demonstrates how to cancel all tasks within a task group by cancelling its associated cancel scope. Tasks waiting on operations will be cancelled immediately, while others will be cancelled at their next yield point. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/cancellation.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio import create_task_group, get_cancelled_exc_class, sleep, run + + +async def waiter(index: int): + try: + await sleep(1) + except get_cancelled_exc_class(): + print(f"Waiter {index} cancelled") + raise + + +async def taskfunc(): + async with create_task_group() as tg: + # Start a couple tasks and wait until they are blocked + tg.start_soon(waiter, 1) + tg.start_soon(waiter, 2) + await sleep(0.1) + + # Cancel the scope and exit the task group + tg.cancel_scope.cancel() + +run(taskfunc) +``` + +-------------------------------- + +TITLE: Fix Pathlib2 Compatibility with aopen() +DESCRIPTION: Ensures compatibility with the `pathlib2` library when using `anyio.aopen()`. This allows asynchronous file operations to work correctly with `pathlib2` objects. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_254 + +LANGUAGE: Python +CODE: +``` +import anyio +from pathlib2 import Path + +async def async_open_pathlib2(): + path = Path("my_file.txt") + async with await anyio.aopen(path) as f: + await f.write("hello") +``` + +-------------------------------- + +TITLE: Fix ResourceWarning on UNIX Socket Connect Fail (Asyncio) +DESCRIPTION: Addresses a ``ResourceWarning`` concerning an unclosed socket that occurred when a UNIX socket connection failed in asyncio. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_148 + +LANGUAGE: Python +CODE: +``` +Fixed ``ResourceWarning`` about an unclosed socket when UNIX socket connect fails on + asyncio +``` + +-------------------------------- + +TITLE: Update UDPSocket Methods in AnyIO +DESCRIPTION: This section describes the modifications to the UDPSocket class in AnyIO, including changes to its attributes, method signatures, and parameter requirements for sending data. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_209 + +LANGUAGE: Python +CODE: +``` +address → .extra(SocketAttribute.local_address) +getsockopt() → .extra(SocketAttribute.raw_socket).getsockopt(...) +port → .extra(SocketAttribute.local_port) +receive() no longer takes a maximum bytes argument +receive_packets() → (removed; use async for on the UDP socket instead) +send() → requires a tuple for destination now (address, port), for compatibility with the new UnreliableObjectStream interface. The sendto() method works like the old send() method. +setsockopt() → .extra(SocketAttribute.raw_socket).setsockopt(...) +``` + +-------------------------------- + +TITLE: Fix connect_tcp() cyclic references in tracebacks +DESCRIPTION: Corrects an issue where `connect_tcp()` could produce cyclic references in tracebacks when exceptions were raised. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_20 + +LANGUAGE: Python +CODE: +``` +Fixed ``connect_tcp()`` producing cyclic references in tracebacks when raising + exceptions (`#809 `_; PR by @graingert) +``` + +-------------------------------- + +TITLE: Fix OSError with IPv6 link-local addresses in create_tcp_listener +DESCRIPTION: Resolves an OSError when using create_tcp_listener() to bind to a link-local IPv6 address. Includes workarounds for related bugs in uvloop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_112 + +LANGUAGE: Python +CODE: +``` +from anyio import create_tcp_listener + +# Example usage: +# listener = create_tcp_listener(local_address=('fe80::1%eth0', 80)) +# The fix ensures this operation succeeds without errors. +``` + +-------------------------------- + +TITLE: File Read Stream +DESCRIPTION: A stream wrapper specifically for reading from files, potentially with added buffering or other optimizations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_52 + +LANGUAGE: Python +CODE: +``` +file_read_stream = anyio.streams.file.FileReadStream(file_object) +``` + +-------------------------------- + +TITLE: Fix UDPSocket.send() Address Parameter Type +DESCRIPTION: Corrects the type hint for the `address` parameter in `UDPSocket.send()` to include `IPAddress` objects. While the underlying implementation already supported these, the type hint was updated for better accuracy and clarity. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_238 + +LANGUAGE: Python +CODE: +``` +from anyio.abc import UDPSocket +from anyio.socket import IPAddress + +async def send_with_ipaddress(socket: UDPSocket, data: bytes, address: IPAddress): + await socket.sendto(data, address) +``` + +-------------------------------- + +TITLE: Writable CancelScope Deadline +DESCRIPTION: Makes the `CancelScope.deadline` attribute writable, allowing dynamic adjustment of cancellation deadlines for tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_165 + +LANGUAGE: Python +CODE: +``` +anyio.CancelScope.deadline +``` + +-------------------------------- + +TITLE: Async Iteration for Queues +DESCRIPTION: Enables asynchronous iteration for queues within AnyIO. This feature simplifies the process of consuming items from queues in an asynchronous manner, making queue processing more straightforward. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_230 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def consume_from_queue(queue: anyio.Queue): + async for item in queue: + print(f"Received: {item}") +``` + +-------------------------------- + +TITLE: Fix context manager __exit__ return type annotations +DESCRIPTION: Corrects the return type annotations for the `__exit__` method of various context managers. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_26 + +LANGUAGE: Python +CODE: +``` +Fixed return type annotation of various context managers' ``__exit__`` method + (`#847 `_; PR by @Enegg) +``` + +-------------------------------- + +TITLE: Fix regression with parametrized async fixtures +DESCRIPTION: Addresses a regression that prevented the use of parametrized async fixtures. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_35 + +LANGUAGE: Python +CODE: +``` +Fixed regression caused by (`#807 `_) + that prevented the use of parametrized async fixtures +``` + +-------------------------------- + +TITLE: Fix TLSListener handshake error logging in asyncio +DESCRIPTION: Addresses a CPython bug where TLSListener.handle_handshake_error() on asyncio logged 'NoneType: None' instead of the actual error message. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_98 + +LANGUAGE: Python +CODE: +``` +def handle_handshake_error(): + # Original code might have logged a generic message + # This fix ensures the specific error is logged + pass +``` + +-------------------------------- + +TITLE: Spooled Temporary File +DESCRIPTION: A temporary file that behaves like an in-memory file until a certain size is reached, at which point it is written to disk. This optimizes for small temporary data. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_38 + +LANGUAGE: Python +CODE: +``` +async with await anyio.SpooledTemporaryFile(max_size=1024) as spooled_file: + await spooled_file.write(b'small data') +``` + +-------------------------------- + +TITLE: Pytest Plugin Changes in AnyIO +DESCRIPTION: This outlines modifications to the AnyIO pytest plugin, including the removal of a command-line option, refactoring for better async fixture utilization, and fixing Hypothesis support. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_215 + +LANGUAGE: Python +CODE: +``` +Removed the --anyio-backends command line option for the pytest plugin. Use the -k option to do ad-hoc filtering, and the anyio_backend fixture to control which backends you wish to run the tests by default. +The pytest plugin was refactored to run the test and all its related async fixtures inside the same event loop, making async fixtures much more useful +Fixed Hypothesis support in the pytest plugin +``` + +-------------------------------- + +TITLE: Define Custom Typed Attributes +DESCRIPTION: Shows how to define custom typed attributes using `TypedAttributeSet` and `typed_attribute`. This involves creating a class that inherits from `TypedAttributeSet` and defining attributes with their types. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/typedattrs.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import TypedAttributeSet, typed_attribute + + +class MyTypedAttribute(TypedAttributeSet): + string_valued_attribute: str = typed_attribute() + some_float_attribute: float = typed_attribute() +``` + +-------------------------------- + +TITLE: Asyncio Shielding and Event Loop Shutdown Cancellation +DESCRIPTION: Demonstrates how asyncio.shield does not protect tasks from cancellation when the event loop is shut down, such as during a simulated SIGINT (Ctrl+C). + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +import asyncio +import signal + + +async def finalizer(): + await asyncio.sleep(1) + print("Finalizer done") + +async def main(): + ... # the business logic goes here + asyncio.get_running_loop().call_soon(signal.raise_signal, signal.SIGINT) # simulate ctrl+C + await asyncio.shield(finalizer()) + +# Prints a traceback containing a KeyboardInterrupt and a CancelledError, but not the "Finalizer done" message +asyncio.run(main()) +``` + +-------------------------------- + +TITLE: File Stream Attributes +DESCRIPTION: Provides attributes related to file streams, such as file descriptor or mode. Useful for inspecting or configuring file-based I/O. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_51 + +LANGUAGE: Python +CODE: +``` +attrs = anyio.streams.file.FileStreamAttribute(file_descriptor=1, mode='r') +``` + +-------------------------------- + +TITLE: AnyIO BaseSocket sendall() Fix +DESCRIPTION: Addresses an issue in `BaseSocket.sendall()` where large buffers were mishandled. This fix ensures reliable transmission of large data chunks, improving performance and data integrity. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_268 + +LANGUAGE: Python +CODE: +``` +from anyio.abc import SocketStream + +async def send_large_data(socket: SocketStream, data: bytes): + await socket.sendall(data) + # The fix ensures this handles large data efficiently. +``` + +-------------------------------- + +TITLE: Context Propagation to Worker Threads (to_thread.run_sync) +DESCRIPTION: Enhances context propagation to and from worker threads for functions like ``to_thread.run_sync()``, ``from_thread.run()``, and ``from_thread.run_sync()``. Requires Python 3.7 for proper functionality with asyncio. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_122 + +LANGUAGE: Python +CODE: +``` +Added context propagation to/from worker threads in ``to_thread.run_sync()``, + ``from_thread.run()`` and ``from_thread.run_sync()`` + (`#363 `_; partially based on a PR by + Sebastián Ramírez) + + **NOTE**: Requires Python 3.7 to work properly on asyncio! +``` + +-------------------------------- + +TITLE: Update anyio.Path Documentation and is_junction Availability +DESCRIPTION: Corrects documentation for anyio.Path regarding method limitations based on Python version and makes 'is_junction' unavailable on Python versions earlier than 3.12. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_46 + +LANGUAGE: Python +CODE: +``` +import anyio +import sys + +async def check_path_junction(path_str): + p = anyio.Path(path_str) + if sys.version_info >= (3, 12): + if await p.is_junction(): + print(f"{path_str} is a junction.") + else: + print("is_junction() is not available on this Python version.") + +# Example usage: +# await check_path_junction("/path/to/some/directory") +``` + +-------------------------------- + +TITLE: Add __slots__ to AsyncResource +DESCRIPTION: Appends '__slots__' to 'AsyncResource', enabling child classes to utilize '__slots__' for memory optimization. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_60 + +LANGUAGE: Python +CODE: +``` +import anyio + +# This change is internal to AnyIO's AsyncResource class. +# The benefit is for developers subclassing AsyncResource. + +class MyAsyncResource(anyio.resources.AsyncResource): + __slots__ = ('my_attribute',) + + def __init__(self, my_attribute): + self.my_attribute = my_attribute + + async def __aenter__(self): + print(f"Entering resource with {self.my_attribute}") + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + print("Exiting resource") + +# Example usage: +# async def main(): +# async with MyAsyncResource("value") as resource: +# print(f"Inside resource: {resource.my_attribute}") +# +# anyio.run(main) +``` + +-------------------------------- + +TITLE: Run Coroutine Function from Worker Thread (AnyIO) +DESCRIPTION: Allows calling a coroutine function in the event loop thread from an AnyIO worker thread. This is a direct counterpart to asyncio's run_coroutine_threadsafe. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_14 + +LANGUAGE: Python +CODE: +``` +anyio.from_thread.run(func, *args, backend=None, backend_options=None) +``` + +-------------------------------- + +TITLE: Update Pytest Plugin for Task Group Execution (AnyIO 4.0.0) +DESCRIPTION: The AnyIO pytest plugin was updated in version 4.0.0 to run all tests and fixtures within the same task group. This allows fixtures to effectively set context variables for tests and other fixtures. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_78 + +LANGUAGE: python +CODE: +``` +**BACKWARDS INCOMPATIBLE** Changes the pytest plugin to run all tests and fixtures in + the same task, allowing fixtures to set context variables for tests and other fixtures +``` + +-------------------------------- + +TITLE: Fix wait_all_tasks_blocked() with Generator Tasks +DESCRIPTION: Corrects the behavior of `wait_all_tasks_blocked()` when used with generator-based tasks in the asyncio backend. It now functions as expected, ensuring accurate task state monitoring. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_241 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def my_generator_task(): + yield + +async def check_blocked_tasks(): + async with anyio.create_task_group() as tg: + await tg.spawn(my_generator_task) + # wait_all_tasks_blocked() should now work correctly +``` + +-------------------------------- + +TITLE: Fix TaskInfo.has_pending_cancellation() false positives +DESCRIPTION: Resolves instances where `TaskInfo.has_pending_cancellation()` on asyncio might return false positives during cleanup code on Python versions 3.11 and later. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_33 + +LANGUAGE: Python +CODE: +``` +Fixed ``TaskInfo.has_pending_cancellation()`` on asyncio returning false positives in + cleanup code on Python >= 3.11 + (`#832 `_; PR by @gschaffner) +``` + +-------------------------------- + +TITLE: Fix to_thread.run_sync() Hanging on Second Call (Asyncio) +DESCRIPTION: Corrects an issue where ``to_thread.run_sync()`` would hang on its second call within asyncio when used with ``loop.run_until_complete()``. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_146 + +LANGUAGE: Python +CODE: +``` +Fixed ``to_thread.run_sync()`` hanging on the second call on asyncio when used with + ``loop.run_until_complete()`` +``` + +-------------------------------- + +TITLE: Task Synchronization Changes in AnyIO +DESCRIPTION: This covers significant changes in AnyIO's task synchronization primitives, including the replacement of queues, addition of acquire/release methods, and removal of Event.clear(). + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_214 + +LANGUAGE: Python +CODE: +``` +Queues were replaced by memory object streams +Added the acquire() and release() methods to the Lock, Condition and Semaphore classes +Removed the Event.clear() method. You must now replace the event object with a new one rather than clear the old one. +Fixed Condition.wait() not working on asyncio and curio +``` + +-------------------------------- + +TITLE: Fix Type Annotation for open_signal_receiver +DESCRIPTION: Corrects the type annotation for ``open_signal_receiver()``, ensuring it accurately reflects its role as a synchronous context manager. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_149 + +LANGUAGE: Python +CODE: +``` +Fixed the type annotation of ``open_signal_receiver()`` as a synchronous context + manager +``` + +-------------------------------- + +TITLE: Workaround for OpenSSL 3.0 Compatibility (bpo-46313) +DESCRIPTION: Introduces a workaround to ensure compatibility with OpenSSL version 3.0, addressing a specific issue tracked as bpo-46313. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_121 + +LANGUAGE: Python +CODE: +``` +Added workaround for bpo-46313_ to enable compatibility with OpenSSL 3.0 + +.. _bpo-46313: https://bugs.python.org/issue46313 +``` + +-------------------------------- + +TITLE: Convert Task Names to str Early (Asyncio) +DESCRIPTION: Optimizes task naming in asyncio by converting task names to strings earlier in the process. This change was contributed by Thomas Grainger. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_155 + +LANGUAGE: Python +CODE: +``` +Changed task names to be converted to ``str`` early on asyncio (PR by Thomas Grainger) +``` + +-------------------------------- + +TITLE: Add lock Parameter to create_condition() +DESCRIPTION: Introduces the `lock` parameter to `anyio.create_condition()`. This allows users to provide their own lock object to be used with the condition variable, offering greater flexibility in synchronization patterns. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_251 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def custom_condition(): + my_lock = anyio.Lock() + condition = anyio.create_condition(lock=my_lock) + # Use the condition variable with the provided lock +``` + +-------------------------------- + +TITLE: Move On After a Timeout +DESCRIPTION: Creates a cancel scope that automatically cancels its operations after a specified duration. If the block finishes before the timeout, the scope is exited normally. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_12 + +LANGUAGE: Python +CODE: +``` +async with anyio.move_on_after(5): + await some_operation() +``` + +-------------------------------- + +TITLE: Set Total Tokens for CapacityLimiter +DESCRIPTION: Adds the `set_total_tokens()` method to the `CapacityLimiter` class. This allows users to dynamically adjust the total number of tokens available in a capacity limiter, providing more control over resource allocation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_224 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def manage_capacity(): + limiter = anyio.CapacityLimiter(10) + await limiter.set_total_tokens(20) + # Further operations using the updated limiter +``` + +-------------------------------- + +TITLE: Fix Linux Abstract Namespace Support for UNIX Sockets +DESCRIPTION: Corrects a regression in v4.2 that broke support for Linux abstract namespaces in UNIX sockets. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_57 + +LANGUAGE: Python +CODE: +``` +import anyio +import sys + +async def connect_to_abstract_unix_socket(socket_name): + if sys.platform.startswith("linux"): + try: + # Abstract namespace sockets are prefixed with null byte + abstract_socket_path = b'\x00' + socket_name.encode('utf-8') + + # Attempt to connect using the abstract socket path + # Note: anyio.connect_unix might need specific handling for abstract sockets + # This example assumes direct support or a workaround. + + # A more robust solution might involve lower-level socket operations if anyio doesn't directly support it. + # For demonstration, let's assume a hypothetical direct support: + # stream = await anyio.connect_unix(abstract_socket_path) + + print(f"Attempting to connect to abstract socket: {socket_name}") + # Placeholder for actual connection logic + await anyio.sleep(1) # Simulate connection attempt + print("Connection logic would go here.") + + except Exception as e: + print(f"Error connecting to abstract socket: {e}") + else: + print("Abstract namespace UNIX sockets are a Linux-specific feature.") + +# Example usage (requires a server listening on an abstract socket): +# anyio.run(connect_to_abstract_unix_socket, "my_abstract_socket") +``` + +-------------------------------- + +TITLE: Add Socket Parameters in AnyIO +DESCRIPTION: Version 1.4.0 of AnyIO adds 'family' and 'reuse_address' parameters to anyio.create_udp_socket() to enable multicast support. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_217 + +LANGUAGE: Python +CODE: +``` +Added the family and reuse_address parameters to anyio.create_udp_socket() +``` + +-------------------------------- + +TITLE: Drop Support for Old-Style Coroutine Functions (AnyIO 4.0.0) +DESCRIPTION: AnyIO version 4.0.0 no longer supports spawning tasks from old-style coroutine functions decorated with `@asyncio.coroutine`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_80 + +LANGUAGE: python +CODE: +``` +**BACKWARDS INCOMPATIBLE** Dropped support for spawning tasks from old-style coroutine + functions (``@asyncio.coroutine``) +``` + +-------------------------------- + +TITLE: Add TaskInfo.has_pending_cancellation() Method +DESCRIPTION: Introduces the 'TaskInfo.has_pending_cancellation()' method, providing a way to check if a task has a pending cancellation request. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_61 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def task_to_check(): + await anyio.sleep(10) + +async def check_task_cancellation(): + task = await anyio.create_task(task_to_check()) + await anyio.sleep(0.1) # Allow task to start + + info = task.get_info() + print(f"Initially, has pending cancellation: {info.has_pending_cancellation()}") + + task.cancel() + await anyio.sleep(0.1) # Allow cancellation to be processed + + info = task.get_info() + print(f"After cancelling, has pending cancellation: {info.has_pending_cancellation()}") + +# anyio.run(check_task_cancellation) +``` + +-------------------------------- + +TITLE: TLS Stream Interface +DESCRIPTION: Defines the interface for streams secured with TLS, handling the encryption and decryption of data. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_62 + +LANGUAGE: Python +CODE: +``` +async def send_secure_data(stream: anyio.streams.tls.TLSStream, data: bytes): + await stream.send_bytes(data) +``` + +-------------------------------- + +TITLE: Low-level Checkpoint Functions +DESCRIPTION: Adds low-level checkpointing functions: `checkpoint()`, `checkpoint_if_cancelled()`, and `cancel_shielded_checkpoint()`. These provide fine-grained control over cancellation points in asynchronous code. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_166 + +LANGUAGE: Python +CODE: +``` +anyio.lowlevel.checkpoint() +``` + +LANGUAGE: Python +CODE: +``` +anyio.lowlevel.checkpoint_if_cancelled() +``` + +LANGUAGE: Python +CODE: +``` +anyio.lowlevel.cancel_shielded_checkpoint() +``` + +-------------------------------- + +TITLE: Asyncio StreamWriter Close and Wait Closed +DESCRIPTION: Closing an asyncio stream requires calling close() followed by awaiting wait_closed() to confirm the stream has finished closing. AnyIO aims to simplify this process. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_11 + +LANGUAGE: Python +CODE: +``` +writer.close() +await writer.wait_closed() +``` + +-------------------------------- + +TITLE: Stream Connectable Interfaces +DESCRIPTION: Defines interfaces for objects that can be connected to, representing network endpoints or other stream sources. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_47 + +LANGUAGE: Python +CODE: +``` +connectable: anyio.abc.ByteStreamConnectable +stream = await anyio.connect_tcp('localhost', 8080) +``` + +-------------------------------- + +TITLE: Add SO_REUSEPORT to AnyIO Sockets +DESCRIPTION: This update adds support for the SO_REUSEPORT option to TCP listeners and UDP sockets in AnyIO, allowing multiple sockets to bind to the same address and port. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_211 + +LANGUAGE: Python +CODE: +``` +Support for the SO_REUSEPORT option has been added to TCP listeners and UDP sockets +``` + +-------------------------------- + +TITLE: UNIXSocketStream for File Descriptors +DESCRIPTION: Adds the `UNIXSocketStream` class, enabling the sending and receiving of file descriptors over UNIX domain sockets. This is crucial for inter-process communication scenarios requiring file descriptor transfer. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_160 + +LANGUAGE: Python +CODE: +``` +anyio.UNIXSocketStream +``` + +-------------------------------- + +TITLE: Run Synchronous Function from Worker Thread (AnyIO) +DESCRIPTION: Enables calling a synchronous function in the event loop thread from an AnyIO worker thread. It waits for the function to complete and returns its value, unlike asyncio's call_soon_threadsafe. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_15 + +LANGUAGE: Python +CODE: +``` +anyio.from_thread.run_sync(func, *args, backend=None, backend_options=None) +``` + +-------------------------------- + +TITLE: Object Stream Interfaces +DESCRIPTION: Defines interfaces for reliable object streams, guaranteeing that data is delivered in order and without loss. Suitable for most application-level communication. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_44 + +LANGUAGE: Python +CODE: +``` +async def send_objects(stream: anyio.abc.ObjectSendStream, objects): + for obj in objects: + await stream.send(obj) +``` + +-------------------------------- + +TITLE: Update TaskGroup for eager task factories +DESCRIPTION: Ensures `TaskGroup` functions correctly with asyncio's eager task factories. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_27 + +LANGUAGE: Python +CODE: +``` +Updated ``TaskGroup`` to work with asyncio's eager task factories + (`#764 `_) +``` + +-------------------------------- + +TITLE: Fix CancelScope.cancel() on asyncio before scope entry +DESCRIPTION: Corrects an issue where CancelScope.cancel() did not function correctly on asyncio if called prior to entering the scope. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_105 + +LANGUAGE: Python +CODE: +``` +from anyio import CancelScope + +# Example scenario: +# scope = CancelScope() +# scope.cancel() +# with scope: +# pass # Cancellation should now be handled correctly +``` + +-------------------------------- + +TITLE: Add Support for PEP 561 +DESCRIPTION: Adds support for PEP 561, which concerns the distribution of type information in Python packages. This ensures that AnyIO packages can be correctly processed by type checkers and other tooling. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_237 + +LANGUAGE: Python +CODE: +``` +# No direct code snippet, but indicates package structure compliance +# This ensures type checkers like MyPy can understand AnyIO's types. +``` + +-------------------------------- + +TITLE: Run Synchronous Function in Worker Thread (AnyIO) +DESCRIPTION: Executes a synchronous function in a worker thread using AnyIO's thread pooling mechanism. It supports context variable propagation and allows specifying a capacity limiter. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/why.rst#_snippet_13 + +LANGUAGE: Python +CODE: +``` +anyio.to_thread.run_sync(func, *args, limiter=None, cancellable=False, backend=None, backend_options=None) +``` + +-------------------------------- + +TITLE: Fix misleading ValueError on DNS failures +DESCRIPTION: Corrects a misleading `ValueError` that could be raised in scenarios involving DNS failures. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_31 + +LANGUAGE: Python +CODE: +``` +Fixed a misleading ``ValueError`` in the context of DNS failures + (`#815 `_; PR by @graingert) +``` + +-------------------------------- + +TITLE: Modify anyio.Path.relative_to() Signature (AnyIO 4.0.0) +DESCRIPTION: In AnyIO 4.0.0, the `anyio.Path.relative_to()` and `anyio.Path.is_relative_to()` methods were updated to accept only one argument, aligning with Python 3.12's deprecation of multiple arguments for these functions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_79 + +LANGUAGE: python +CODE: +``` +**BACKWARDS INCOMPATIBLE** Changed ``anyio.Path.relative_to()`` and + ``anyio.Path.is_relative_to()`` to only accept one argument, as passing multiple + arguments is deprecated as of Python 3.12 +``` + +-------------------------------- + +TITLE: AnyIO IPv6 Address Reporting +DESCRIPTION: This snippet explains a backwards-incompatible change in AnyIO 2.0.0 regarding IPv6 address reporting. IPv6 addresses are now reported as 2-tuples, with a non-zero scope ID appended to the address with '%' as a separator if present in the original 4-tuple. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_200 + +LANGUAGE: Python +CODE: +``` +IPv6 addresses are now reported as 2-tuples. If original 4-tuple form contains a nonzero scope ID, it is appended to the address with "%" as the separator. +``` + +-------------------------------- + +TITLE: Fix SocketListener Close ValueError +DESCRIPTION: Resolves `ValueError: Invalid file descriptor: -1` when closing a `SocketListener` on asyncio, ensuring proper resource cleanup. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_193 + +LANGUAGE: Python +CODE: +``` +asyncio SocketListener close() +``` + +-------------------------------- + +TITLE: Use asyncio.Runner on Asyncio Backend (AnyIO 4.0.0) +DESCRIPTION: AnyIO version 4.0.0 now utilizes `asyncio.Runner` (or a back-ported version for older Python versions) on the asyncio backend for running asynchronous operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_82 + +LANGUAGE: python +CODE: +``` +Changed ``anyio.run()`` to use ``asyncio.Runner`` (or a back-ported version of it on + Pythons older than 3.11) on the ``asyncio`` backend +``` + +-------------------------------- + +TITLE: Add feed_data() to BufferedByteReceiveStream +DESCRIPTION: Introduces the `feed_data()` method to the `BufferedByteReceiveStream` class, enabling direct data injection into the buffer. This is useful for managing stream data programmatically. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +from anyio.streams.buffered_io import BufferedByteReceiveStream + +# Example usage: +# stream = BufferedByteReceiveStream() +# stream.feed_data(b'some data') + +``` + +-------------------------------- + +TITLE: AnyIO Function Renames and Removals +DESCRIPTION: This snippet lists several backwards-incompatible changes in AnyIO 2.0.0, including function renames and removals. It highlights the replacement of `finalize()` with `contextlib.aclosing()` and renames for signal receivers, thread execution, and thread limiters. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_196 + +LANGUAGE: Python +CODE: +``` +"finalize()" → (removed; use "contextlib.aclosing()" instead) +"receive_signals()" → "open_signal_receiver()" +"run_in_thread()" → "run_sync_in_worker_thread()" +"current_default_thread_limiter()" → "current_default_worker_thread_limiter()" +"ResourceBusyError" → "BusyResourceError" +``` + +-------------------------------- + +TITLE: Buffered Byte Receive Stream +DESCRIPTION: A byte stream that buffers data read from the underlying stream. This can improve performance by reducing the number of read operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_49 + +LANGUAGE: Python +CODE: +``` +buffered_stream = anyio.streams.buffered.BufferedByteReceiveStream(underlying_stream) +``` + +-------------------------------- + +TITLE: Socket Read/Write Operations +DESCRIPTION: Enables waiting for socket readability or writability. This is crucial for non-blocking I/O operations, allowing programs to efficiently handle network events without blocking the main thread. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_70 + +LANGUAGE: Python +CODE: +``` +anyio.wait_readable +anyio.wait_socket_readable +anyio.wait_socket_writable +anyio.wait_writable +``` + +-------------------------------- + +TITLE: Fix AssertionError with nest-asyncio +DESCRIPTION: Resolves an `AssertionError` that occurred when using the `nest-asyncio` library. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_25 + +LANGUAGE: Python +CODE: +``` +Fixed ``AssertionError`` when using ``nest-asyncio`` + (`#840 `_) +``` + +-------------------------------- + +TITLE: Worker Threads Spawned with daemon=True +DESCRIPTION: Ensures that worker threads are spawned with `daemon=True` across all AnyIO backends, not just trio. This setting allows the application to exit even if these worker threads are still running. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_250 + +LANGUAGE: Python +CODE: +``` +# This is an internal implementation detail change, affecting how threads are managed. +``` + +-------------------------------- + +TITLE: Fix CapacityLimiter FIFO ordering on asyncio +DESCRIPTION: Ensures that waiting tasks for the CapacityLimiter on the asyncio backend are ordered in a First-In, First-Out (FIFO) manner, correcting a previous Last-In, First-Out (LIFO) behavior. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_104 + +LANGUAGE: Python +CODE: +``` +from anyio import CapacityLimiter + +# The CapacityLimiter now processes waiting tasks in the order they were added. +``` + +-------------------------------- + +TITLE: Fix Inconsistent Queue Capacity +DESCRIPTION: Corrects the inconsistent behavior of queue capacity across different AnyIO backends when the capacity is defined as 0. Previously, Trio treated 0 capacity as infinite, while others behaved differently. This fix harmonizes the behavior. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_246 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def create_and_use_queue(): + queue = anyio.Queue(0) # Capacity is now consistently handled across backends +``` + +-------------------------------- + +TITLE: Update File I/O API in AnyIO +DESCRIPTION: This section details backwards incompatible changes to the asynchronous file I/O API in AnyIO, including the adoption of a common codebase and changes to function names. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_213 + +LANGUAGE: Python +CODE: +``` +Asynchronous file I/O functionality now uses a common code base (anyio.AsyncFile) instead of backend-native classes +aopen() → open_file() +AsyncFileclose() → AsyncFileaclose() +``` + +-------------------------------- + +TITLE: Connect UDP Socket to Specific Destination +DESCRIPTION: Creates a UDP socket connected to a specific remote host and port. This allows sending data without specifying the destination address each time, optimizing for frequent transmissions to the same peer. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +from anyio import create_connected_udp_socket, run + + +async def main(): + async with await create_connected_udp_socket( + remote_host='hostname', remote_port=1234) as udp: + await udp.send(b'Hi there!\n') + +run(main) +``` + +-------------------------------- + +TITLE: Fix TLSStream TypeError on Windows with Truststore +DESCRIPTION: Resolves a TypeError occurring with TLSStream on Windows when a certificate verification error happens while using a truststore SSL certificate. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_45 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def connect_with_truststore(): + try: + # This connection might raise TypeError on Windows with truststore and verification errors + stream = await anyio.connect_tls("example.com", 443, backend='asyncio', trust_roots=anyio.get_cancelled_exc_class()) + await stream.receive(1024) + except Exception as e: + print(f"An error occurred: {e}") +``` + +-------------------------------- + +TITLE: Fix TaskGroup traceback formatting in asyncio +DESCRIPTION: Addresses an issue where traceback formatting in `TaskGroup` could grow quadratically with nesting levels in asyncio, particularly when raising `ExceptionGroups` in `TaskGroup.__aexit__`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_18 + +LANGUAGE: Python +CODE: +``` +Fixed traceback formatting growing quadratically with level of ``TaskGroup`` + nesting on asyncio due to exception chaining when raising ``ExceptionGroups`` + in ``TaskGroup.__aexit__`` + (`#863 `_; PR by @tapetersen) +``` + +-------------------------------- + +TITLE: Writable CapacityLimiter Total Tokens +DESCRIPTION: Makes the `CapacityLimiter.total_tokens` property writable, allowing direct modification of the total available tokens. The `set_total_tokens()` method is now deprecated. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_170 + +LANGUAGE: Python +CODE: +``` +anyio.CapacityLimiter.total_tokens +``` + +-------------------------------- + +TITLE: Run Async Function from Another Thread +DESCRIPTION: Allows running an asynchronous function from a synchronous thread. It bridges the gap between threaded and asynchronous code execution. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_25 + +LANGUAGE: Python +CODE: +``` +anyio.from_thread.run(async_function, arg1) +``` + +-------------------------------- + +TITLE: Call Synchronous Code in Event Loop Thread (AnyIO) +DESCRIPTION: Illustrates how to call synchronous code that needs to interact with the event loop thread from a worker thread. This is necessary for thread-unsafe operations, using `anyio.from_thread.run_sync` to safely execute the call within the event loop's context. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_2 + +LANGUAGE: Python +CODE: +``` +import time + +from anyio import Event, from_thread, to_thread, run + +def worker(event): + time.sleep(1) + from_thread.run_sync(event.set) + +async def main(): + event = Event() + await to_thread.run_sync(worker, event) + await event.wait() + +run(main) +``` + +-------------------------------- + +TITLE: Fix UNIX Socket Listener Non-Blocking Mode (Asyncio) +DESCRIPTION: Ensures that UNIX sockets created by a listener are set to non-blocking mode on asyncio. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_129 + +LANGUAGE: Python +CODE: +``` +Fixed UNIX socket listener not setting accepted sockets to non-blocking mode on + asyncio +``` + +-------------------------------- + +TITLE: Connect UNIX Datagram Socket to Specific Path +DESCRIPTION: Creates a UNIX datagram socket connected to a specific remote path. This is useful for sending multiple packets to a single destination without repeatedly specifying the path, similar to the connected UDP socket. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/networking.rst#_snippet_9 + +LANGUAGE: Python +CODE: +``` +from anyio import create_connected_unix_datagram_socket, run + + +async def main(): + async with await create_connected_unix_datagram_socket( + remote_path='/dev/log' + ) as unix_dg: + await unix_dg.send(b'Hi there!\n') + +run(main) +``` + +-------------------------------- + +TITLE: Fix from_thread.run and run_sync Sniffio Issue on Asyncio +DESCRIPTION: Resolves an issue where `from_thread.run` and `from_thread.run_sync` did not correctly set sniffio on the asyncio backend. This fixes failures when calling sniffio-dependent functions and deadlocks in specific scenarios. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_87 + +LANGUAGE: python +CODE: +``` +Fixed ``from_thread.run`` and ``from_thread.run_sync`` not setting sniffio on asyncio. + As a result: + + - Fixed ``from_thread.run_sync`` failing when used to call sniffio-dependent functions + on asyncio + - Fixed ``from_thread.run`` failing when used to call sniffio-dependent functions on + asyncio from a thread running trio or curio + - Fixed deadlock when using ``from_thread.start_blocking_portal(backend="asyncio")`` + in a thread running trio or curio (PR by Ganden Schaffner) +``` + +-------------------------------- + +TITLE: Notify closing for wait_readable/wait_writable +DESCRIPTION: Adds `anyio.notify_closing` to wake up tasks waiting on `anyio.wait_readable` and `anyio.wait_writable` before a socket is closed. This prevents potential `OSError` exceptions, particularly on ProactorEventLoop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def example(): + # Assume sock is a socket object + # await anyio.wait_writable(sock) + # anyio.notify_closing(sock) + pass + +``` + +-------------------------------- + +TITLE: FileReadStream and FileWriteStream +DESCRIPTION: Introduces `FileReadStream` and `FileWriteStream` for asynchronous file I/O operations. These classes provide an asynchronous interface for reading from and writing to files. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_161 + +LANGUAGE: Python +CODE: +``` +anyio.FileReadStream +``` + +LANGUAGE: Python +CODE: +``` +anyio.FileWriteStream +``` + +-------------------------------- + +TITLE: Fix RunVar name conflicts +DESCRIPTION: Corrects an issue where `RunVar` instances with the same name could incorrectly share storage. This ensures that each `RunVar` instance maintains its own independent storage, preventing name-based conflicts. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_10 + +LANGUAGE: Python +CODE: +``` +import anyio + +# Example usage: +# var1 = anyio.RunVar('my_var', default=1) +# var2 = anyio.RunVar('my_var', default=2) +# # Previously, var1 and var2 might have shared storage. Now they don't. + +``` + +-------------------------------- + +TITLE: Fail After a Timeout +DESCRIPTION: Creates a cancel scope that raises an exception (typically `anyio.get_cancelled_exc_class()`) if the operations within it do not complete within the specified duration. This is useful for enforcing deadlines. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_13 + +LANGUAGE: Python +CODE: +``` +async with anyio.fail_after(10): + await long_running_operation() +``` + +-------------------------------- + +TITLE: AnyIO TLS and Buffering Changes +DESCRIPTION: This snippet details backwards-incompatible changes in AnyIO 2.0.0 related to TLS and buffering. TLS functionality is now split from `SocketStream` and can work over any bidirectional bytes-based stream. Buffering functions like `receive_until()` and `receive_exactly()` are now in a separate stream wrapper class. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_199 + +LANGUAGE: Python +CODE: +``` +TLS functionality has been split off from `SocketStream` and can now work over any bidirectional bytes-based stream – you can now establish a TLS encrypted communications pathway over UNIX sockets or even memory object streams. The `TLSRequired` exception has also been removed as it is no longer necessary. +Buffering functionality (`receive_until()` and `receive_exactly()`) was split off from `SocketStream` into a stream wrapper class (`anyio.streams.buffered.BufferedByteReceiveStream`) +``` + +-------------------------------- + +TITLE: Fix KeyError in Cancel Scope on Asyncio/Curio +DESCRIPTION: Resolves a `KeyError` that occurred on asyncio and curio backends when entering and exiting a cancel scope across different tasks. This ensures correct state management for cancellation scopes in concurrent scenarios. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_248 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def task_a(): + async with anyio.open_cancel_scope(): + pass + +async def task_b(): + # This operation should now be safe without KeyErrors +``` + +-------------------------------- + +TITLE: AnyIO Cancellation Check with sleep(0) +DESCRIPTION: This snippet describes a change in AnyIO 2.0.0 for both asyncio and curio backends. A cancellation check now calls `sleep(0)`, which allows the scheduler to switch to a different task, improving responsiveness during cancellation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_204 + +LANGUAGE: Python +CODE: +``` +a cancellation check now calls `sleep(0)`, allowing the scheduler to switch to a different task +``` + +-------------------------------- + +TITLE: Run Sync Function from Another Thread +DESCRIPTION: Executes a synchronous function from within an asynchronous context, typically when called from another thread. It ensures the synchronous function runs without blocking the event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_26 + +LANGUAGE: Python +CODE: +``` +anyio.from_thread.run_sync(sync_function) +``` + +-------------------------------- + +TITLE: Fix Corner Cases of Cancellation +DESCRIPTION: Addresses several corner cases related to task cancellation, ensuring that AnyIO's behavior on asyncio and curio backends more closely matches that of trio. This leads to more consistent cancellation handling across different backends. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_257 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def test_cancellation_consistency(): + # Test scenarios involving cancellation across different backends +``` + +-------------------------------- + +TITLE: Add Support for Python 3.12 and Drop Python 3.7 (AnyIO 4.0.0) +DESCRIPTION: AnyIO version 4.0.0 introduces support for Python 3.12 and drops support for Python 3.7, updating the minimum required Python version. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_83 + +LANGUAGE: python +CODE: +``` +Dropped support for Python 3.7 +Added support for Python 3.12 +``` + +-------------------------------- + +TITLE: AnyIO Socket API Changes +DESCRIPTION: This snippet details backwards-incompatible changes to the AnyIO socket API in version 2.0.0. It includes changes to TCP and UDP server creation functions, parameter renames for UDP socket creation, and modifications to TCP connection parameters, including TLS handling. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_197 + +LANGUAGE: Python +CODE: +``` +"create_tcp_server()" → "create_tcp_listener()" +"create_unix_server()" → "create_unix_listener()" +"create_udp_socket()" had some of its parameters changed: + + "interface" → "local_address" + "port" → "local_port" + "reuse_address" was replaced with "reuse_port" (and sets "SO_REUSEPORT" instead of "SO_REUSEADDR") +"connect_tcp()" had some of its parameters changed: + + "address" → "remote_address" + "port" → "remote_port" + "bind_host" → "local_address" + "bind_port" → (removed) + "autostart_tls" → "tls" + "tls_hostname" (new parameter, when you want to match the certificate against against something else than "remote_address") +``` + +-------------------------------- + +TITLE: Socket ABCs +DESCRIPTION: Defines abstract base classes for various socket types, including generic socket attributes, stream sockets, UDP sockets, and UNIX domain sockets. These classes provide a common interface for interacting with different socket implementations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_71 + +LANGUAGE: Python +CODE: +``` +anyio.abc.SocketAttribute +anyio.abc.SocketStream +anyio.abc.SocketListener +anyio.abc.UDPSocket +anyio.abc.ConnectedUDPSocket +anyio.abc.UNIXSocketStream +anyio.abc.UNIXDatagramSocket +anyio.abc.ConnectedUNIXDatagramSocket +``` + +-------------------------------- + +TITLE: Fix Socket Creation Failure Crashing connect_tcp() +DESCRIPTION: Prevents `connect_tcp()` from crashing due to socket creation failures. The function now handles these errors more gracefully, ensuring that connection attempts do not terminate the entire process. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_247 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def connect_safely(host, port): + try: + async with await anyio.connect_tcp(host, port) as stream: + pass + except Exception as e: + print(f"Connection failed: {e}") # Handles socket creation errors +``` + +-------------------------------- + +TITLE: Fix Deprecation Warnings for asyncio.Event() +DESCRIPTION: Addresses deprecation warnings related to the `loop` argument of `asyncio.Event()` on Python 3.8. This update ensures compatibility with newer Python versions and removes unnecessary warnings. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_249 + +LANGUAGE: Python +CODE: +``` +import anyio +import asyncio + +async def use_event_without_warnings(): + # The asyncio backend now handles Event creation correctly without the loop argument +``` + +-------------------------------- + +TITLE: Fix Cancellation Exception Bubbling in Except* Blocks (AnyIO) +DESCRIPTION: Corrects a discrepancy between asyncio and trio regarding how cancellation exceptions bubble out of cancel scopes when reraised in an 'except*' block. This ensures consistent cancellation behavior across backends. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_75 + +LANGUAGE: python +CODE: +``` +Fixed discrepancy between ``asyncio`` and ``trio`` where reraising a cancellation + exception in an ``except*`` block would incorrectly bubble out of its cancel scope + (`#634 `_) +``` + +-------------------------------- + +TITLE: Fix RuntimeError in start_blocking_portal on event loop close +DESCRIPTION: Prevents an unwarranted RuntimeError: 'This portal is not running' when a task raises an exception that causes the event loop to close. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_108 + +LANGUAGE: Python +CODE: +``` +from anyio import start_blocking_portal + +# The fix ensures proper handling of exceptions during portal operation +# when the underlying event loop is being shut down. +``` + +-------------------------------- + +TITLE: Fix OP_IGNORE_UNEXPECTED_EOF flag in TLSStream.wrap() +DESCRIPTION: Addresses an issue where the OP_IGNORE_UNEXPECTED_EOF flag was inadvertently set on Python 3.11.3 and 3.10.11 for SSL contexts created by default in TLSStream.wrap(). + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_110 + +LANGUAGE: Python +CODE: +``` +from anyio.streams.tls import TLSStream + +# The fix ensures the flag is not incorrectly applied on specific Python versions. +``` + +-------------------------------- + +TITLE: Fix Trio v0.13 Compatibility on Windows +DESCRIPTION: Ensures compatibility with Trio version 0.13 specifically on Windows operating systems. This resolves potential issues that may arise from differences in how Trio handles concurrency on Windows. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_245 + +LANGUAGE: Python +CODE: +``` +# No direct code snippet, but indicates internal adjustments for Trio compatibility on Windows. +``` + +-------------------------------- + +TITLE: Fix Type Annotation for DeprecatedAwaitable +DESCRIPTION: Updates the type annotation for ``DeprecatedAwaitable(|List|Float).__await__`` to align with the ``typing.Awaitable`` protocol. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_150 + +LANGUAGE: Python +CODE: +``` +Fixed the type annotation of ``DeprecatedAwaitable(|List|Float).__await__`` to match + the ``typing.Awaitable`` protocol +``` + +-------------------------------- + +TITLE: Fix Process stdin/stdout/stderr aclose checkpoint on asyncio +DESCRIPTION: Ensures that closing the standard input, output, or error streams of a process on the asyncio backend includes a checkpoint, preventing potential issues during cancellation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_66 + +LANGUAGE: Python +CODE: +``` +async def aclose(self) -> None: + """Close the stream.""" + # ... implementation details ... + pass +``` + +-------------------------------- + +TITLE: Fix CapacityLimiter edge case on asyncio +DESCRIPTION: Resolves an edge case in `CapacityLimiter` on the asyncio backend where a cancelled task waiting for a token might incorrectly receive and use the token, failing to notify the next waiting task. This ensures fair token distribution. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_12 + +LANGUAGE: Python +CODE: +``` +import anyio + +# async def use_capacity_limiter(): +# limiter = anyio.CapacityLimiter(1) +# async with limiter: +# # acquire token +# pass + +``` + +-------------------------------- + +TITLE: Fix rollover boundary check in SpooledTemporaryFile +DESCRIPTION: Addresses an issue in `SpooledTemporaryFile` where the rollover boundary check was too strict. The fix ensures rollover only occurs when the buffer size actually exceeds `max_size`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +from anyio.tempfile import SpooledTemporaryFile + +# Example usage: +# with SpooledTemporaryFile(max_size=1024) as tmp: +# tmp.write(b'data' * 500) +# # Rollover occurs here if buffer exceeds 1024 bytes + +``` + +-------------------------------- + +TITLE: Improve type annotations for coroutine-like inputs +DESCRIPTION: Several functions and methods now accept 'Awaitable[Any]' instead of 'Coroutine[Any, Any, Any]' for return types of callables. This broadens compatibility with coroutine-like inputs, such as async generator asend objects. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_102 + +LANGUAGE: Python +CODE: +``` +from typing import Any, Awaitable + +# Example of updated type hint: +def run_async_function(func: Callable[[], Awaitable[Any]]) -> None: + pass +``` + +-------------------------------- + +TITLE: Use Capacity Limiters for Thread Pool +DESCRIPTION: Adds the capability to use capacity limiters for controlling the maximum number of threads that can be spawned or used. This enhances resource management for thread-based operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_232 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def run_with_thread_limit(): + thread_limiter = anyio.CapacityLimiter(5) # Limit to 5 threads + await anyio.run_in_thread(some_function, limiter=thread_limiter) +``` + +-------------------------------- + +TITLE: AnyIO API Changes: Submodules, Method Signatures, and Renames +DESCRIPTION: Details backwards incompatible changes in AnyIO 3.0.0, including making submodules private, converting coroutine methods to synchronous, and renaming/moving functions. It also notes changes in context manager behavior and method signatures. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_157 + +LANGUAGE: Python +CODE: +``` +# Old usage (now deprecated or removed): +# import anyio.abc.submodule +# await anyio.current_time() +# async with anyio.open_cancel_scope(): ... +# await anyio.create_blocking_portal() + +# New usage: +import anyio + +# Accessing ABCs directly from anyio +# Example: anyio.abc.Task + +# Synchronous methods (previously coroutines): +# MemoryObjectReceiveStream.receive_nowait() + +# Awaiting these now emits a deprecation warning: +# await anyio.current_time() + +# Synchronous context managers: +# with anyio.CancelScope() as scope: ... + +# Renamed/moved functions: +# portal = anyio.from_thread.BlockingPortal() +# limiter = anyio.CapacityLimiter() +# event = anyio.Event() +# lock = anyio.Lock() +# condition = anyio.Condition() +# semaphore = anyio.Semaphore() +# anyio.to_thread.current_default_thread_limiter() +# scope = anyio.CancelScope() +# anyio.to_thread.run_sync(lambda: None) +# anyio.from_thread.run(lambda: None) +# anyio.from_thread.run_sync(lambda: None) + +# BlockingPortal usage: +# with anyio.from_thread.BlockingPortal() as portal: +# portal.start_task_soon(lambda: None) +# portal.stop_from_external_thread() # Use portal.call(portal.stop) instead + +# TaskGroup usage: +# async with anyio.create_task_group() as tg: +# tg.start_soon(lambda: None) # Previously tg.spawn() + +# CapacityLimiter usage: +# limiter.total_tokens = 10 # Previously limiter.set_total_tokens(10) +``` + +-------------------------------- + +TITLE: Fix IPv6 Support in Network Functions +DESCRIPTION: Improves IPv6 support in several network functions, including `create_tcp_server()`, `connect_tcp()`, and `create_udp_socket()`. This ensures reliable operation with IPv6 addresses. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_252 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def create_ipv6_server(): + server = await anyio.create_tcp_server(port=8080, family=socket.AF_INET6) + # IPv6 server creation is now reliable +``` + +-------------------------------- + +TITLE: Fix MemoryObjectItemReceiver __repr__ +DESCRIPTION: Corrects the '__repr__()' method of 'MemoryObjectItemReceiver' when the 'item' attribute is not defined, preventing potential errors. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_51 + +LANGUAGE: Python +CODE: +``` +import anyio + +# Assuming MemoryObjectItemReceiver is part of anyio's internal implementation +# This is a conceptual example of how the fix might be applied. + +class MockMemoryObjectItemReceiver: + def __init__(self, item=None): + self.item = item + + def __repr__(self): + if self.item is not None: + return f"MockMemoryObjectItemReceiver(item={self.item!r})" + else: + return "MockMemoryObjectItemReceiver(item=undefined)" + +# Example usage: +receiver_with_item = MockMemoryObjectItemReceiver(item=123) +print(repr(receiver_with_item)) + +receiver_without_item = MockMemoryObjectItemReceiver() +print(repr(receiver_without_item)) +``` + +-------------------------------- + +TITLE: Fix Unnecessary Delay in connect_tcp() +DESCRIPTION: Eliminates an unnecessary delay in `connect_tcp()` that occurred when an earlier connection attempt succeeded. This optimization improves the performance of establishing TCP connections. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_242 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def connect_efficiently(host, port): + async with await anyio.connect_tcp(host, port) as stream: + # Connection established without unnecessary delays +``` + +-------------------------------- + +TITLE: Fix SocketStream.receive() returning bytearray on Windows Asyncio +DESCRIPTION: Corrects an issue where SocketStream.receive() returned a 'bytearray' instead of 'bytes' when using asyncio with ProactorEventLoop on Windows. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_55 + +LANGUAGE: Python +CODE: +``` +import anyio +import sys + +async def receive_bytes_on_windows(): + if sys.platform == "win32": + try: + # This requires setting up a server and client connection + # For demonstration, we'll simulate the expected behavior. + # In a real scenario, you'd connect to a server. + + # Mocking a stream that might return bytearray + class MockSocketStream: + async def receive(self, max_bytes): + return bytearray(b'test data') # Simulating the problematic return type + + stream = MockSocketStream() + data = await stream.receive(10) + + # The fix ensures 'data' is bytes + if isinstance(data, bytearray): + print("Received data is bytearray (potential issue).") + data = bytes(data) + + print(f"Received data type: {type(data)}") + print(f"Received data: {data}") + + except Exception as e: + print(f"An error occurred: {e}") + else: + print("This test is specific to Windows with ProactorEventLoop.") + +# anyio.run(receive_bytes_on_windows) +``` + +-------------------------------- + +TITLE: Fix to_thread.run_sync() hanging on StopIteration on asyncio +DESCRIPTION: Resolves a hang issue in to_thread.run_sync() on asyncio when the target callable raises StopIteration. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_107 + +LANGUAGE: Python +CODE: +``` +from anyio.to_thread import run_sync + +# def function_raising_stop_iteration(): +# raise StopIteration + +# run_sync(function_raising_stop_iteration) # Should no longer hang +``` + +-------------------------------- + +TITLE: Update asyncio Backend Policy Option to loop_factory (AnyIO 4.0.0) +DESCRIPTION: The 'policy' option for the asyncio backend in AnyIO has been renamed to 'loop_factory' in version 4.0.0 to better accommodate `asyncio.Runner`. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_81 + +LANGUAGE: python +CODE: +``` +**BACKWARDS INCOMPATIBLE** The ``policy`` option on the ``asyncio`` backend was + changed to ``loop_factory`` to accommodate ``asyncio.Runner`` +``` + +-------------------------------- + +TITLE: Bump Minimum Trio Version (AnyIO 4.0.0) +DESCRIPTION: AnyIO version 4.0.0 increases the minimum required version of the trio library to v0.22. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_84 + +LANGUAGE: python +CODE: +``` +Bumped minimum version of trio to v0.22 +``` + +-------------------------------- + +TITLE: AnyIO curio Backend Fixes +DESCRIPTION: This snippet outlines fixes and improvements for the AnyIO curio backend in version 2.0.0. It addresses a limitation where tasks could only be cancelled twice and includes workarounds for task cancellation issues. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_203 + +LANGUAGE: Python +CODE: +``` +Worked around the limitation where a task can only be cancelled twice (any cancellations beyond that were ignored) +``` + +-------------------------------- + +TITLE: Support voluntary thread cancellation +DESCRIPTION: Introduces `anyio.from_thread.check_cancelled()` to allow voluntary thread cancellation, enabling threads to periodically check if they have been requested to cancel. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_73 + +LANGUAGE: Python +CODE: +``` +from anyio.from_thread import check_cancelled + +def my_thread_function(): + # ... some work ... + check_cancelled() + # ... more work ... + pass +``` + +-------------------------------- + +TITLE: Add Support for CancelScope.shield Mutation +DESCRIPTION: Enables mutation of ``CancelScope.shield``, providing more control over cancellation behavior. This feature was contributed by John Belmonte. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_152 + +LANGUAGE: Python +CODE: +``` +Added support for mutation of ``CancelScope.shield`` (PR by John Belmonte) +``` + +-------------------------------- + +TITLE: Improve type annotations with TypeVarTuple +DESCRIPTION: Enhances type checking for functions accepting variadic positional arguments by utilizing PEP 646 TypeVarTuple, improving static analysis for various AnyIO functions and methods. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_69 + +LANGUAGE: Python +CODE: +``` +from typing import TypeVarTuple, Tuple + +Ts = TypeVarTuple('Ts') + +def start_soon(self, func: Callable[..., Any], *args: Unpack[Ts]) -> None: + """Start a new task in the task group.""" + pass + +def run(*args: Unpack[Ts]) -> None: + """Run a function until completion.""" + pass +``` + +-------------------------------- + +TITLE: Run Synchronous Function in a Worker Process +DESCRIPTION: Executes a synchronous function in a separate worker process. This offers strong isolation and is suitable for CPU-bound tasks or running code that requires a different Python environment. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_23 + +LANGUAGE: Python +CODE: +``` +result = await anyio.to_process.run_sync(sync_function, arg1, process_pool=my_pool) +``` + +-------------------------------- + +TITLE: Add Lock Parameter to create_condition() +DESCRIPTION: Adds the `lock` parameter to the `anyio.create_condition()` function. This allows users to specify a custom lock object to be used with the condition variable, offering more flexibility in synchronization. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_229 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def use_custom_lock_condition(): + custom_lock = anyio.Lock() + condition = anyio.create_condition(lock=custom_lock) + # Use the condition variable with the custom lock +``` + +-------------------------------- + +TITLE: Force WindowsSelectorEventLoopPolicy on Windows +DESCRIPTION: Forces the use of `WindowsSelectorEventLoopPolicy` in `asyncio.run` when running on Windows with the asyncio backend. This ensures network functionality remains stable and avoids potential issues with default loop policies. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_228 + +LANGUAGE: Python +CODE: +``` +import anyio +import asyncio +import sys + +async def run_asyncio_app(): + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + # Run application logic + pass + +# AnyIO's asyncio backend will now automatically apply this policy on Windows +``` + +-------------------------------- + +TITLE: Fix Timeout Errors in AnyIO +DESCRIPTION: Version 1.4.0 addresses issues with timeout errors in AnyIO, specifically fixing fail.after(0) on asyncio and curio, and ensuring socket operations correctly cancel tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_218 + +LANGUAGE: Python +CODE: +``` +Fixed fail.after(0) not raising a timeout error on asyncio and curio +Fixed socket operations not allowing timeouts to cancel the task +``` + +-------------------------------- + +TITLE: Fix Open Signal Receiver Hanging +DESCRIPTION: Resolves an issue where `open_signal_receiver()` on asyncio and curio could hang if the cancel scope was cancelled before the function could run. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_184 + +LANGUAGE: Python +CODE: +``` +anyio.open_signal_receiver() +``` + +-------------------------------- + +TITLE: Configure AnyIO TLSListener for Non-Standard Shutdown +DESCRIPTION: Shows how to configure AnyIO's TLSListener to disable the standard TLS closing handshake. This is necessary for protocols that do not strictly follow the TLS standard for connection termination. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/streams.rst#_snippet_10 + +LANGUAGE: Python +CODE: +``` +# Example usage with TLSListener: +# listener = TLSListener(..., standard_compatible=False) + +# Example usage with TLSStream.wrap: +# wrapped_stream = await TLSStream.wrap(..., standard_compatible=False) +``` + +-------------------------------- + +TITLE: Forcefully Close an Async Resource +DESCRIPTION: Immediately closes an asynchronous resource, even if it is currently in use. This is typically used for cleanup operations when normal closing is not possible. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_7 + +LANGUAGE: Python +CODE: +``` +await anyio.aclose_forcefully(resource) +``` + +-------------------------------- + +TITLE: Asyncio Thread Limiter RunVar +DESCRIPTION: Uses `RunVar` for the default thread limiter on asyncio, scoping it to the current event loop to prevent conflicts between multiple running event loops. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_176 + +LANGUAGE: Python +CODE: +``` +asyncio.RunVar +``` + +-------------------------------- + +TITLE: Memory Object Stream Statistics +DESCRIPTION: Provides statistics about the performance and usage of memory object streams, such as the number of items sent or received. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_55 + +LANGUAGE: Python +CODE: +``` +stats = anyio.streams.memory.MemoryObjectStreamStatistics(items_sent=10, items_received=10) +``` + +-------------------------------- + +TITLE: Fix anyio.Path.iterdir() blocking call in Python 3.13 +DESCRIPTION: Resolves a problem where `anyio.Path.iterdir()` would make a blocking call when used with Python 3.13. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_19 + +LANGUAGE: Python +CODE: +``` +Fixed ``anyio.Path.iterdir()`` making a blocking call in Python 3.13 + (`#873 `_; PR by @cbornet and + @agronholm) +``` + +-------------------------------- + +TITLE: Handle IPv6 name resolution results +DESCRIPTION: Modifies `anyio.getaddrinfo()` to ignore invalid IPv6 name resolution results when IPv6 support is disabled in Python. This prevents potential errors and ensures more robust network address lookups. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_17 + +LANGUAGE: Python +CODE: +``` +import anyio + +# async def get_addresses(): +# # Example: get address info, handling potential IPv6 issues +# addresses = await anyio.getaddrinfo('example.com', 80) +# print(addresses) + +``` + +-------------------------------- + +TITLE: Fix AnyIO processes being lost in process pool +DESCRIPTION: Resolves an issue where processes spawned by anyio.to_process() became unusable after idling for over 5 minutes due to pruning in to_process.run_sync(). This led to increased memory consumption. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_100 + +LANGUAGE: Python +CODE: +``` +from anyio import to_process + +# The fix ensures that pruned processes are handled correctly +# and do not become 'lost' to the process pool. +``` + +-------------------------------- + +TITLE: Fix Timeouts Not Propagating on Asyncio/Curio +DESCRIPTION: Resolves an issue where timeouts did not propagate correctly from nested cancel scopes on asyncio and curio backends. This ensures that nested timeouts are handled as expected. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_255 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def nested_timeout_operation(): + async with anyio.open_cancel_scope(deadline=10): + async with anyio.open_cancel_scope(deadline=5): + # This inner scope's timeout should now propagate correctly +``` + +-------------------------------- + +TITLE: Change Default uvloop Option (Asyncio) +DESCRIPTION: Modifies the default value of the ``use_uvloop`` asyncio backend option to ``False``. This change prevents unsafe event loop policy modifications across different threads. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_145 + +LANGUAGE: Python +CODE: +``` +Changed the default value of the ``use_uvloop`` asyncio backend option to ``False`` to + prevent unsafe event loop policy changes in different threads +``` + +-------------------------------- + +TITLE: Fix anyio.Path type annotations +DESCRIPTION: Corrects various type annotations for the anyio.Path class to align with Typeshed definitions, ensuring better compatibility and accuracy with static type checkers. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_70 + +LANGUAGE: Python +CODE: +``` +class Path: + def __lt__(self, other: 'Path') -> bool: + """Compare paths less than.""" + pass + + def hardlink_to(self, target: 'Path') -> 'Path': + """Create a hardlink.""" + pass + + def symlink_to(self, target: str | 'Path') -> 'Path': + """Create a symbolic link.""" + pass +``` + +-------------------------------- + +TITLE: Add Python 3.10 Compatibility +DESCRIPTION: Ensures AnyIO is compatible with Python version 3.10. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_143 + +LANGUAGE: Python +CODE: +``` +Added Python 3.10 compatibility +``` + +-------------------------------- + +TITLE: AnyIO Stream API Overhaul +DESCRIPTION: This snippet summarizes the backwards-incompatible overhaul of the AnyIO stream class structure in version 2.0.0. It introduces separate abstract base classes for receive and send streams, byte streams, and object streams, improving support for stream wrappers and a new 'typed extra attribute' system. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_198 + +LANGUAGE: Python +CODE: +``` +The stream class structure was completely overhauled. There are now separate abstract base classes for receive and send streams, byte streams and reliable and unreliable object streams. Stream wrappers are much better supported by this new ABC structure and a new "typed extra attribute" system that lets you query the wrapper chain for the attributes you want via ".extra(...)". +``` + +-------------------------------- + +TITLE: Synchronous Closing of Memory Object Streams +DESCRIPTION: Enables the closing of memory object streams synchronously, including support for their use as synchronous context managers. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_144 + +LANGUAGE: Python +CODE: +``` +Added the ability to close memory object streams synchronously (including support for + use as a synchronous context manager) +``` + +-------------------------------- + +TITLE: Improve Type Annotations for MemoryObjectStream (AnyIO) +DESCRIPTION: Enhances type annotations for `create_memory_object_stream`. The `item_type` argument is now deprecated in favor of generic type hinting (e.g., `create_memory_object_stream[T_Item]()`), improving type checking with uninstantiable item types. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_88 + +LANGUAGE: python +CODE: +``` +Improved type annotations: + + - The ``item_type`` argument of ``create_memory_object_stream`` was deprecated. + To indicate the item type handled by the stream, use + ``create_memory_object_stream[T_Item]()`` instead. Type checking should no longer + fail when annotating memory object streams with uninstantiable item types (PR by + Ganden Schaffner) +``` + +-------------------------------- + +TITLE: Workaround PyPy bug with anyio.getaddrinfo() and IPv6 +DESCRIPTION: Addresses a PyPy bug encountered when using anyio.getaddrinfo() with IPv6 link-local addresses that include interface names. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_113 + +LANGUAGE: Python +CODE: +``` +from anyio import getaddrinfo + +# The workaround ensures that getaddrinfo functions correctly with IPv6 +# link-local addresses containing interface names on PyPy. +``` + +-------------------------------- + +TITLE: AnyIO Socket Concurrency Guards +DESCRIPTION: This update introduces guards to prevent concurrent read/write operations on sockets by multiple tasks, enhancing stability and preventing race conditions. It also adds the `notify_socket_close()` function for better socket management. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_265 + +LANGUAGE: Python +CODE: +``` +def notify_socket_close(): + # Implementation details for notifying socket closure + pass +``` + +-------------------------------- + +TITLE: Override Typed Attributes in a Subclass +DESCRIPTION: Demonstrates how to override existing typed attributes in a subclass by inheriting from a `TypedAttributeProvider` and redefining the `extra_attributes` property, ensuring to include the parent's attributes. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/typedattrs.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +class AnotherAttributeProvider(MyAttributeProvider): + @property + def extra_attributes() -> Mapping[Any, Callable[[], Any]]: + return { + **super().extra_attributes, + MyTypedAttribute.string_valued_attribute: lambda: 'overridden attribute value' + } +``` + +-------------------------------- + +TITLE: Provide Python 3.8 regression fix +DESCRIPTION: An interim release to address a regression affecting Python 3.8, also incorporating fixes present in v4.6.1. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_42 + +LANGUAGE: Python +CODE: +``` +As Python 3.8 support was dropped in v4.6.0, this interim release was created to bring a + regression fix to Python 3.8, and adds a few other fixes also present in v4.6.1. +``` + +-------------------------------- + +TITLE: Task Status Interface +DESCRIPTION: Represents the status of a task within a task group, allowing for synchronization and signaling between tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_18 + +LANGUAGE: Python +CODE: +``` +async def worker(task_status: anyio.abc.TaskStatus): + task_status.started() + await do_work() + task_status.finished() +``` + +-------------------------------- + +TITLE: Fix MultiListener Extra Method +DESCRIPTION: Fixes a broken `MultiListener.extra()` method, ensuring its functionality is restored. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_195 + +LANGUAGE: Python +CODE: +``` +anyio.MultiListener.extra() +``` + +-------------------------------- + +TITLE: Fix SyntaxWarning for return in finally on Python 3.14 +DESCRIPTION: Eliminates a `SyntaxWarning` related to using `return` within a `finally` block, specifically on Python 3.14. This improves code cleanliness and adherence to Python's syntax guidelines. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_9 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def safe_operation(): + try: + # operation that might raise an exception + pass + finally: + # Code here should not use 'return' if it might be reached + # during exception handling in a way that causes the warning. + pass + +``` + +-------------------------------- + +TITLE: Fix RuntimeError in Async Generator Fixtures (AnyIO) +DESCRIPTION: Addresses a RuntimeError that occurred when running higher-scoped async generator fixtures in certain scenarios within the AnyIO framework. This fix ensures smoother execution of asynchronous fixtures. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_74 + +LANGUAGE: python +CODE: +``` +Fixed ``RuntimeError: Runner is closed`` when running higher-scoped async generator + fixtures in some cases (`#619 `_) +``` + +-------------------------------- + +TITLE: Fix Socket Stream Send Exception +DESCRIPTION: Ensures `send()` on socket streams raises appropriate exceptions on asyncio (e.g., `BrokenPipeError`) and Trio/Curio when the peer disconnects, improving error reporting. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_191 + +LANGUAGE: Python +CODE: +``` +socket streams send() +``` + +-------------------------------- + +TITLE: Fix Current Effective Deadline KeyError +DESCRIPTION: Fixes `current_effective_deadline()` raising `KeyError` on asyncio when no cancel scope is active, ensuring correct behavior in all scenarios. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_178 + +LANGUAGE: Python +CODE: +``` +anyio.current_effective_deadline() +``` + +-------------------------------- + +TITLE: Fix Race Condition with Multiple Event Loops +DESCRIPTION: Resolves a race condition that could lead to crashes when multiple event loops of the same backend were running concurrently in separate threads and attempting to use AnyIO. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_64 + +LANGUAGE: Python +CODE: +``` +import anyio +import threading + +async def worker_task(): + # Simulate using AnyIO resources within a thread-specific event loop + try: + await anyio.sleep(0.1) + print(f"Worker task running in thread: {threading.current_thread().name}") + except Exception as e: + print(f"Error in worker task: {e}") + +def run_in_thread(): + # Each thread needs its own event loop managed by AnyIO + anyio.run(worker_task, backend='asyncio') + +# Example of how to trigger the scenario (requires careful setup): +# threads = [] +# for i in range(3): +# thread = threading.Thread(target=run_in_thread, name=f"Worker-{i}") +# threads.append(thread) +# thread.start() +# +# for thread in threads: +# thread.join() +``` + +-------------------------------- + +TITLE: Update SocketStream Methods in AnyIO +DESCRIPTION: This snippet details the changes to methods and attributes of the SocketStream class in AnyIO. It highlights the renaming of parameters and the replacement of certain methods with new ones or different approaches. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_208 + +LANGUAGE: Python +CODE: +``` +address → .extra(SocketAttribute.local_address) +alpn_protocol → .extra(TLSAttribute.alpn_protocol) +close() → aclose() +get_channel_binding → .extra(TLSAttribute.channel_binding_tls_unique) +cipher → .extra(TLSAttribute.cipher) +getpeercert → .extra(SocketAttribute.peer_certificate) or .extra(SocketAttribute.peer_certificate_binary) +getsockopt() → .extra(SocketAttribute.raw_socket).getsockopt(...) +peer_address → .extra(SocketAttribute.remote_address) +receive_chunks() → (removed; use async for on the stream instead) +receive_delimited_chunks() → (removed) +receive_exactly() → BufferedReceiveStream.receive_exactly() +receive_some() → receive() +receive_until() → BufferedReceiveStream.receive_until() +send_all() → send() +setsockopt() → .extra(SocketAttribute.raw_socket).setsockopt(...) +shared_ciphers → .extra(TLSAttribute.shared_ciphers) +server_side → .extra(TLSAttribute.server_side) +start_tls() → stream = TLSStream.wrap(...) +tls_version → .extra(TLSAttribute.tls_version) +``` + +-------------------------------- + +TITLE: Fix Asyncio Warning on Unretrieved Future (Socket Write) +DESCRIPTION: Addresses an asyncio warning related to a ``Future`` with an unretrieved exception. This occurred when a socket write operation was followed by an abrupt peer connection closure. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_135 + +LANGUAGE: Python +CODE: +``` +Fixed asyncio warning about a ``Future`` with an exception that was never retrieved + which happened when a socket was already written to but the peer abruptly closed the + connection +``` + +-------------------------------- + +TITLE: Add Capacity Limiters +DESCRIPTION: Introduces capacity limiters as a feature within AnyIO. Capacity limiters are used to control the rate or number of concurrent operations, helping to manage resource usage and prevent overload. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_231 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def use_capacity_limiter(): + limiter = anyio.CapacityLimiter(10) # Limit to 10 concurrent operations + async with limiter: + # Perform an operation that is subject to the capacity limit +``` + +-------------------------------- + +TITLE: Fix current_effective_deadline() with Shielded Scopes +DESCRIPTION: Ensures that `current_effective_deadline()` correctly accounts for shielded cancellation scopes on asyncio and curio backends. This provides accurate deadline information even when cancellation is shielded. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_258 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def check_deadline_with_shield(): + async with anyio.open_cancel_scope(shield=True): + deadline = anyio.current_effective_deadline() + # deadline should now be correctly calculated +``` + +-------------------------------- + +TITLE: Fix Worker Threads Marked as Event Loop Threads (Sniffio) +DESCRIPTION: Corrects a bug where worker threads were incorrectly marked as event loop threads by the sniffio library. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_125 + +LANGUAGE: Python +CODE: +``` +Fixed worker threads being marked as being event loop threads in sniffio +``` + +-------------------------------- + +TITLE: Drop async_generator Dependency +DESCRIPTION: Removes the unnecessary dependency on the ``async_generator`` library from AnyIO. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_139 + +LANGUAGE: Python +CODE: +``` +Dropped unnecessary dependency on the ``async_generator`` library +``` + +-------------------------------- + +TITLE: Fix ExceptionGroup Formatted Output +DESCRIPTION: Addresses an issue with the formatted output of `ExceptionGroup` containing excessive newlines, improving the readability of grouped exceptions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_186 + +LANGUAGE: Python +CODE: +``` +anyio.ExceptionGroup +``` + +-------------------------------- + +TITLE: Fix TaskGroup/CancelScope cyclic references +DESCRIPTION: Resolves an issue where `TaskGroup` and `CancelScope` could produce cyclic references in tracebacks when exceptions were raised. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_36 + +LANGUAGE: Python +CODE: +``` +Fixed TaskGroup and CancelScope producing cyclic references in tracebacks + when raising exceptions (`#806 `_) + (PR by @graingert) +``` + +-------------------------------- + +TITLE: Fix asyncio test runner exception handling +DESCRIPTION: Corrects the exception handler in the asyncio test runner to properly manage contexts that do not contain the 'exception' key. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_115 + +LANGUAGE: Python +CODE: +``` +# The test runner's exception handling logic has been updated to be more robust. +``` + +-------------------------------- + +TITLE: Fix Task Parent ID Setting (Asyncio) +DESCRIPTION: Ensures the task parent ID is correctly set in asyncio environments. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_126 + +LANGUAGE: Python +CODE: +``` +Fixed task parent ID not getting set to the correct value on asyncio +``` + +-------------------------------- + +TITLE: Call Async Code from Worker Thread (AnyIO) +DESCRIPTION: Shows how to call an asynchronous function (coroutine) from a synchronous function running in a worker thread. This is achieved using `anyio.from_thread.run`, which bridges the worker thread back to the AnyIO event loop to execute the coroutine. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_1 + +LANGUAGE: Python +CODE: +``` +from anyio import from_thread, sleep, to_thread, run + + +def blocking_function(): + from_thread.run(sleep, 5) + + +async def main(): + await to_thread.run_sync(blocking_function) + +run(main) +``` + +-------------------------------- + +TITLE: Re-add item_type to create_memory_object_stream +DESCRIPTION: The 'item_type' argument has been re-added to create_memory_object_stream(). While it raises a deprecation warning and has no effect on static types, it restores previous functionality. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_99 + +LANGUAGE: Python +CODE: +``` +from anyio import create_memory_object_stream + +# Example usage (will raise DeprecationWarning) +# sender, receiver = create_memory_object_stream(item_type=int) +``` + +-------------------------------- + +TITLE: Fix Process Pruning Leading to Memory Issues in anyio.to_process +DESCRIPTION: Resolves a memory consumption issue in `anyio.to_process` where pruned idle processes were incorrectly marked as unusable, leading to increased memory usage. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_95 + +LANGUAGE: python +CODE: +``` +Fixed processes spawned by ``anyio.to_process()`` being "lost" as unusable to the + process pool when processes that have idled over 5 minutes are pruned at part of the + ``to_process.run_sync()`` call, leading to increased memory consumption + (PR by Anael Gorfinkel) +``` + +-------------------------------- + +TITLE: Fix TLSStream BrokenResourceError on Peer Close +DESCRIPTION: Corrects an issue in ``TLSStream`` where ``BrokenResourceError`` was raised instead of ``EndOfStream`` when the peer abruptly closed the connection during data reception with ``standard_compatible=False``. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_128 + +LANGUAGE: Python +CODE: +``` +Fixed ``BrokenResourceError`` instead of ``EndOfStream`` being raised in ``TLSStream`` + when the peer abruptly closes the connection while ``TLSStream`` is receiving data + with ``standard_compatible=False`` set +``` + +-------------------------------- + +TITLE: Drop Python 3.8 support +DESCRIPTION: Removes support for Python 3.8, as certain issues, like cancel message support, cannot be resolved without it. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_37 + +LANGUAGE: Python +CODE: +``` +Dropped support for Python 3.8 + (as `#698 `_ cannot be resolved + without cancel message support) +``` + +-------------------------------- + +TITLE: Run Synchronous Function in Worker Process (Python) +DESCRIPTION: Executes a CPU-intensive synchronous function in a separate worker process to bypass Python's Global Interpreter Lock (GIL). This is crucial for true parallelism on multi-core systems. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/subprocesses.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +import time + +from anyio import run, to_process + + +def cpu_intensive_function(arg1, arg2): + time.sleep(1) + return arg1 + arg2 + +async def main(): + result = await to_process.run_sync(cpu_intensive_function, 'Hello, ', 'world!') + print(result) + +# This check is important when the application uses to_process.run_sync() +if __name__ == '__main__': + run(main) +``` + +-------------------------------- + +TITLE: Check for Cancellation in Worker Threads +DESCRIPTION: Illustrates how to voluntarily check for cancellation within a synchronous function running in a worker thread. If the host task's scope has been cancelled, `from_thread.check_cancelled()` raises a cancellation exception. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_9 + +LANGUAGE: Python +CODE: +``` +import time + +from anyio import to_thread, from_thread, move_on_after + +def sync_function(): + while True: + from_thread.check_cancelled() + print("Not cancelled yet") + time.sleep(1) + +async def foo(): + with move_on_after(3): + await to_thread.run_sync(sync_function) +``` + +-------------------------------- + +TITLE: Asyncio Test Runner Exception Capture +DESCRIPTION: Modifies the asyncio test runner to capture unhandled exceptions from asynchronous callbacks and unbound native tasks, raising them after test completion for better error reporting. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_174 + +LANGUAGE: Python +CODE: +``` +asyncio test runner +``` + +-------------------------------- + +TITLE: Fix Incorrect Call Order in Socket Close +DESCRIPTION: Corrects the incorrect call order in socket close notifications, particularly affecting Windows. This ensures that sockets are closed in the proper sequence, preventing potential resource leaks or errors. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_256 + +LANGUAGE: Python +CODE: +``` +# This is an internal implementation detail fix related to socket handling. +``` + +-------------------------------- + +TITLE: AnyIO Task Introspection +DESCRIPTION: Introduces the ability to introspect running tasks within the AnyIO framework using `anyio.get_running_tasks()`. This allows developers to monitor and manage concurrent operations more effectively. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_266 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def monitor_tasks(): + running_tasks = anyio.get_running_tasks() + print(f"Currently running tasks: {running_tasks}") +``` + +-------------------------------- + +TITLE: Update Cancellation Semantics (AnyIO 4.0.0) +DESCRIPTION: Version 4.0.0 of AnyIO introduced significant changes to cancellation semantics. Exceptions raised from task groups are now nested within an ExceptionGroup, and task group exit behavior has been refined to ensure yielding points and proper handling of deferred cancellations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_77 + +LANGUAGE: python +CODE: +``` +**BACKWARDS INCOMPATIBLE** Changes to cancellation semantics: + + - Any exceptions raising out of a task groups are now nested inside an + ``ExceptionGroup`` (or ``BaseExceptionGroup`` if one or more ``BaseException`` were + included) + - Fixed task group not raising a cancellation exception on asyncio at exit if no child + tasks were spawned and an outer cancellation scope had been cancelled before + - Ensured that exiting a ``TaskGroup`` always hits a yield point, regardless of + whether there are running child tasks to be waited on + - On asyncio, cancel scopes will defer cancelling tasks that are scheduled to resume + with a finished future + - On asyncio and Python 3.9/3.10, cancel scopes now only suppress cancellation + exceptions if the cancel message matches the scope + - Task groups on all backends now raise a single cancellation exception when an outer + cancel scope is cancelled, and no exceptions other than cancellation exceptions are + raised in the group +``` + +-------------------------------- + +TITLE: Replace AnyIO ExceptionGroup with PEP 654 (AnyIO 4.0.0) +DESCRIPTION: In version 4.0.0, AnyIO replaced its custom ExceptionGroup class with the standard PEP 654 BaseExceptionGroup and ExceptionGroup. This change standardizes exception handling for grouped exceptions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_76 + +LANGUAGE: python +CODE: +``` +**BACKWARDS INCOMPATIBLE** Replaced AnyIO's own ``ExceptionGroup`` class with the PEP + 654 ``BaseExceptionGroup`` and ``ExceptionGroup`` +``` + +-------------------------------- + +TITLE: Rename BrokenWorkerIntepreter to BrokenWorkerInterpreter +DESCRIPTION: Renames the `BrokenWorkerIntepreter` exception to `BrokenWorkerInterpreter` for better clarity and correctness. The old name is retained as a deprecated alias for backward compatibility. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_11 + +LANGUAGE: Python +CODE: +``` +import anyio + +# try: +# # code that might raise the exception +# pass +# except anyio.BrokenWorkerInterpreter as e: +# print(f"Caught expected exception: {e}") +# except anyio.BrokenWorkerIntepreter as e: # Deprecated alias +# print(f"Caught deprecated exception: {e}") + +``` + +-------------------------------- + +TITLE: Fix current_effective_deadline() returning -inf on asyncio +DESCRIPTION: Corrects current_effective_deadline() on asyncio to return -inf when the active cancel scope has been cancelled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_109 + +LANGUAGE: Python +CODE: +``` +from anyio import current_effective_deadline, CancelScope + +# async with CancelScope() as scope: +# scope.cancel() +# deadline = current_effective_deadline() +# # deadline should now be -inf +``` + +-------------------------------- + +TITLE: Add Parent ID to TaskInfo +DESCRIPTION: Adds the `parent_id` attribute to the `anyio.TaskInfo` object. This allows for tracking the hierarchical relationship between tasks, making it easier to understand task dependencies and execution flow. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_233 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def get_task_hierarchy(): + task_infos = await anyio.get_all_tasks() + for task_info in task_infos: + print(f"Task ID: {task_info.id}, Parent ID: {task_info.parent_id}") +``` + +-------------------------------- + +TITLE: Add CancelScope.cancelled_caught Property (AnyIO) +DESCRIPTION: Introduces the `CancelScope.cancelled_caught` property, which allows users to determine if a specific cancel scope has suppressed a cancellation exception. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_89 + +LANGUAGE: python +CODE: +``` +Added the ``CancelScope.cancelled_caught`` property which tells users if the cancel + scope suppressed a cancellation exception +``` + +-------------------------------- + +TITLE: Bind Unconnected UDP Sockets (Asyncio + Windows) +DESCRIPTION: Modifies unconnected UDP sockets to always be bound to a local port on the 'any' interface, preventing errors specifically on asyncio with Windows. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_130 + +LANGUAGE: Python +CODE: +``` +Changed unconnected UDP sockets to be always bound to a local port (on "any" + interface) to avoid errors on asyncio + Windows +``` + +-------------------------------- + +TITLE: Fix Lock Acquisition Twice on Asyncio +DESCRIPTION: Addresses an issue where acquiring a lock twice within the same task on asyncio would cause a hang instead of raising a RuntimeError. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_43 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def example(): + lock = anyio.Lock() + async with lock: + # Acquire lock again - this should now raise RuntimeError instead of hanging + async with lock: + pass +``` + +-------------------------------- + +TITLE: Parameter Renaming in AnyIO +DESCRIPTION: This change in AnyIO involves renaming the 'max_size' parameter to 'max_bytes' across all occurrences to ensure consistent naming conventions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_210 + +LANGUAGE: Python +CODE: +``` +Renamed the max_size parameter to max_bytes wherever it occurred +``` + +-------------------------------- + +TITLE: Fix AssertionError in connect_tcp() +DESCRIPTION: Resolves an `AssertionError` that could occur in `connect_tcp()` when multiple connection attempts succeeded simultaneously. This ensures the function handles concurrent successful connections robustly. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_243 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def connect_robustly(host, port): + async with await anyio.connect_tcp(host, port) as stream: + # Connection established successfully even with concurrent attempts +``` + +-------------------------------- + +TITLE: Fix get_running_tasks() Returning Other Loop Tasks +DESCRIPTION: Resolves an issue where `get_running_tasks()` incorrectly returned tasks from other event loops in the asyncio backend. This ensures that the function only returns tasks belonging to the current event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_264 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def get_current_loop_tasks(): + tasks = await anyio.get_all_tasks() + # tasks should now only contain tasks from the current loop +``` + +-------------------------------- + +TITLE: Fix TaskInfo.has_pending_cancellation() on Asyncio +DESCRIPTION: Ensures that TaskInfo.has_pending_cancellation() correctly respects shielded scopes on asyncio, preventing incorrect status reporting. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_54 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def shielded_task(): + async with anyio.CancelScope(shield=True): + await anyio.sleep(100) + +async def check_cancellation_status(): + task = await anyio.create_task(shielded_task()) + await anyio.sleep(0.1) # Give the task a moment to start + + task_info = task.get_info() + # This should now correctly return False because the scope is shielded + print(f"Has pending cancellation (shielded): {task_info.has_pending_cancellation()}") + + # Cancel the task explicitly + task.cancel() + await anyio.sleep(0.1) + task_info = task.get_info() + print(f"Has pending cancellation (after cancel): {task_info.has_pending_cancellation()}") + +# anyio.run(check_cancellation_status) +``` + +-------------------------------- + +TITLE: Exclusive Lock and Condition Release +DESCRIPTION: Enforces that `Lock` and `Condition` can only be released by the task that acquired them. This ensures consistent behavior across different backends, improving robustness. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_169 + +LANGUAGE: Python +CODE: +``` +anyio.Lock.release() +``` + +LANGUAGE: Python +CODE: +``` +anyio.Condition.release() +``` + +-------------------------------- + +TITLE: Update CancelScope to Consider Cancellation Count +DESCRIPTION: The `CancelScope` in AnyIO now also considers the cancellation count in addition to other factors when managing cancellation, providing more nuanced control over cancellation propagation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_97 + +LANGUAGE: python +CODE: +``` +Changed ``CancelScope`` to also consider the cancellation count (in addition to the +``` + +-------------------------------- + +TITLE: Fix Cancellation Propagation from Nested Task Groups +DESCRIPTION: Ensures that cancellation correctly propagates from nested task groups. This fix guarantees that if a task in an inner task group is cancelled, the cancellation signal is passed up to the outer task groups as expected. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_263 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def nested_task_groups(): + async with anyio.create_task_group() as tg1: + async with anyio.create_task_group() as tg2: + await tg2.spawn(some_task) + await tg1.cancel_scope.cancel() # Cancellation should propagate to tg2 +``` + +-------------------------------- + +TITLE: Asyncio Thread Pooling +DESCRIPTION: Enables thread pooling on asyncio using `run_sync_in_worker_thread()`, improving performance for synchronous operations executed within the asyncio event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_177 + +LANGUAGE: Python +CODE: +``` +anyio.run_sync_in_worker_thread() +``` + +-------------------------------- + +TITLE: Fix run_sync_in_worker_thread UnboundLocalError +DESCRIPTION: Resolves `UnboundLocalError` in `run_sync_in_worker_thread()` on asyncio when cancelled, ensuring proper exception handling during cancellation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_190 + +LANGUAGE: Python +CODE: +``` +anyio.run_sync_in_worker_thread() +``` + +-------------------------------- + +TITLE: Fix Trio Test Runner BaseException Errors +DESCRIPTION: Corrects the Trio test runner to prevent unwarranted test errors when encountering `BaseException`, ensuring more accurate test results. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_185 + +LANGUAGE: Python +CODE: +``` +Trio test runner +``` + +-------------------------------- + +TITLE: Fix KeyboardInterrupt Hanging Asyncio Pytest Runner +DESCRIPTION: Resolves an issue where pressing Ctrl+C (KeyboardInterrupt) would cause the asyncio pytest runner to hang. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_58 + +LANGUAGE: Python +CODE: +``` +import anyio +import pytest + +async def long_running_task(): + while True: + await anyio.sleep(1) + +# To test this, run pytest with the asyncio plugin: +# pytest --asyncio-mode=auto your_test_file.py + +# Inside your test file: +# async def test_long_task_interrupt(): +# async with anyio.create_task_group() as tg: +# await tg.spawn(long_running_task) +# # During test execution, press Ctrl+C. +# # The test should terminate cleanly instead of hanging. +# await anyio.sleep(10) # Keep the task group alive for interruption + +``` + +-------------------------------- + +TITLE: Fix Async Generator Await AttributeError +DESCRIPTION: Addresses an `AttributeError: 'async_generator_asend' object has no attribute 'cr_await'` on asyncio, improving the stability of asynchronous generators. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_194 + +LANGUAGE: Python +CODE: +``` +async_generator_asend cr_await +``` + +-------------------------------- + +TITLE: Fix fail_after() TimeoutError on Cancellation (AnyIO) +DESCRIPTION: Corrects an issue where `fail_after()` incorrectly raised a `TimeoutError` when the associated cancel scope was cancelled before its deadline was reached. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_90 + +LANGUAGE: python +CODE: +``` +Fixed ``fail_after()`` raising an unwarranted ``TimeoutError`` when the cancel scope + was cancelled before reaching its deadline +``` + +-------------------------------- + +TITLE: Fix asyncio task groups yielding control at exit +DESCRIPTION: Ensures that asyncio task groups yield control to the event loop upon exit, even if there are no child tasks to wait for. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_40 + +LANGUAGE: Python +CODE: +``` +Fixed asyncio task groups not yielding control to the event loop at exit if there were + no child tasks to wait on +``` + +-------------------------------- + +TITLE: Fix TypeError in get_current_task() with custom Task factory +DESCRIPTION: Resolves a TypeError that occurred in get_current_task() on asyncio when a custom Task factory was used. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_116 + +LANGUAGE: Python +CODE: +``` +from anyio.from_thread import get_current_task + +# The function now correctly handles custom Task implementations. +``` + +-------------------------------- + +TITLE: Unreliable Object Stream Interfaces +DESCRIPTION: Defines interfaces for unreliable object streams, where data might be lost or arrive out of order. These are typically used for datagram-based protocols. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_43 + +LANGUAGE: Python +CODE: +``` +async def process_unreliable_stream(stream: anyio.abc.UnreliableObjectStream): + await stream.send(data) + received_data = await stream.receive() +``` + +-------------------------------- + +TITLE: Use nonlocal for Local Variables in Exception Handlers +DESCRIPTION: Illustrates how to modify local variables within exception handler functions in AnyIO. The `nonlocal` keyword is used to declare that a variable refers to a variable in the nearest enclosing scope that is not global. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/tasks.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +import anyio +from anyio import create_task_group, catch + +def handle_keyerror(excgroup: ExceptionGroup) -> None: + # handle each KeyError + pass + +async def some_task(): + pass + +async def another_task(): + pass + +async def yourfunc(): + somevariable: str | None = None + + def handle_valueerror(exc): + nonlocal somevariable + somevariable = 'whatever' + + with catch({ + ValueError: handle_valueerror, + KeyError: handle_keyerror + }): + async with create_task_group() as tg: + tg.start_soon(some_task) + tg.start_soon(another_task) + + print(f"{somevariable=}") +``` + +-------------------------------- + +TITLE: Run Synchronous Function in Worker Thread (AnyIO) +DESCRIPTION: Demonstrates how to execute a synchronous function, like `time.sleep`, in a separate worker thread using `anyio.to_thread.run_sync`. This prevents blocking the main event loop. The `cancellable=True` option can be used to allow cancellation of the task while waiting for the thread. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_0 + +LANGUAGE: Python +CODE: +``` +import time + +from anyio import to_thread, run + + +async def main(): + await to_thread.run_sync(time.sleep, 5) + +run(main) +``` + +-------------------------------- + +TITLE: Fix MemoryObjectSendStream Send BrokenResourceError +DESCRIPTION: Fixes `MemoryObjectSendStream.send()` raising `BrokenResourceError` when the last receiver is closed immediately after receiving an item, ensuring correct resource management. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_192 + +LANGUAGE: Python +CODE: +``` +anyio.MemoryObjectSendStream.send() +``` + +-------------------------------- + +TITLE: Adjust Default Maximum Worker Threads +DESCRIPTION: Shows how to modify the default maximum number of worker threads used by AnyIO's thread limiter. This is done by accessing and setting the `total_tokens` attribute of the current default thread limiter. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/threads.rst#_snippet_8 + +LANGUAGE: Python +CODE: +``` +from anyio import to_thread + +async def foo(): + # Set the maximum number of worker threads to 60 + to_thread.current_default_thread_limiter().total_tokens = 60 +``` + +-------------------------------- + +TITLE: Fix Asyncio Socket Stream Half-Close +DESCRIPTION: Fixes a bug in the asyncio socket stream where receiving a half-close from the peer would shut down the entire connection, ensuring proper handling of connection states. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_181 + +LANGUAGE: Python +CODE: +``` +asyncio socket stream +``` + +-------------------------------- + +TITLE: Fix compatibility of anyio.to_interpreter with Python 3.14.0b2 +DESCRIPTION: Resolves compatibility issues with the `anyio.to_interpreter` function when used with Python 3.14.0b2, ensuring correct behavior in inter-interpreter communication. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_8 + +LANGUAGE: Python +CODE: +``` +import anyio + +# Example usage: +# async def run_in_other_interpreter(): +# await anyio.to_interpreter(some_function, interpreter=...) + +``` + +-------------------------------- + +TITLE: Fix RuntimeError in to_thread.run_sync() on asyncio +DESCRIPTION: Addresses a RuntimeError occurring in `to_thread.run_sync()` when using asyncio without a root task. The fix binds worker thread lifetimes to the host task of the cancel scope hierarchy or the current task if no cancel scope is active. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_156 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def main(): + # Example usage that might have caused the error + await anyio.to_thread.run_sync(lambda: None) + +# In a scenario where a root task might not be obvious: +# anyio.run(main) +``` + +-------------------------------- + +TITLE: Add send_eof() to AnyIO Streams +DESCRIPTION: The send_eof() method has been introduced to all bidirectional streams in AnyIO, enabling the explicit signaling of the end of a transmission. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_212 + +LANGUAGE: Python +CODE: +``` +The send_eof() method was added to all (bidirectional) streams +``` + +-------------------------------- + +TITLE: Add a Typed Attribute +DESCRIPTION: Adds a typed attribute to an object, allowing for type-safe attribute access. This is useful for extending objects with custom metadata. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_9 + +LANGUAGE: Python +CODE: +``` +anyio.typed_attribute(name, type) +``` + +-------------------------------- + +TITLE: Fix to_thread.run_sync() Prematurely Inactive Thread +DESCRIPTION: Resolves a bug where ``to_thread.run_sync()`` would prematurely mark a worker thread as inactive if a task awaiting its result was cancelled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_147 + +LANGUAGE: Python +CODE: +``` +Fixed ``to_thread.run_sync()`` prematurely marking a worker thread inactive when a + task await on the result is cancelled +``` + +-------------------------------- + +TITLE: Fix sending large buffers via UNIX stream sockets on asyncio +DESCRIPTION: Corrects an issue related to sending large buffers over UNIX stream sockets when using the asyncio backend. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_101 + +LANGUAGE: Python +CODE: +``` +from anyio.streams.uds import UnixStreamSocket + +# The fix likely involves adjustments in how data is buffered or sent +# over the socket connection. +``` + +-------------------------------- + +TITLE: Fix Native Task Names on Asyncio +DESCRIPTION: Fixes native task names not being set on asyncio for Python 3.8+, ensuring tasks are correctly identified in debugging and profiling. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_182 + +LANGUAGE: Python +CODE: +``` +asyncio native task names +``` + +-------------------------------- + +TITLE: Fix Level-Triggered Cancellation Resume (Asyncio) +DESCRIPTION: Resolves a cancellation problem in asyncio where level-triggered cancellation for all parent cancel scopes would not resume after exiting a shielded nested scope. This issue is tracked under issue #370. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_131 + +LANGUAGE: Python +CODE: +``` +Fixed cancellation problem on asyncio where level-triggered cancellation for **all** + parent cancel scopes would not resume after exiting a shielded nested scope + (`#370 `_) +``` + +-------------------------------- + +TITLE: Fix TLSStream Send EOF Error +DESCRIPTION: Corrects `TLSStream.send_eof()` to raise `NotImplementedError` instead of `ValueError` when the operation is not supported, providing clearer error feedback. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_183 + +LANGUAGE: Python +CODE: +``` +anyio.TLSStream.send_eof() +``` + +-------------------------------- + +TITLE: AnyIO Recursive Traceback Fix +DESCRIPTION: This snippet details a bug fix in AnyIO 2.0.0 for asyncio and curio backends. It resolves an issue that caused recursive tracebacks when an exception from an inner task group was re-raised in an outer task group. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_207 + +LANGUAGE: Python +CODE: +``` +Fixed recursive tracebacks when a single exception from an inner task group is reraised in an outer task group +``` + +-------------------------------- + +TITLE: Run Synchronous Function in a Worker Thread +DESCRIPTION: Executes a synchronous function in a separate worker thread, preventing it from blocking the main asynchronous event loop. Useful for I/O-bound or CPU-bound synchronous code. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_19 + +LANGUAGE: Python +CODE: +``` +result = await anyio.to_thread.run_sync(sync_function, arg1, kwarg1=value) +``` + +-------------------------------- + +TITLE: Change Asyncio Task Groups Cancellation Handling +DESCRIPTION: Modifies how asyncio task groups handle cancellations. If only ``CancelledErrors`` are raised by host and child tasks, a single ``CancelledError`` is now raised instead of an ``ExceptionGroup``, allowing asyncio to ignore it during propagation. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_154 + +LANGUAGE: Python +CODE: +``` +Changed asyncio task groups so that if the host and child tasks have only raised + ``CancelledErrors``, just one ``CancelledError`` will now be raised instead of an + ``ExceptionGroup``, allowing asyncio to ignore it when it propagates out of the task +``` + +-------------------------------- + +TITLE: Fix CancelScope task uncancellation on Python 3.11 +DESCRIPTION: Implements a fix for CancelScope to properly handle asyncio task uncancellation on Python 3.11. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_111 + +LANGUAGE: Python +CODE: +``` +from anyio import CancelScope + +# This fix ensures that tasks within a CancelScope behave as expected +# regarding cancellation and uncancellation on Python 3.11. +``` + +-------------------------------- + +TITLE: Fix Task Cancellation Timing +DESCRIPTION: Ensures that task cancellation occurs immediately on asyncio and curio when a cancel scope is entered after its deadline has already passed. This prevents delays in cancellation processing. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_259 + +LANGUAGE: Python +CODE: +``` +import anyio +import time + +async def cancel_immediately_after_deadline(): + await anyio.sleep(1) # Simulate time passing + async with anyio.open_cancel_scope(deadline=0): + # Cancellation should happen immediately here +``` + +-------------------------------- + +TITLE: Sleep Until a Specific Time in AnyIO +DESCRIPTION: Suspends the current asynchronous task until a specified deadline (a timestamp) is reached. This is useful for scheduling tasks to run at a particular time. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_5 + +LANGUAGE: Python +CODE: +``` +await anyio.sleep_until(deadline_timestamp) +``` + +-------------------------------- + +TITLE: Asyncio CancelScope Exit Error +DESCRIPTION: Changes the asyncio `CancelScope` to raise a `RuntimeError` if a cancel scope is exited before it was entered, preventing incorrect scope management. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_173 + +LANGUAGE: Python +CODE: +``` +asyncio.CancelScope +``` + +-------------------------------- + +TITLE: Semaphore Max Value Property +DESCRIPTION: Adds the `max_value` property to `Semaphore`, providing access to the maximum value the semaphore can hold. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_171 + +LANGUAGE: Python +CODE: +``` +anyio.Semaphore.max_value +``` + +-------------------------------- + +TITLE: Check for Cancellation from Another Thread +DESCRIPTION: Checks if the current asynchronous operation, called from another thread, has been cancelled. This allows for cooperative cancellation handling. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_27 + +LANGUAGE: Python +CODE: +``` +if anyio.from_thread.check_cancelled(): + raise CancelledError() +``` + +-------------------------------- + +TITLE: Fix AssertionError on Subprocess Feed Data After Feed EOF +DESCRIPTION: Corrects an 'AssertionError: feed_data after feed_eof' that occurred on asyncio when a subprocess was closed early before its output could be read. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_53 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def run_subprocess_and_feed_early_close(): + try: + # Start a process that produces output + process = await anyio.open_process("python", "-c", "import sys; sys.stdout.write('hello\n'); sys.stdout.flush()") + + # Read some output + output = await process.read(10) + print(f"Read: {output.decode()}") + + # Close the subprocess early before reading all output + await process.terminate() + await process.wait() + + # Attempting to feed data after EOF might have caused the error + # This scenario is complex to reproduce directly without specific timing + + except anyio.exceptions.ProcessError as e: + print(f"Process error: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + +# anyio.run(run_subprocess_and_feed_early_close) +``` + +-------------------------------- + +TITLE: Fix MemoryObjectReceiveStream Cancellation State on Asyncio +DESCRIPTION: Resolves a bug in `MemoryObjectReceiveStream.receive()` on asyncio where the receiving task could remain in a cancelled state if cancellation occurred after an item was queued but before it was received. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_91 + +LANGUAGE: python +CODE: +``` +Fixed ``MemoryObjectReceiveStream.receive()`` causing the receiving task on asyncio to + remain in a cancelled state if the operation was cancelled after an item was queued to + be received by the task (but before the task could actually receive the item) +``` + +-------------------------------- + +TITLE: Asyncio Cancel Scope Enforcement +DESCRIPTION: Ensures cancel scopes are properly enforced with native asyncio coroutine functions without explicit AnyIO checkpoints, improving cancellation reliability in asyncio. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_172 + +LANGUAGE: Python +CODE: +``` +asyncio.CancelScope +``` + +-------------------------------- + +TITLE: Fix Exception Group Handling with Timed Scopes +DESCRIPTION: Ensures that an exception group containing only cancellation exceptions is correctly swallowed by a timed-out cancel scope on asyncio and curio. This prevents unexpected propagation of cancellation-related exceptions. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_260 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def handle_cancellation_exception_group(): + try: + async with anyio.open_cancel_scope(deadline=1): + # Simulate raising an exception group with cancellations + pass + except anyio.get_cancelled_exc_class(): + # This should be caught by the timed scope +``` + +-------------------------------- + +TITLE: Fix Debugger Quit Hanging Pytest Test Session +DESCRIPTION: Resolves a bug where quitting the debugger within a pytest test session, while in an active task group, caused the test to fail instead of exiting the session. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_56 + +LANGUAGE: Python +CODE: +``` +import anyio +import pytest + +async def task_that_might_be_debugged(): + await anyio.sleep(10) + +# To reproduce: Run pytest with a debugger attached, +# set a breakpoint inside task_that_might_be_debugged, +# and then quit the debugger while the breakpoint is hit. + +# Example test function structure: +# async def test_debugger_quit(): +# async with anyio.create_task_group() as tg: +# await tg.spawn(task_that_might_be_debugged) +# # Set breakpoint here or within the spawned task +# await anyio.sleep(1) # Keep task group alive + +``` + +-------------------------------- + +TITLE: Fix RuntimeError on CancelledError from BlockingPortal Task (Asyncio) +DESCRIPTION: Resolves a ``RuntimeError`` that occurred in asyncio when a ``CancelledError`` was raised from a task spawned via a ``BlockingPortal`. This is related to issue #357. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_134 + +LANGUAGE: Python +CODE: +``` +Fixed ``RuntimeError`` on asyncio when a ``CancelledError`` is raised from a task + spawned through a ``BlockingPortal`` + (`#357 `_) +``` + +-------------------------------- + +TITLE: Fix asyncio cancel scopes not propagating CancelledError +DESCRIPTION: Addresses an issue where cancel scopes on asyncio would not propagate `CancelledError` on exit when the enclosing cancel scope was effectively cancelled. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_39 + +LANGUAGE: Python +CODE: +``` +Fixed cancel scopes on asyncio not propagating ``CancelledError`` on exit when the + enclosing cancel scope has been effectively cancelled + (`#698 `_) +``` + +-------------------------------- + +TITLE: Fix Task Cancellation Mishandling +DESCRIPTION: Corrects the mishandling of task cancellation while a task is running a worker thread on asyncio and curio backends. This ensures that cancellations are processed correctly even when involving threads. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_253 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def task_in_thread(): + # Simulate work in a thread + pass + +async def cancel_task_in_thread(): + async with anyio.create_task_group() as tg: + await tg.spawn(anyio.run_in_thread, task_in_thread) + await tg.cancel_scope.cancel() +``` + +-------------------------------- + +TITLE: Fix Worker Thread Hang (Asyncio) +DESCRIPTION: Corrects an asyncio bug where, under specific circumstances, a stopping worker thread would continue to accept new assignments, leading to a program hang. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_141 + +LANGUAGE: Python +CODE: +``` +Fixed an asyncio bug where under certain circumstances, a stopping worker thread would + still accept new assignments, leading to a hang +``` + +-------------------------------- + +TITLE: Cancellable Parameter for run_in_thread +DESCRIPTION: Adds a `cancellable` parameter to the `anyio.run_in_thread()` function. When set to `True`, tasks running in threads via this function can be cancelled, allowing for better control over long-running threaded operations. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_226 + +LANGUAGE: Python +CODE: +``` +import anyio + +def blocking_io(): + # Simulate a long-running blocking operation + pass + +async def run_cancellable_thread(): + await anyio.run_in_thread(blocking_io, cancellable=True) +``` + +-------------------------------- + +TITLE: Remove Unnecessary Wait Cycle in Event.wait() on Asyncio +DESCRIPTION: Optimizes `Event.wait()` on the asyncio backend by removing an unnecessary extra waiting cycle that occurred when the event was not yet set. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_94 + +LANGUAGE: python +CODE: +``` +Removed unnecessary extra waiting cycle in ``Event.wait()`` on asyncio in the case + where the event was not yet set +``` + +-------------------------------- + +TITLE: Fix inconsistent task uncancellation on asyncio +DESCRIPTION: Corrects inconsistent task uncancellation behavior on asyncio when cancel scopes belong to a task group with running child tasks. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_41 + +LANGUAGE: Python +CODE: +``` +Fixed inconsistent task uncancellation with asyncio cancel scopes belonging to a + task group when said task group has child tasks running +``` + +-------------------------------- + +TITLE: AnyIO Byte Stream Closing Behavior +DESCRIPTION: This snippet describes a backwards-incompatible change in AnyIO 2.0.0 for byte streams. Byte streams, including socket streams, now raise `EndOfStream` instead of returning an empty bytes object when the stream is closed by the other end. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_201 + +LANGUAGE: Python +CODE: +``` +Byte streams (including socket streams) now raise `EndOfStream` instead of returning an empty bytes object when the stream has been closed from the other end +``` + +-------------------------------- + +TITLE: Fix cancelled cancel scopes on asyncio exit +DESCRIPTION: Corrects behavior where cancelled cancel scopes on asyncio would call `asyncio.Task.uncancel` when propagating a `CancelledError` on exit to a cancelled parent scope. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_34 + +LANGUAGE: Python +CODE: +``` +Fixed cancelled cancel scopes on asyncio calling ``asyncio.Task.uncancel`` when + propagating a ``CancelledError`` on exit to a cancelled parent scope + (`#790 `_; PR by @gschaffner) +``` + +-------------------------------- + +TITLE: Fix Idle Thread Pruning (Asyncio) +DESCRIPTION: Addresses an issue in asyncio where idle thread pruning could sometimes result in an expired worker thread being assigned a task. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_142 + +LANGUAGE: Python +CODE: +``` +Fixed idle thread pruning on asyncio sometimes causing an expired worker thread to be + assigned a task +``` + +-------------------------------- + +TITLE: Fix Deadlock in Synchronization Primitives (Asyncio) +DESCRIPTION: Addresses a deadlock issue in AnyIO's synchronization primitives when used with asyncio. This occurs when a task acquiring a primitive is cancelled by a native (non-AnyIO) cancellation at a critical moment, causing subsequent tasks to wait indefinitely. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_120 + +LANGUAGE: Python +CODE: +``` +Fixed deadlock in synchronization primitives on asyncio which can happen if a task + acquiring a primitive is hit with a native (not AnyIO) cancellation with just the + right timing, leaving the next acquiring task waiting forever + (`#398 `_) +``` + +-------------------------------- + +TITLE: Fix Async Fixture Self in Class-Based Tests +DESCRIPTION: Corrects an issue where the 'self' of an async fixture differed from the 'self' of the test in class-based tests. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_44 + +LANGUAGE: Python +CODE: +``` +import anyio +import pytest + +class TestMyClass: + @pytest.fixture + async def async_fixture(self): + return self + + async def test_fixture_self(self, async_fixture): + assert async_fixture is self +``` + +-------------------------------- + +TITLE: Fix asyncio CPU usage in exiting task groups +DESCRIPTION: Corrects a problem causing 100% CPU usage on asyncio when waiting for an exiting task group that is within a cancelled cancel scope. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_38 + +LANGUAGE: Python +CODE: +``` +Fixed 100% CPU use on asyncio while waiting for an exiting task group to finish while + said task group is within a cancelled cancel scope + (`#695 `_) +``` + +-------------------------------- + +TITLE: Fix Race Condition in Lock and Semaphore (Asyncio) +DESCRIPTION: Resolves a race condition in the ``Lock`` and ``Semaphore`` classes within AnyIO when used with asyncio. This issue occurred when a task waiting to acquire a primitive was cancelled while another task was also waiting for the same primitive. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_123 + +LANGUAGE: Python +CODE: +``` +Fixed race condition in ``Lock`` and ``Semaphore`` classes when a task waiting on + ``acquire()`` is cancelled while another task is waiting to acquire the same primitive + (`#387 `_) +``` + +-------------------------------- + +TITLE: Fix Trio Event Inheritance +DESCRIPTION: Corrects `Event` objects on the Trio backend to inherit from `anyio.abc.Event`, ensuring consistent interface adherence. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_189 + +LANGUAGE: Python +CODE: +``` +anyio.abc.Event +``` + +-------------------------------- + +TITLE: Fix AttributeError on Generator Task Cancellation +DESCRIPTION: Resolves an `AttributeError` that occurred when cancelling generator-based tasks in the asyncio backend. This ensures that cancellation works correctly for all task types. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_240 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def generator_task(): + yield + +async def cancel_generator_task(): + task = await anyio.create_task_group().spawn(generator_task) + await task.cancel() +``` + +-------------------------------- + +TITLE: Sleep for a Duration in AnyIO +DESCRIPTION: Suspends the current asynchronous task for a specified number of seconds. This is a fundamental operation for yielding control back to the event loop. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_3 + +LANGUAGE: Python +CODE: +``` +await anyio.sleep(1.5) +``` + +-------------------------------- + +TITLE: Fix CancelledError Leaking from Task Group +DESCRIPTION: Addresses an issue where `CancelledError` could leak from a task group when contained within an exception group. This fix ensures that cancellation exceptions are handled correctly within task group structures. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_244 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def task_with_exception_group(): + async with anyio.create_task_group() as tg: + await tg.spawn(some_task_that_raises_cancellation) + # Exception group handling is now more robust +``` + +-------------------------------- + +TITLE: Sleep Indefinitely in AnyIO +DESCRIPTION: Suspends the current asynchronous task indefinitely until it is cancelled. This is useful for tasks that should run continuously until explicitly stopped. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/api.rst#_snippet_4 + +LANGUAGE: Python +CODE: +``` +await anyio.sleep_forever() +``` + +-------------------------------- + +TITLE: AnyIO Task Group Cancellation Bug Fix +DESCRIPTION: This snippet addresses a bug fixed in AnyIO 2.0.0 for asyncio and curio backends. It resolves an issue where a task group would abandon its subtasks if its own cancel scope was cancelled while waiting for subtasks to finish. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_206 + +LANGUAGE: Python +CODE: +``` +Fixed a bug where a task group would abandon its subtasks if its own cancel scope was cancelled while it was waiting for subtasks to finish +``` + +-------------------------------- + +TITLE: Fix CancelledError Leaking from Cancel Scope +DESCRIPTION: Addresses an issue where `CancelledError` could leak from a cancel scope on asyncio if the task had previously received a cancellation exception. This fix ensures proper cancellation propagation and cleanup. + +SOURCE: https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst#_snippet_239 + +LANGUAGE: Python +CODE: +``` +import anyio + +async def problematic_operation(): + try: + async with anyio.open_cancel_scope() as scope: + # Operation that might be cancelled + pass + except anyio.get_cancelled_exc_class(): + # Handle cancellation properly +``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 198eaadf..6f63da65 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -16,8 +16,15 @@ The easiest way to install ccproxy is using pipx, which installs it in an isolat python -m pip install --user pipx python -m pipx ensurepath -# Install ccproxy -pipx install git+https://github.com/CaddyGlow/ccproxy-api.git +# Install the latest release (extras optional) +pipx install "ccproxy-api[all]==0.2.0" +``` + +To run the CLI without installing it globally, use `uvx` to pull the same +release on demand: + +```bash +uvx --with "ccproxy-api[all]==0.2.0" ccproxy serve --reload ``` ### From Source (Development) @@ -104,6 +111,8 @@ This displays full credential details including storage location and automatical ### Option 1: Local Installation After authentication, test the installation: + +```bash ccproxy claude -- /status ``` diff --git a/docs/migration/0.2-plugin-first.md b/docs/migration/0.2-plugin-first.md new file mode 100644 index 00000000..df424eae --- /dev/null +++ b/docs/migration/0.2-plugin-first.md @@ -0,0 +1,53 @@ +# Migration Guide: 0.2 Plugin-First + +CCProxy 0.2 introduces a plugin-first runtime that replaces the monolithic service wiring from the 0.1 series. This guide walks through the changes you need to make when upgrading an existing deployment or custom integrations to the new extension system. + +## What Changed in 0.2 +- **Manifests drive discovery** – Every provider and system feature now declares a `PluginManifest` that advertises routes, hooks, middleware, CLI extensions, and configuration schemas. +- **DI container owns lifecycle** – Factories receive a `ServiceContainer` and register dependencies explicitly instead of importing singletons from core modules. +- **Hook-based observability** – Access logging, analytics, and tracing now flow through the hook manager instead of bespoke middleware stacks. +- **Format adapters are declarative** – Request/response conversions are registered with the global format registry and validated during startup. + +## Prerequisites +- Upgrade to Python 3.11 or later. +- Run `make dev-install` to sync the new dependency graph and pre-commit hooks. +- Ensure your configuration files are tracked in version control before you modify them. + +## Step 1 – Enable the Plugin Runtime +1. Verify `enable_plugins = true` is set in `ccproxy.toml` or environment variables. +2. Remove `ENABLED_PLUGINS`/`DISABLED_PLUGINS` bash scripts that patched the legacy loader; use `[plugins]` configuration blocks instead. +3. If you ship custom plugins, add their directories to `[plugin_discovery.directories]`. + +## Step 2 – Update Provider Integrations +1. Move adapter code into `ccproxy/plugins//` and expose a `factory` that inherits from `ProviderPluginFactory`. +2. Implement `create_runtime()` to return a subclass of `ProviderPluginRuntime` that wires adapters, detection services, and token managers through the container. +3. Register routes using `PluginManifest.routes` instead of `FastAPI.include_router()` inside the adapter. +4. Provide manifest metadata (version, description, component type) so `/plugins` and health endpoints remain accurate. + +## Step 3 – Declare Hooks and Middleware +1. Replace custom middleware with hook implementations where possible. Hooks receive structured `HookContext` objects and do not block the request pipeline when they fail. +2. If you still need middleware, return `MiddlewareSpec` entries from the manifest so the `MiddlewareManager` can order them consistently. +3. Use `HookEvent.HTTP_REQUEST`/`HTTP_RESPONSE` for analytics instead of tapping directly into streaming handlers. + +## Step 4 – Configure Format Chains +1. Annotate route handler functions with `@format_chain([...])` decorators from `ccproxy.services.adapters.chain_validation`. +2. Add adapter factories to the manifest via `format_adapters`. +3. Run `make test-unit` to confirm `validate_chains` and `validate_stream_pairs` succeed during startup. + +## Step 5 – Update Configuration Files +1. Move provider-specific keys under `[plugins.]` sections. +2. Use nested environment variables (e.g., `PLUGINS__CODex__MODEL_MAPPINGS`) to override settings in production. +3. Replace deprecated `[claude]`/`[codex]` roots with their plugin-scoped equivalents. + +## Step 6 – Refresh CLI Commands +1. The CLI now discovers plugin subcommands through `discover_plugin_cli_extensions()`. Ensure your plugin manifest declares CLI entries in `cli_commands`. +2. Regenerate shell completions after upgrading: `ccproxy --show-completion zsh > ~/.zfunc/_ccproxy`. + +## Validation Checklist +- `make ci` passes (ruff, mypy, pytest) with your custom plugins enabled. +- `uv run mkdocs build` renders docs without missing references. +- `/plugins`, `/health`, and `/plugins//health` display expected metadata. +- Authentication flows succeed via `ccproxy auth login ` and `ccproxy auth status`. + +## Rollback +The 0.1.x runtime remains available on the `release/0.1` branch. Rolling back requires restoring the earlier configuration format and removing plugin manifests. Because persistence schemas changed in 0.2, snapshot your credential stores before downgrading. diff --git a/docs/release-v0.2-status.md b/docs/release-v0.2-status.md new file mode 100644 index 00000000..45929ab6 --- /dev/null +++ b/docs/release-v0.2-status.md @@ -0,0 +1,30 @@ +# Release Readiness – v0.2.0 + +## Blocking Issues +- Version metadata still marked as development builds (`ccproxy/core/_version.py:31`; `CHANGELOG.md:8`). Tag `v0.2.0`, regenerate `_version.py`, and update the changelog date before packaging. +- Packaging depends on a local placeholder plugin (`pyproject.toml:22`; `uv.lock:561-575`). Remove the `dummy` dependency and refresh `uv.lock` or publish the plugin properly. +- MkDocs navigation references missing documents (`docs/migration/0.2-plugin-first.md`, `docs/OAUTH_PLUGIN_ARCHITECTURE.md`). Restore the sources or update navigation/README links so docs build succeeds. + +## Major Work Remaining +- Installation docs still point to dev branches (`README.md:155-159`; `docs/getting-started/installation.md:18-24`). Update once the official 0.2.0 artefacts exist. +- Backups and temporary files linger (.bak files under `ccproxy/`, `.ccproxy.toml.bak`, `scripts/test_endpoint.sh.20250913-110546.bak`). Remove them so they do not ship in sdists. +- `uv.lock` pins unreleased versions (e.g., `duckdb 1.3.2` dated 2025-07-08). Re-sync dependencies against published wheels and re-run the lock. + +## Outstanding TODOs – Detailed Triage +- **ccproxy/api/app.py:546** – The startup sequence still performs format-chain validation inline. Refactor this block into `MiddlewareManager` or a dedicated validator in `ccproxy/core/middleware.py`, then invoke it from startup. Add regression coverage via a unit test that exercises `MiddlewareManager.apply_to_app` with a mocked format registry to ensure validation happens before middleware application. +- **ccproxy/api/routes/health.py:9** – While the handlers manually set the `Content-Type`, FastAPI may still advertise `application/json` in the OpenAPI schema. Define `responses={200: {"content": {"application/health+json": {}}}}` or set `response_class=PlainTextResponse` with explicit serialization to guarantee the IETF media type. Add a contract test that hits `/health` and asserts `response.headers["content-type"] == "application/health+json"`. +- **ccproxy/services/adapters/http_adapter.py:577** – Streaming responses bypass the reverse format chain, so downstream clients may receive provider-native payloads. Implement a reversible SSE/JSON chunk transformer (possibly reusing `ccproxy.llms.streaming.accumulators`) and cover it with integration tests for both Anthropic→OpenAI and OpenAI→Anthropic streaming paths. +- **ccproxy/plugins/claude_api/detection_service.py:105** / **ccproxy/plugins/codex/detection_service.py:112** – Detection logic cannot be exercised without the respective CLIs. Introduce feature-flagged test fixtures that monkeypatch `_get_*_version` to raise `FileNotFoundError`, assert the fallback JSON is returned, and document in release notes that CLI-less environments rely on packaged defaults. +- **ccproxy/plugins/copilot/oauth/provider.py:319** – The `copilot_access` flag should reflect profile capabilities. Extend `CopilotProfile` parsing to surface access entitlements and populate the flag when fetching `get_user_profile()`. Include a unit test that loads a mocked profile with business access and confirms the flag is true. +- **ccproxy/plugins/docker/protocol.py:24** – The protocol omits discovery helpers (`get_version`, `image_info`, `list_images`). Either expand the protocol and the concrete adapter to implement them or prune the TODO and document the current surface so typed plugins do not rely on undefined methods. +- **ccproxy/plugins/credential_balancer/README.md:83** – The README still lists “cooldown TODO”. Either deliver dynamic cooldown handling (parsing `Retry-After` headers, plumbing through the hook) or rewrite the section to describe the current manual cooldown behaviour. +- **ccproxy/templates/plugin_scaffold.py:287** – The scaffold intentionally raises `503` with TODO messaging. Decide whether to ship a production-ready minimal adapter (e.g., echo passthrough) or update the README so new plugin authors understand they must replace the stub before shipping. + +## Minor Follow-Ups +- Hook implementation docstring promises hooks that are not exported (`ccproxy/core/plugins/hooks/implementations/__init__.py:7`). Align the documentation with the available implementations or include the missing hooks. +- Confirm final release copy for the changelog and docs (e.g., target release date, OAuth migration material) before tagging. + +## Suggested Next Steps +1. Clean up packaging (drop `dummy`, update lockfile, verify wheels build). +2. Repair documentation sources and navigation, then run `uv run mkdocs build` and `make ci`. +3. Finalize release metadata: set the changelog date, regenerate `_version.py`, tag `v0.2.0`, and rebuild artefacts. diff --git a/pyproject.toml b/pyproject.toml index 8f17e2e8..aabfe4b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ dependencies = [ "packaging>=25.0", "sortedcontainers>=2.4.0", "pyjwt>=2.10.1", - "dummy", ] [build-system] @@ -47,22 +46,25 @@ all = [ "sqlmodel>=0.0.24", "SQLAlchemy>=2.0.0", "duckdb-engine>=0.17.0", - "duckdb>=1.1.0", + "duckdb>=1.1.0,<1.4.0", "fastapi-mcp>=0.3.7", "textual>=3.7.1", "aioconsole>=0.8.1", "prometheus-client>=0.22.1", - "claude-agent-sdk>=0.1.0", "sse-starlette>=3.0.2", + "claude-agent-sdk>=0.1.0", ] -plugins-claude = ["claude-agent-sdk>=0.0.19", "qrcode>=8.2"] +plugins-claude = [ + "claude-agent-sdk>=0.1.0", + "qrcode>=8.2", +] plugins-codex = ["qrcode>=8.2", "pyjwt>=2.10.1"] plugins-storage = [ "sqlmodel>=0.0.24", "SQLAlchemy>=2.0.0", "duckdb-engine>=0.17.0", - "duckdb>=1.1.0", + "duckdb>=1.1.0,<1.4.0", ] plugins-mcp = ["fastapi-mcp>=0.3.7"] plugins-tui = ["textual>=3.7.1", "aioconsole>=0.8.1"] @@ -364,10 +366,6 @@ legacy_tox_ini = """ [tool.uv] package = true -[tool.uv.sources] -claude-agent-sdk = { git = "https://github.com/anthropics/claude-agent-sdk-python.git" } -dummy = { path = "../../.config/ccproxy/plugins/dummy.ccproxy" } - [tool.pyright] pythonVersion = "3.11" diff --git a/scripts/check_import_boundaries.py b/scripts/check_import_boundaries.py index 18132111..64dab4b2 100644 --- a/scripts/check_import_boundaries.py +++ b/scripts/check_import_boundaries.py @@ -23,6 +23,12 @@ from typing import NamedTuple +REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] + +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + + DEFAULT_CONTEXT_LINES = 4 # Default number of context lines to show around violations diff --git a/tests/api/middleware/test_streaming_stress.py b/tests/api/middleware/test_streaming_stress.py new file mode 100644 index 00000000..863f680f --- /dev/null +++ b/tests/api/middleware/test_streaming_stress.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import time +from collections.abc import MutableMapping +from typing import Any + +import anyio +import pytest + +from ccproxy.api.middleware.streaming_hooks import StreamingResponseWithHooks +from ccproxy.core.logging import get_logger +from ccproxy.core.plugins.hooks import HookManager +from ccproxy.core.plugins.hooks.base import Hook, HookContext +from ccproxy.core.plugins.hooks.events import HookEvent +from ccproxy.core.plugins.hooks.registry import HookRegistry +from ccproxy.core.request_context import RequestContext + + +class _StreamingCollectorHook(Hook): + name = "streaming_collector" + events = [HookEvent.HTTP_RESPONSE, HookEvent.REQUEST_COMPLETED] + priority = 500 + + def __init__(self, results: dict[str, set[HookEvent]]) -> None: + self._results = results + + async def __call__(self, context: HookContext) -> None: + request_id = context.data.get("request_id") + if not request_id: + return + self._results.setdefault(request_id, set()).add(context.event) + + +class _DummyStreamResponse: + def __init__(self, chunks: list[bytes]) -> None: + self._chunks = chunks + self.status_code = 200 + self.headers = {"content-type": "text/event-stream"} + self.media_type = "text/event-stream" + + async def aiter_bytes(self) -> Any: + for chunk in self._chunks: + await anyio.sleep(0) + yield chunk + + async def aread(self) -> bytes: + return b"".join(self._chunks) + + +@pytest.mark.slow +@pytest.mark.asyncio +async def test_streaming_hooks_stress() -> None: + registry = HookRegistry() + results: dict[str, set[HookEvent]] = {} + collector = _StreamingCollectorHook(results) + registry.register(collector) + + hook_manager = HookManager(registry) + + total_streams = 40 + chunks_per_stream = 5 + + async def run_stream(index: int) -> None: + request_id = f"stream-stress-{index}" + request_context = RequestContext( + request_id=request_id, + start_time=time.perf_counter(), + logger=get_logger(__name__).bind(test="stream-stress"), + metadata={"service_type": "stream-stress"}, + ) + + stream_chunks = [ + f"chunk-{index}-{idx}\n".encode() for idx in range(chunks_per_stream) + ] + response = StreamingResponseWithHooks( + content=_DummyStreamResponse(stream_chunks).aiter_bytes(), + hook_manager=hook_manager, + request_id=request_id, + request_data={ + "method": "GET", + "url": f"https://example.test/stream/{index}", + "headers": {"accept": "text/event-stream"}, + }, + request_metadata=request_context.metadata, + start_time=time.perf_counter(), + status_code=200, + ) + + scope = { + "type": "http", + "asgi": {"version": "3.0", "spec_version": "2.3"}, + "http_version": "1.1", + "method": "GET", + "path": f"/stream/{index}", + "headers": [], + } + + receive_called = False + + async def receive() -> dict[str, Any]: # noqa: ANN401 + nonlocal receive_called + if receive_called: + await anyio.sleep(0) + return {"type": "http.disconnect"} + receive_called = True + return {"type": "http.request", "body": b"", "more_body": False} + + async def send(message: MutableMapping[str, Any]) -> None: + # Consume messages without storing to keep test lightweight + if message.get("type") == "http.response.body" and not message.get( + "more_body", False + ): + await anyio.sleep(0) + + await response(scope, receive, send) + + async with anyio.create_task_group() as tg: + for stream_index in range(total_streams): + tg.start_soon(run_stream, stream_index) + + # Allow background hook tasks to flush + await anyio.sleep(0.05) + + assert len(results) == total_streams + for recorded_events in results.values(): + assert HookEvent.HTTP_RESPONSE in recorded_events + assert HookEvent.REQUEST_COMPLETED in recorded_events diff --git a/tests/fixtures/integration.py b/tests/fixtures/integration.py index 42348093..b3a19a20 100644 --- a/tests/fixtures/integration.py +++ b/tests/fixtures/integration.py @@ -124,7 +124,7 @@ async def _create_app(plugin_configs: dict[str, dict[str, Any]]) -> FastAPI: # Set up logging manually for test environment - minimal logging for speed from ccproxy.core.logging import setup_logging - setup_logging(json_logs=False, log_level_name="DEBUG") + setup_logging(json_logs=False, log_level_name="ERROR") # Explicitly disable known default-on system plugins that can cause I/O # side effects in isolated test environments unless requested. diff --git a/tests/plugins/analytics/integration/test_analytics_pagination_stress.py b/tests/plugins/analytics/integration/test_analytics_pagination_stress.py new file mode 100644 index 00000000..5d00493f --- /dev/null +++ b/tests/plugins/analytics/integration/test_analytics_pagination_stress.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import random +import time +from pathlib import Path + +import anyio +import pytest + +from ccproxy.plugins.analytics.service import AnalyticsService +from ccproxy.plugins.duckdb_storage.storage import SimpleDuckDBStorage + + +@pytest.mark.integration +@pytest.mark.slow +@pytest.mark.asyncio +async def test_duckdb_analytics_pagination_stress(tmp_path: Path) -> None: + storage = SimpleDuckDBStorage(tmp_path / "pagination.duckdb") + await storage.initialize() + engine = storage._engine + assert engine is not None + + total_logs = 1500 + base_time = time.time() + + for index in range(total_logs): + payload = { + "request_id": f"log-{index}", + "timestamp": base_time - index, + "method": "POST", + "endpoint": f"/v1/tools/{index % 5}", + "path": f"/v1/tools/{index % 5}", + "query": "", + "client_ip": "127.0.0.1", + "user_agent": "pytest-agent", + "service_type": "analytics-stress", + "provider": "duckdb", + "model": f"model-{index % 3}", + "streaming": False, + "status_code": 200, + "duration_ms": random.uniform(5, 40), + "duration_seconds": 0.02, + "tokens_input": index % 11, + "tokens_output": index % 7, + "cache_read_tokens": 0, + "cache_write_tokens": 0, + "cost_usd": 0.0001, + } + await storage.store_request(payload) + + svc = AnalyticsService(engine) + limit = 200 + cursor = None + fetched = 0 + iterations = 0 + + while True: + page = svc.query_logs(limit=limit, cursor=cursor, order="desc") + fetched += page["count"] + iterations += 1 + + if not page["has_more"]: + break + + cursor = page["next_cursor"] + assert cursor is not None + assert page["count"] == limit + assert page["results"][0]["timestamp"] >= page["results"][-1]["timestamp"] + + # Yield control periodically to avoid tight loop in async context + await anyio.sleep(0) + + assert fetched == total_logs + assert iterations >= 2 diff --git a/tests/plugins/analytics/integration/test_duckdb_access_log_stress.py b/tests/plugins/analytics/integration/test_duckdb_access_log_stress.py new file mode 100644 index 00000000..bc39454f --- /dev/null +++ b/tests/plugins/analytics/integration/test_duckdb_access_log_stress.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from pathlib import Path + +import anyio +import pytest +from sqlmodel import Session, select + +from ccproxy.core.plugins.hooks.base import HookContext +from ccproxy.core.plugins.hooks.events import HookEvent +from ccproxy.plugins.access_log.config import AccessLogConfig +from ccproxy.plugins.access_log.hook import AccessLogHook +from ccproxy.plugins.analytics.ingest import AnalyticsIngestService +from ccproxy.plugins.analytics.models import AccessLog +from ccproxy.plugins.duckdb_storage.storage import SimpleDuckDBStorage + + +@pytest.mark.integration +@pytest.mark.slow +@pytest.mark.asyncio +async def test_duckdb_access_log_ingest_stress(tmp_path: Path) -> None: + """High-volume ingest ensures AccessLogHook + DuckDB stay in sync.""" + + storage = SimpleDuckDBStorage(tmp_path / "stress.duckdb") + await storage.initialize() + engine = storage._engine + assert engine is not None + + ingest_service = AnalyticsIngestService(engine) + access_log_config = AccessLogConfig( + client_log_file=str(tmp_path / "access.log"), + buffer_size=32, + flush_interval=0.05, + provider_enabled=False, + ) + + hook = AccessLogHook(access_log_config) + hook.ingest_service = ingest_service + + total_requests = 200 + + async def emit_request(request_index: int) -> None: + request_id = f"stress-{request_index}" + path = f"/v1/test/{request_index}" + + start_context = HookContext( + event=HookEvent.REQUEST_STARTED, + timestamp=datetime.now(UTC), + data={ + "request_id": request_id, + "method": "POST", + "url": f"https://api.example.com{path}", + "path": path, + "client_ip": "127.0.0.1", + "user_agent": "pytest-agent", + "service_type": "stress", + "provider": "duckdb", + "model": "stress-model", + }, + metadata={}, + ) + + completion_context = HookContext( + event=HookEvent.REQUEST_COMPLETED, + timestamp=datetime.now(UTC), + data={ + "request_id": request_id, + "status_code": 200, + "body_size": 512, + "response_status": 200, + }, + metadata={ + "tokens_input": request_index % 7, + "tokens_output": (request_index * 2) % 11, + "cost_usd": 0.01, + }, + ) + + await hook(start_context) + await anyio.sleep(0) # encourage interleaving between tasks + await hook(completion_context) + + async with anyio.create_task_group() as task_group: + for index in range(total_requests): + task_group.start_soon(emit_request, index) + + await hook.close() + + with Session(engine) as session: + rows = session.exec(select(AccessLog)).all() + + assert len(rows) == total_requests + + sample = rows[0] + assert sample.service_type == "stress" + assert sample.provider == "duckdb" + assert sample.user_agent == "pytest-agent" diff --git a/tests/plugins/claude_api/integration/test_auth_missing.py b/tests/plugins/claude_api/integration/test_auth_missing.py deleted file mode 100644 index 7a275c49..00000000 --- a/tests/plugins/claude_api/integration/test_auth_missing.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Any -from unittest.mock import patch - -import httpx -import pytest - - -@pytest.mark.asyncio -@pytest.mark.integration -async def test_claude_api_missing_auth_manager_returns_401_integration( - integration_client_factory: Any, -) -> None: - plugin_configs = { - "claude_api": { - "enabled": True, - "auth_manager": "missing_claude_manager", - }, - "oauth_claude": {"enabled": True}, - } - - client = await integration_client_factory(plugin_configs) - - blocked_hosts = {"api.anthropic.com"} - original_send = httpx.AsyncClient.send - - async def guard_send( - self: httpx.AsyncClient, request: httpx.Request, *args: Any, **kwargs: Any - ) -> httpx.Response: - if request.url.host in blocked_hosts: - raise AssertionError(f"Unexpected upstream call to {request.url!s}") - return await original_send(self, request, *args, **kwargs) - - async with client as http: - with patch("httpx.AsyncClient.send", guard_send): - resp = await http.post( - "/claude/v1/messages", - json={ - "model": "claude-3-haiku", - "messages": [], - "max_tokens": 128, - }, - ) - - assert resp.status_code == 401 diff --git a/tests/plugins/claude_api/integration/test_claude_api_basic.py b/tests/plugins/claude_api/integration/test_claude_api_basic.py index f20fc90c..6fc65c40 100644 --- a/tests/plugins/claude_api/integration/test_claude_api_basic.py +++ b/tests/plugins/claude_api/integration/test_claude_api_basic.py @@ -363,7 +363,7 @@ async def test_anthropic_messages_streaming( pytestmark = pytest.mark.asyncio(loop_scope="module") -@pytest_asyncio.fixture(scope="function", loop_scope="function") +@pytest_asyncio.fixture(scope="module", loop_scope="module") async def claude_api_client() -> Any: # type: ignore[misc] # Build app and client once to avoid factory scope conflicts from httpx import ASGITransport, AsyncClient diff --git a/tests/plugins/claude_api/integration/test_endpoint_runner.py b/tests/plugins/claude_api/integration/test_endpoint_runner.py index 69e124d5..0ecdcb9a 100644 --- a/tests/plugins/claude_api/integration/test_endpoint_runner.py +++ b/tests/plugins/claude_api/integration/test_endpoint_runner.py @@ -2,7 +2,11 @@ from __future__ import annotations +from collections.abc import AsyncGenerator +from typing import Any + import pytest +import pytest_asyncio from httpx import ASGITransport, AsyncClient from tests.helpers.endpoint_runner import ( AVAILABLE_CASES, @@ -20,7 +24,7 @@ pytestmark = [ pytest.mark.integration, pytest.mark.e2e, - pytest.mark.asyncio(loop_scope="function"), + pytest.mark.asyncio(loop_scope="module"), ] @@ -28,29 +32,37 @@ SAMPLES = provider_sample_names(PROVIDER) +@pytest_asyncio.fixture(scope="module", loop_scope="module") +async def claude_endpoint_tester() -> AsyncGenerator[TestEndpoint, None]: + """Initialize a shared app/client pair for Claude endpoint tests.""" + + app_builder = PROVIDER_APP_BUILDERS[PROVIDER] + async with app_builder() as app: + transport = ASGITransport(app=app) + client = AsyncClient(transport=transport, base_url=BASE_URL) + tester = TestEndpoint(base_url=BASE_URL, client=client) + try: + yield tester + finally: + await client.aclose() + + @pytest.mark.parametrize("sample_name", SAMPLES, ids=SAMPLES) async def test_claude_endpoint_sample( # type: ignore[no-untyped-def] sample_name: str, request: pytest.FixtureRequest, - httpx_mock, + httpx_mock: Any, + claude_endpoint_tester: TestEndpoint, ) -> None: fixture_name = PROVIDER_FIXTURES.get(PROVIDER) if fixture_name: request.getfixturevalue(fixture_name) - app_builder = PROVIDER_APP_BUILDERS[PROVIDER] - - async with app_builder() as app: - transport = ASGITransport(app=app) - async with ( - AsyncClient(transport=transport, base_url=BASE_URL) as client, - TestEndpoint(base_url=BASE_URL, client=client) as tester, - ): - endpoint_case = AVAILABLE_CASES[sample_name] - result = await tester.run_endpoint_test( - endpoint_case, - CASE_INDEX_LOOKUP[endpoint_case.name], - ) + endpoint_case = AVAILABLE_CASES[sample_name] + result = await claude_endpoint_tester.run_endpoint_test( + endpoint_case, + CASE_INDEX_LOOKUP[endpoint_case.name], + ) assert_initial_request(result, endpoint_case.stream, endpoint_case.request) assert_follow_up_requests(result) diff --git a/tests/plugins/claude_api/unit/test_auth_missing.py b/tests/plugins/claude_api/unit/test_auth_missing.py index fb417b6d..b1893e61 100644 --- a/tests/plugins/claude_api/unit/test_auth_missing.py +++ b/tests/plugins/claude_api/unit/test_auth_missing.py @@ -1,8 +1,12 @@ -from unittest.mock import patch +from contextlib import AsyncExitStack +from unittest.mock import AsyncMock, patch import httpx import pytest +from ccproxy.models.detection import DetectedHeaders, DetectedPrompts +from ccproxy.plugins.claude_api.models import ClaudeCacheData + @pytest.mark.unit @pytest.mark.asyncio @@ -17,8 +21,6 @@ async def test_claude_api_missing_auth_manager_returns_401( "oauth_claude": {"enabled": True}, } - client = await integration_client_factory(plugin_configs) # type: ignore[operator] - blocked_hosts = {"api.anthropic.com"} original_send = httpx.AsyncClient.send @@ -29,7 +31,47 @@ async def guard_send( raise AssertionError(f"Unexpected upstream call to {request.url!s}") return await original_send(self, request, *args, **kwargs) # type: ignore[arg-type] - async with client as http: + prompts = DetectedPrompts.from_body( + {"system": [{"type": "text", "text": "Hello from tests."}]} + ) + detection_data = ClaudeCacheData( + claude_version="fallback", + headers=DetectedHeaders({}), + prompts=prompts, + body_json=prompts.raw, + method="POST", + url=None, + path=None, + query_params=None, + ) + + async def init_detection_stub(self): # type: ignore[no-untyped-def] + self._cached_data = detection_data + return detection_data + + async with AsyncExitStack() as stack: + stack.enter_context( + patch( + "ccproxy.plugins.claude_api.detection_service.ClaudeAPIDetectionService.initialize_detection", + new=init_detection_stub, + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.oauth_claude.manager.ClaudeApiTokenManager.get_access_token", + new=AsyncMock(return_value="test-claude-access-token"), + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.oauth_claude.manager.ClaudeApiTokenManager.load_credentials", + new=AsyncMock(return_value=None), + ) + ) + + client = await integration_client_factory(plugin_configs) # type: ignore[operator] + http = await stack.enter_async_context(client) + with patch("httpx.AsyncClient.send", guard_send): resp = await http.post( "/claude/v1/messages", diff --git a/tests/plugins/codex/integration/test_auth_missing.py b/tests/plugins/codex/integration/test_auth_missing.py deleted file mode 100644 index e5479680..00000000 --- a/tests/plugins/codex/integration/test_auth_missing.py +++ /dev/null @@ -1,39 +0,0 @@ -from unittest.mock import patch - -import httpx -import pytest - - -@pytest.mark.asyncio -@pytest.mark.integration -async def test_codex_missing_auth_manager_returns_401_integration( - integration_client_factory: object, -) -> None: - plugin_configs = { - "codex": { - "enabled": True, - "auth_manager": "missing_codex_manager", - }, - "oauth_codex": {"enabled": True}, - } - - client = await integration_client_factory(plugin_configs) # type: ignore[operator] - - blocked_hosts = {"chatgpt.com", "api.openai.com"} - original_send = httpx.AsyncClient.send - - async def guard_send( - self: httpx.AsyncClient, request: httpx.Request, *args: object, **kwargs: object - ) -> httpx.Response: - if request.url.host in blocked_hosts: - raise AssertionError(f"Unexpected upstream call to {request.url!s}") - return await original_send(self, request, *args, **kwargs) # type: ignore[arg-type] - - async with client as http: - with patch("httpx.AsyncClient.send", guard_send): - resp = await http.post( - "/codex/v1/responses", - json={"model": "gpt-4o-mini", "input": []}, - ) - - assert resp.status_code == 401 diff --git a/tests/plugins/codex/integration/test_codex_basic.py b/tests/plugins/codex/integration/test_codex_basic.py index e46aebbf..ec2cd8a1 100644 --- a/tests/plugins/codex/integration/test_codex_basic.py +++ b/tests/plugins/codex/integration/test_codex_basic.py @@ -111,9 +111,9 @@ async def test_openai_chat_completions_streaming( pytestmark = pytest.mark.asyncio(loop_scope="module") -@pytest_asyncio.fixture(scope="function", loop_scope="function") +@pytest_asyncio.fixture(scope="module", loop_scope="module") async def codex_client() -> Any: # type: ignore[misc] - # Build app and client once to avoid factory scope conflicts + # Build app and client once per module to avoid factory scope conflicts from httpx import ASGITransport, AsyncClient from ccproxy.api.app import create_app, initialize_plugins_startup diff --git a/tests/plugins/codex/integration/test_endpoint_runner.py b/tests/plugins/codex/integration/test_endpoint_runner.py index c2f29910..4cfbd602 100644 --- a/tests/plugins/codex/integration/test_endpoint_runner.py +++ b/tests/plugins/codex/integration/test_endpoint_runner.py @@ -2,7 +2,11 @@ from __future__ import annotations +from collections.abc import AsyncGenerator +from typing import Any + import pytest +import pytest_asyncio from httpx import ASGITransport, AsyncClient from tests.helpers.endpoint_runner import ( AVAILABLE_CASES, @@ -20,7 +24,7 @@ pytestmark = [ pytest.mark.integration, pytest.mark.e2e, - pytest.mark.asyncio(loop_scope="function"), + pytest.mark.asyncio(loop_scope="module"), ] @@ -28,29 +32,37 @@ SAMPLES = provider_sample_names(PROVIDER) +@pytest_asyncio.fixture(scope="module", loop_scope="module") +async def codex_endpoint_tester() -> AsyncGenerator[TestEndpoint, None]: + """Initialize a shared app/client pair for Codex endpoint tests.""" + + app_builder = PROVIDER_APP_BUILDERS[PROVIDER] + async with app_builder() as app: + transport = ASGITransport(app=app) + client = AsyncClient(transport=transport, base_url=BASE_URL) + tester = TestEndpoint(base_url=BASE_URL, client=client) + try: + yield tester + finally: + await client.aclose() + + @pytest.mark.parametrize("sample_name", SAMPLES, ids=SAMPLES) async def test_codex_endpoint_sample( # type: ignore[no-untyped-def] sample_name: str, request: pytest.FixtureRequest, - httpx_mock, + httpx_mock: Any, + codex_endpoint_tester: TestEndpoint, ) -> None: fixture_name = PROVIDER_FIXTURES.get(PROVIDER) if fixture_name: request.getfixturevalue(fixture_name) - app_builder = PROVIDER_APP_BUILDERS[PROVIDER] - - async with app_builder() as app: - transport = ASGITransport(app=app) - async with ( - AsyncClient(transport=transport, base_url=BASE_URL) as client, - TestEndpoint(base_url=BASE_URL, client=client) as tester, - ): - endpoint_case = AVAILABLE_CASES[sample_name] - result = await tester.run_endpoint_test( - endpoint_case, - CASE_INDEX_LOOKUP[endpoint_case.name], - ) + endpoint_case = AVAILABLE_CASES[sample_name] + result = await codex_endpoint_tester.run_endpoint_test( + endpoint_case, + CASE_INDEX_LOOKUP[endpoint_case.name], + ) assert_initial_request(result, endpoint_case.stream, endpoint_case.request) assert_follow_up_requests(result) diff --git a/tests/plugins/codex/unit/test_auth_missing.py b/tests/plugins/codex/unit/test_auth_missing.py index fe4e3d65..a8bd5e14 100644 --- a/tests/plugins/codex/unit/test_auth_missing.py +++ b/tests/plugins/codex/unit/test_auth_missing.py @@ -1,9 +1,13 @@ +from contextlib import AsyncExitStack from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import httpx import pytest +from ccproxy.models.detection import DetectedHeaders, DetectedPrompts +from ccproxy.plugins.codex.models import CodexCacheData + @pytest.mark.unit @pytest.mark.asyncio @@ -18,8 +22,6 @@ async def test_codex_missing_auth_manager_returns_401( "oauth_codex": {"enabled": True}, } - client = await integration_client_factory(plugin_configs) - blocked_hosts = {"chatgpt.com", "api.openai.com"} original_send = httpx.AsyncClient.send @@ -30,7 +32,47 @@ async def guard_send( raise AssertionError(f"Unexpected upstream call to {request.url!s}") return await original_send(self, request, *args, **kwargs) - async with client as http: + prompts = DetectedPrompts.from_body( + {"instructions": "You are a helpful coding assistant."} + ) + detection_data = CodexCacheData( + codex_version="fallback", + headers=DetectedHeaders({}), + prompts=prompts, + body_json=prompts.raw, + method="POST", + url="https://chatgpt.com/backend-codex/responses", + path="/api/backend-codex/responses", + query_params={}, + ) + + async def init_detection_stub(self): # type: ignore[no-untyped-def] + self._cached_data = detection_data + return detection_data + + async with AsyncExitStack() as stack: + stack.enter_context( + patch( + "ccproxy.plugins.codex.detection_service.CodexDetectionService.initialize_detection", + new=init_detection_stub, + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.oauth_codex.manager.CodexTokenManager.load_credentials", + new=AsyncMock(return_value=None), + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.oauth_codex.manager.CodexTokenManager.get_profile_quick", + new=AsyncMock(return_value=None), + ) + ) + + client = await integration_client_factory(plugin_configs) + http = await stack.enter_async_context(client) + with patch("httpx.AsyncClient.send", guard_send): resp = await http.post( "/codex/v1/responses", diff --git a/tests/plugins/copilot/integration/test_auth_missing.py b/tests/plugins/copilot/integration/test_auth_missing.py deleted file mode 100644 index 3ec524c5..00000000 --- a/tests/plugins/copilot/integration/test_auth_missing.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Any -from unittest.mock import patch - -import httpx -import pytest - - -@pytest.mark.asyncio -@pytest.mark.integration -async def test_copilot_missing_auth_manager_returns_401_integration( - integration_client_factory: Any, -) -> None: - plugin_configs = { - "copilot": { - "enabled": True, - "auth_manager": "missing_copilot_manager", - } - } - - client = await integration_client_factory(plugin_configs) - - blocked_hosts = {"api.githubcopilot.com", "api.github.com"} - original_send = httpx.AsyncClient.send - - async def guard_send( - self: httpx.AsyncClient, request: httpx.Request, *args: Any, **kwargs: Any - ) -> httpx.Response: - if request.url.host in blocked_hosts: - raise AssertionError(f"Unexpected upstream call to {request.url!s}") - return await original_send(self, request, *args, **kwargs) - - async with client as http: - with patch("httpx.AsyncClient.send", guard_send): - resp = await http.post( - "/copilot/v1/chat/completions", - json={ - "model": "gpt-4o-mini", - "messages": [{"role": "user", "content": "hi"}], - }, - ) - - assert resp.status_code == 401 diff --git a/tests/plugins/copilot/integration/test_end_to_end.py b/tests/plugins/copilot/integration/test_end_to_end.py index b462f508..2f777182 100644 --- a/tests/plugins/copilot/integration/test_end_to_end.py +++ b/tests/plugins/copilot/integration/test_end_to_end.py @@ -1,6 +1,8 @@ """End-to-end integration tests for Copilot plugin.""" import json +from collections.abc import AsyncGenerator +from contextlib import AsyncExitStack from datetime import datetime from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -585,11 +587,11 @@ async def test_copilot_token_info_endpoint( assert "copilot_expires_at" in data -# Async fixtures per test to avoid cross-test event loop interactions -pytestmark = pytest.mark.asyncio(loop_scope="function") +# Async fixtures run at module scope; individual tests apply their own mocks +pytestmark = pytest.mark.asyncio(loop_scope="module") -@pytest_asyncio.fixture(scope="function", loop_scope="function") +@pytest_asyncio.fixture(scope="module", loop_scope="module") async def copilot_integration_app(): """Pre-configured app for Copilot plugin integration tests - session scoped.""" from ccproxy.api.app import create_app @@ -625,50 +627,63 @@ async def copilot_integration_app(): return create_app(service_container), settings -@pytest_asyncio.fixture(loop_scope="function") -async def copilot_integration_client(copilot_integration_app): +@pytest_asyncio.fixture(scope="module", loop_scope="module") +async def copilot_integration_client( + copilot_integration_app: Any, +) -> AsyncGenerator[AsyncClient, None]: """HTTP client for Copilot integration tests - uses shared app.""" from ccproxy.api.app import initialize_plugins_startup, shutdown_plugins app, settings = copilot_integration_app - detection_patch = patch( - "ccproxy.plugins.copilot.detection_service.CopilotDetectionService.initialize_detection", - new=AsyncMock( - return_value=CopilotCacheData( - cli_available=False, - cli_version=None, - auth_status=None, - username=None, - ) - ), - ) - ensure_copilot_patch = patch( - "ccproxy.plugins.copilot.manager.CopilotTokenManager.ensure_copilot_token", - new=AsyncMock(return_value="copilot_test_service_token"), - ) - ensure_oauth_patch = patch( - "ccproxy.plugins.copilot.oauth.provider.CopilotOAuthProvider.ensure_oauth_token", - new=AsyncMock(return_value="gh_oauth_access_token"), - ) - profile_patch = patch( - "ccproxy.plugins.copilot.manager.CopilotTokenManager.get_profile_quick", - new=AsyncMock(return_value=None), - ) - service_container = app.state.service_container await start_task_manager(container=service_container) - with detection_patch, ensure_copilot_patch, ensure_oauth_patch, profile_patch: - # Initialize plugins async (once per test, but app is shared) - await initialize_plugins_startup(app, settings) + try: + async with AsyncExitStack() as stack: + stack.enter_context( + patch( + "ccproxy.plugins.copilot.detection_service.CopilotDetectionService.initialize_detection", + new=AsyncMock( + return_value=CopilotCacheData( + cli_available=False, + cli_version=None, + auth_status=None, + username=None, + ) + ), + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.copilot.manager.CopilotTokenManager.ensure_copilot_token", + new=AsyncMock(return_value="copilot_test_service_token"), + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.copilot.oauth.provider.CopilotOAuthProvider.ensure_oauth_token", + new=AsyncMock(return_value="gh_oauth_access_token"), + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.copilot.manager.CopilotTokenManager.get_profile_quick", + new=AsyncMock(return_value=None), + ) + ) - transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client + await initialize_plugins_startup(app, settings) + + transport = ASGITransport(app=app) + client = await stack.enter_async_context( + AsyncClient(transport=transport, base_url="http://test") + ) - await shutdown_plugins(app) - await stop_task_manager(container=service_container) - if hasattr(app.state, "service_container"): - await app.state.service_container.shutdown() + yield client + finally: + await shutdown_plugins(app) + await stop_task_manager(container=service_container) + if hasattr(app.state, "service_container"): + await app.state.service_container.shutdown() diff --git a/tests/plugins/copilot/integration/test_endpoint_runner.py b/tests/plugins/copilot/integration/test_endpoint_runner.py index 6e209184..b0839ffd 100644 --- a/tests/plugins/copilot/integration/test_endpoint_runner.py +++ b/tests/plugins/copilot/integration/test_endpoint_runner.py @@ -2,9 +2,11 @@ from __future__ import annotations +from collections.abc import AsyncGenerator from typing import Any import pytest +import pytest_asyncio from httpx import ASGITransport, AsyncClient from ccproxy.testing.endpoints import TestEndpoint @@ -22,7 +24,7 @@ pytestmark = [ pytest.mark.integration, pytest.mark.e2e, - pytest.mark.asyncio(loop_scope="function"), + pytest.mark.asyncio(loop_scope="module"), ] @@ -30,29 +32,37 @@ SAMPLES = provider_sample_names(PROVIDER) +@pytest_asyncio.fixture(scope="module", loop_scope="module") +async def copilot_endpoint_tester() -> AsyncGenerator[TestEndpoint, None]: + """Initialize a shared app/client pair for Copilot endpoint tests.""" + + app_builder = PROVIDER_APP_BUILDERS[PROVIDER] + async with app_builder() as app: + transport = ASGITransport(app=app) + client = AsyncClient(transport=transport, base_url=BASE_URL) + tester = TestEndpoint(base_url=BASE_URL, client=client) + try: + yield tester + finally: + await client.aclose() + + @pytest.mark.parametrize("sample_name", SAMPLES, ids=SAMPLES) async def test_copilot_endpoint_sample( sample_name: str, request: pytest.FixtureRequest, httpx_mock: Any, + copilot_endpoint_tester: TestEndpoint, ) -> None: fixture_name = PROVIDER_FIXTURES.get(PROVIDER) if fixture_name: request.getfixturevalue(fixture_name) - app_builder = PROVIDER_APP_BUILDERS[PROVIDER] - - async with app_builder() as app: - transport = ASGITransport(app=app) - async with ( - AsyncClient(transport=transport, base_url=BASE_URL) as client, - TestEndpoint(base_url=BASE_URL, client=client) as tester, - ): - endpoint_case = AVAILABLE_CASES[sample_name] - result = await tester.run_endpoint_test( - endpoint_case, - CASE_INDEX_LOOKUP[endpoint_case.name], - ) + endpoint_case = AVAILABLE_CASES[sample_name] + result = await copilot_endpoint_tester.run_endpoint_test( + endpoint_case, + CASE_INDEX_LOOKUP[endpoint_case.name], + ) assert_initial_request(result, endpoint_case.stream, endpoint_case.request) assert_follow_up_requests(result) diff --git a/tests/plugins/copilot/unit/oauth/test_client.py b/tests/plugins/copilot/unit/oauth/test_client.py index 140be527..7b900c27 100644 --- a/tests/plugins/copilot/unit/oauth/test_client.py +++ b/tests/plugins/copilot/unit/oauth/test_client.py @@ -219,7 +219,10 @@ async def test_poll_for_token_success( with ( patch.object(client, "_get_http_client", return_value=mock_client), - patch("asyncio.sleep", new_callable=AsyncMock), + patch( + "ccproxy.plugins.copilot.oauth.client.runtime_sleep", + new_callable=AsyncMock, + ), ): result = await client.poll_for_token("device-code", 1, 60) @@ -259,7 +262,10 @@ async def test_poll_for_token_pending( with ( patch.object(client, "_get_http_client", return_value=mock_client), - patch("asyncio.sleep", new_callable=AsyncMock), + patch( + "ccproxy.plugins.copilot.oauth.client.runtime_sleep", + new_callable=AsyncMock, + ), ): result = await client.poll_for_token( "device-code", 1, 60 @@ -290,11 +296,15 @@ async def test_poll_for_token_expired( with ( patch.object(client, "_get_http_client", return_value=mock_client), + patch( + "ccproxy.plugins.copilot.oauth.client.runtime_sleep", + new_callable=AsyncMock, + ), pytest.raises(TimeoutError, match="Device code has expired"), ): await client.poll_for_token( - "device-code", 1, 60 - ) # Much faster interval for tests + "device-code", 1, 1 + ) # Minimal interval and timeout for fast tests async def test_poll_for_token_denied( self, @@ -318,11 +328,15 @@ async def test_poll_for_token_denied( with ( patch.object(client, "_get_http_client", return_value=mock_client), + patch( + "ccproxy.plugins.copilot.oauth.client.runtime_sleep", + new_callable=AsyncMock, + ), pytest.raises(ValueError, match="User denied authorization"), ): await client.poll_for_token( - "device-code", 1, 60 - ) # Much faster interval for tests + "device-code", 1, 1 + ) # Minimal interval and timeout for fast tests async def test_exchange_for_copilot_token_success( self, diff --git a/tests/plugins/copilot/unit/test_auth_missing.py b/tests/plugins/copilot/unit/test_auth_missing.py index 53ddbe23..770b13bb 100644 --- a/tests/plugins/copilot/unit/test_auth_missing.py +++ b/tests/plugins/copilot/unit/test_auth_missing.py @@ -1,9 +1,12 @@ +from contextlib import AsyncExitStack from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import httpx import pytest +from ccproxy.plugins.copilot.models import CopilotCacheData + @pytest.mark.unit @pytest.mark.asyncio @@ -17,8 +20,6 @@ async def test_copilot_missing_auth_manager_returns_401( } } - client = await integration_client_factory(plugin_configs) - blocked_hosts = {"api.githubcopilot.com", "api.github.com"} original_send = httpx.AsyncClient.send @@ -29,7 +30,40 @@ async def guard_send( raise AssertionError(f"Unexpected upstream call to {request.url!s}") return await original_send(self, request, *args, **kwargs) - async with client as http: + detection_data = CopilotCacheData( + cli_available=False, + cli_version=None, + auth_status="not_authenticated", + username=None, + ) + + async def init_detection_stub(self): # type: ignore[no-untyped-def] + self._cache = detection_data + return detection_data + + async with AsyncExitStack() as stack: + stack.enter_context( + patch( + "ccproxy.plugins.copilot.detection_service.CopilotDetectionService.initialize_detection", + new=init_detection_stub, + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.copilot.manager.CopilotTokenManager.ensure_copilot_token", + new=AsyncMock(return_value="copilot_test_service_token"), + ) + ) + stack.enter_context( + patch( + "ccproxy.plugins.copilot.oauth.provider.CopilotOAuthProvider.ensure_oauth_token", + new=AsyncMock(return_value="gh_oauth_access_token"), + ) + ) + + client = await integration_client_factory(plugin_configs) + http = await stack.enter_async_context(client) + with patch("httpx.AsyncClient.send", guard_send): resp = await http.post( "/copilot/v1/chat/completions", diff --git a/tests/plugins/metrics/integration/test_metrics_basic.py b/tests/plugins/metrics/integration/test_metrics_basic.py index ce953f7b..3aa02396 100644 --- a/tests/plugins/metrics/integration/test_metrics_basic.py +++ b/tests/plugins/metrics/integration/test_metrics_basic.py @@ -93,7 +93,7 @@ async def test_metrics_endpoint_returns_prometheus_format( response = await metrics_integration_client.get("/metrics") assert response.status_code == 200 assert ( - response.headers["content-type"] == "text/plain; version=0.0.4; charset=utf-8" + response.headers["content-type"] == "text/plain; version=1.0.0; charset=utf-8" ) assert response.content.strip() diff --git a/tests/plugins/permissions/integration/test_permission_service_integration.py b/tests/plugins/permissions/integration/test_permission_service_integration.py index 677c8de1..8620984f 100644 --- a/tests/plugins/permissions/integration/test_permission_service_integration.py +++ b/tests/plugins/permissions/integration/test_permission_service_integration.py @@ -297,11 +297,12 @@ async def test_permission_service_wait_for_permission_timeout( try: request_id = await service.request_permission("bash", {"command": "test"}) - # Don't resolve - let it timeout + # Don't resolve - let it timeout with minimal wait with pytest.raises(asyncio.TimeoutError): await service.wait_for_permission( - request_id, timeout_seconds=1 - ) # Use int instead of float + request_id, + timeout_seconds=0, + ) # Minimal timeout keeps test fast finally: await service.stop() diff --git a/tests/plugins/permissions/integration/test_permissions_sse_stress.py b/tests/plugins/permissions/integration/test_permissions_sse_stress.py new file mode 100644 index 00000000..1bf6c964 --- /dev/null +++ b/tests/plugins/permissions/integration/test_permissions_sse_stress.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any, cast + +import anyio +import httpx +import pytest + +from ccproxy.core.async_task_manager import AsyncTaskManager +from ccproxy.plugins.permissions.handlers.cli import SSEConfirmationHandler +from ccproxy.plugins.permissions.models import PermissionRequest + + +class _DummyTerminalHandler: + def __init__(self) -> None: + self.handled: list[str] = [] + self.cancelled: list[tuple[str, str]] = [] + + async def handle_permission(self, request: PermissionRequest) -> bool: + self.handled.append(request.id) + await anyio.sleep(0) + return True + + def cancel_confirmation(self, request_id: str, reason: str = "cancelled") -> None: + self.cancelled.append((request_id, reason)) + + +class _DummyHttpClient: + async def post(self, url: str, json: Any) -> httpx.Response: # noqa: ANN401 + await anyio.sleep(0) + return httpx.Response(status_code=200) + + async def aclose(self) -> None: + await anyio.sleep(0) + + +@pytest.mark.integration +@pytest.mark.slow +@pytest.mark.asyncio +async def test_sse_confirmation_handler_stress( + monkeypatch: pytest.MonkeyPatch, +) -> None: + manager = AsyncTaskManager(max_tasks=512, shutdown_timeout=5.0) + await manager.start() + + async def create_task_stub(coro, *args, **kwargs): # noqa: ANN001 + return await manager.create_task(coro, *args, **kwargs) + + monkeypatch.setattr( + "ccproxy.plugins.permissions.handlers.cli.create_managed_task", + create_task_stub, + ) + + async def fast_sleep(delay: float) -> None: + await anyio.sleep(0) + + monkeypatch.setattr( + "ccproxy.plugins.permissions.handlers.cli.runtime_sleep", + fast_sleep, + ) + + handler = SSEConfirmationHandler( + api_url="https://example.test", + terminal_handler=_DummyTerminalHandler(), + ui=True, + auto_reconnect=False, + ) + handler.client = cast(httpx.AsyncClient, _DummyHttpClient()) + + total_requests = 80 + request_ids = [f"perm-{idx}" for idx in range(total_requests)] + + async def emit_request(request_id: str) -> None: + expires_at = (datetime.now(UTC) + timedelta(minutes=5)).isoformat() + data = { + "request_id": request_id, + "tool_name": "stress-tool", + "input": {"command": "echo"}, + "expires_at": expires_at, + } + await handler._handle_permission_request(data) + + async with anyio.create_task_group() as tg: + for request_id in request_ids: + tg.start_soon(emit_request, request_id) + + await anyio.sleep(0.05) + + async def resolve_request(request_id: str) -> None: + await handler._handle_permission_resolved( + {"request_id": request_id, "allowed": True} + ) + + async with anyio.create_task_group() as tg: + for request_id in request_ids: + tg.start_soon(resolve_request, request_id) + + await anyio.sleep(0.05) + + terminal_handler = cast(_DummyTerminalHandler, handler.terminal_handler) + assert len(terminal_handler.handled) == total_requests + assert handler._ongoing_requests == {} + + await manager.stop() diff --git a/tests/unit/core/test_async_task_manager.py b/tests/unit/core/test_async_task_manager.py index 2eac2c45..5104af3b 100644 --- a/tests/unit/core/test_async_task_manager.py +++ b/tests/unit/core/test_async_task_manager.py @@ -4,6 +4,7 @@ from collections.abc import AsyncGenerator from unittest.mock import Mock, patch +import anyio import pytest from ccproxy.api.bootstrap import create_service_container @@ -131,11 +132,12 @@ async def test_start_and_stop(self, manager): await manager.start() assert manager.is_started - assert manager._cleanup_task is not None + assert manager._tasks == {} + assert manager._active_tasks == 0 await manager.stop() assert not manager.is_started - assert manager._cleanup_task is None or manager._cleanup_task.done() + assert manager._tasks == {} async def test_double_start(self, manager): """Test that starting twice doesn't cause issues.""" @@ -532,3 +534,43 @@ async def create_task_wrapper(i): await asyncio.gather(*created_tasks) await manager.stop() + + @pytest.mark.slow + @pytest.mark.asyncio + async def test_manager_with_cancellation_stress(self) -> None: + """Exercise repeated start/stop cycles with mixed task outcomes.""" + + manager = AsyncTaskManager( + cleanup_interval=0.05, + shutdown_timeout=5.0, + max_tasks=512, + ) + + async def worker(delay: float, should_fail: bool) -> None: + try: + await anyio.sleep(delay) + if should_fail: + raise RuntimeError("intentional failure") + except asyncio.CancelledError: + raise + + for cycle in range(3): + await manager.start() + created_tasks = [] + for index in range(160): + delay = (index % 5) * 0.002 + should_fail = index % 13 == 0 + task = await manager.create_task( + worker(delay, should_fail), + name=f"stress_task_{cycle}_{index}", + creator="stress", + ) + created_tasks.append(task) + + for idx, task in enumerate(created_tasks): + if idx % 7 == 0: + task.cancel() + + await anyio.sleep(0.05) + await manager.stop() + assert manager._tasks == {} diff --git a/tests/unit/core/test_background_hook_thread_manager.py b/tests/unit/core/test_background_hook_thread_manager.py index ee95fcb7..0cc44dcc 100644 --- a/tests/unit/core/test_background_hook_thread_manager.py +++ b/tests/unit/core/test_background_hook_thread_manager.py @@ -2,6 +2,7 @@ from datetime import UTC, datetime from typing import Any +import anyio import pytest from ccproxy.core.plugins.hooks.base import HookContext @@ -31,17 +32,13 @@ async def hook_fn(ctx: HookContext) -> None: # noqa: ARG001 registry = _Registry([hook_fn]) manager = BackgroundHookThreadManager() - manager.start() - # Immediately emit without any sleep to simulate race window - # Use an existing HookEvent value to avoid creating a new Enum ctx = HookContext( event=HookEvent.CUSTOM_EVENT, timestamp=datetime.now(UTC), data={}, metadata={} ) - manager.emit_async(ctx, registry) + await manager.emit_async(ctx, registry) - # allow background thread to process await asyncio.sleep(0.05) - manager.stop() + await manager.stop() assert sum(executed) == 1 logs = "\n".join( @@ -65,14 +62,13 @@ async def hook_fn(ctx: HookContext) -> None: # noqa: ARG001 registry = _Registry([hook_fn]) manager = BackgroundHookThreadManager() - # Do not call start() explicitly; emit triggers lazy start ctx = HookContext( event=HookEvent.CUSTOM_EVENT, timestamp=datetime.now(UTC), data={}, metadata={} ) - manager.emit_async(ctx, registry) + await manager.emit_async(ctx, registry) await asyncio.sleep(0.05) - manager.stop() + await manager.stop() assert sum(executed) == 1 logs = "\n".join( @@ -80,3 +76,42 @@ async def hook_fn(ctx: HookContext) -> None: # noqa: ARG001 ) assert "background_thread_not_ready_dropping_task" not in logs assert "is bound to a different event loop" not in logs + + +@pytest.mark.unit +@pytest.mark.slow +@pytest.mark.asyncio +async def test_background_hook_manager_stress_high_concurrency() -> None: + total_events = 250 + processed = 0 + processed_lock = anyio.Lock() + + async def hook_fn(ctx: HookContext) -> None: # noqa: ARG001 + nonlocal processed + async with processed_lock: + processed += 1 + await anyio.sleep(0) + + registry = _Registry([hook_fn]) + manager = BackgroundHookThreadManager() + + async with anyio.create_task_group() as tg: + for _ in range(total_events): + ctx = HookContext( + event=HookEvent.CUSTOM_EVENT, + timestamp=datetime.now(UTC), + data={}, + metadata={}, + ) + tg.start_soon(manager.emit_async, ctx, registry) + + with anyio.fail_after(3.0): + while True: + async with processed_lock: + if processed >= total_events: + break + await anyio.sleep(0.01) + + await manager.stop() + + assert processed == total_events diff --git a/tests/unit/services/mocking/test_mock_handler.py b/tests/unit/services/mocking/test_mock_handler.py index a4e13005..2e7ba52f 100644 --- a/tests/unit/services/mocking/test_mock_handler.py +++ b/tests/unit/services/mocking/test_mock_handler.py @@ -49,7 +49,9 @@ async def test_generate_standard_response_success( async def fast_sleep(_: float) -> None: return None - monkeypatch.setattr(asyncio, "sleep", fast_sleep) + monkeypatch.setattr( + "ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep + ) mock_logger = MagicMock() ctx = RequestContext(request_id="req", start_time=0, logger=mock_logger) # type: ignore[arg-type] @@ -75,7 +77,9 @@ async def test_generate_standard_response_error( async def fast_sleep(_: float) -> None: return None - monkeypatch.setattr(asyncio, "sleep", fast_sleep) + monkeypatch.setattr( + "ccproxy.services.mocking.mock_handler.runtime_sleep", fast_sleep + ) mock_logger = MagicMock() mock_ctx = RequestContext(request_id="req", start_time=0, logger=mock_logger) # type: ignore[arg-type] diff --git a/tests/unit/services/test_scheduler_tasks.py b/tests/unit/services/test_scheduler_tasks.py index 62559618..4cca7e40 100644 --- a/tests/unit/services/test_scheduler_tasks.py +++ b/tests/unit/services/test_scheduler_tasks.py @@ -477,9 +477,11 @@ async def test_version_check_task_startup_skip_recent_check(self) -> None: new_callable=AsyncMock, ) as mock_fetch, patch("ccproxy.scheduler.tasks.get_version_check_state_path") as mock_path, + patch("ccproxy.scheduler.tasks.get_current_version") as mock_current, ): mock_load.return_value = recent_state mock_path.return_value = "/tmp/version_check.json" + mock_current.return_value = "1.0.0" task = VersionUpdateCheckTask( name="version_skip_test", diff --git a/tests/unit/testing/test_scenarios.py b/tests/unit/testing/test_scenarios.py index 37a336e0..b141aad3 100644 --- a/tests/unit/testing/test_scenarios.py +++ b/tests/unit/testing/test_scenarios.py @@ -1,6 +1,6 @@ """Tests for scenario generation utilities.""" -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta import pytest @@ -88,7 +88,7 @@ def test_analyze_distribution(): message_type="tool", streaming=False, response_type="success", - timestamp=now.replace(second=now.second + 1), + timestamp=now + timedelta(seconds=1), api_format="anthropic", headers={}, ), diff --git a/uv.lock b/uv.lock index 34f6a5ca..5bb568dc 100644 --- a/uv.lock +++ b/uv.lock @@ -31,16 +31,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] [[package]] @@ -126,7 +126,6 @@ name = "ccproxy-api" source = { editable = "." } dependencies = [ { name = "aiofiles" }, - { name = "dummy" }, { name = "fastapi", extra = ["standard"] }, { name = "httpx", extra = ["http2"] }, { name = "packaging" }, @@ -221,7 +220,6 @@ plugins-tui = [ requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, { name = "bandit", marker = "extra == 'dev'" }, - { name = "dummy", directory = "../../.config/ccproxy/plugins/dummy.ccproxy" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.14" }, { name = "httpx", extras = ["http2"], specifier = ">=0.28.1" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.5.3" }, @@ -267,8 +265,8 @@ provides-extras = ["dev", "docs", "test"] [package.metadata.requires-dev] all = [ { name = "aioconsole", specifier = ">=0.8.1" }, - { name = "claude-agent-sdk", git = "https://github.com/anthropics/claude-agent-sdk-python.git" }, - { name = "duckdb", specifier = ">=1.1.0" }, + { name = "claude-agent-sdk", specifier = ">=0.1.0" }, + { name = "duckdb", specifier = ">=1.1.0,<1.4.0" }, { name = "duckdb-engine", specifier = ">=0.17.0" }, { name = "fastapi-mcp", specifier = ">=0.3.7" }, { name = "prometheus-client", specifier = ">=0.22.1" }, @@ -280,7 +278,7 @@ all = [ { name = "textual", specifier = ">=3.7.1" }, ] plugins-claude = [ - { name = "claude-agent-sdk", git = "https://github.com/anthropics/claude-agent-sdk-python.git" }, + { name = "claude-agent-sdk", specifier = ">=0.1.0" }, { name = "qrcode", specifier = ">=8.2" }, ] plugins-codex = [ @@ -290,7 +288,7 @@ plugins-codex = [ plugins-mcp = [{ name = "fastapi-mcp", specifier = ">=0.3.7" }] plugins-metrics = [{ name = "prometheus-client", specifier = ">=0.22.1" }] plugins-storage = [ - { name = "duckdb", specifier = ">=1.1.0" }, + { name = "duckdb", specifier = ">=1.1.0,<1.4.0" }, { name = "duckdb-engine", specifier = ">=0.17.0" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "sqlmodel", specifier = ">=0.0.24" }, @@ -383,22 +381,26 @@ wheels = [ [[package]] name = "claude-agent-sdk" version = "0.1.0" -source = { git = "https://github.com/anthropics/claude-agent-sdk-python.git#2a9693e2585228e956ab240c1fad557d4eb510d4" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "mcp" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/dd/21/9466f7c38cf02f0a4e5ddeeba7450ef4cddbf21945d2497eac4637f463dc/claude_agent_sdk-0.1.0.tar.gz", hash = "sha256:f60d148f8e9f69991582863a6cdbf337ff908c1bd7d5de2f7845eb830f72d2a5", size = 42827, upload-time = "2025-09-28T23:39:33.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/27/0081f232b1119d3973ce5301b31f407bb604ca4434152d39ff62b9e18a0c/claude_agent_sdk-0.1.0-py3-none-any.whl", hash = "sha256:92d9a83689a6a3a54f69aac1224dd2004109bf1fecd0272e2241251747afdb3d", size = 32635, upload-time = "2025-09-28T23:39:32.184Z" }, +] [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, ] [[package]] @@ -412,77 +414,89 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/ba/2c9817e62018e7d480d14f684c160b3038df9ff69c5af7d80e97d143e4d1/coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f", size = 216514, upload-time = "2025-08-17T00:24:34.188Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/093412a959a6b6261446221ba9fb23bb63f661a5de70b5d130763c87f916/coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88", size = 216914, upload-time = "2025-08-17T00:24:35.881Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1f/2fdf4a71cfe93b07eae845ebf763267539a7d8b7e16b062f959d56d7e433/coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb", size = 247308, upload-time = "2025-08-17T00:24:37.61Z" }, - { url = "https://files.pythonhosted.org/packages/ba/16/33f6cded458e84f008b9f6bc379609a6a1eda7bffe349153b9960803fc11/coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9", size = 249241, upload-time = "2025-08-17T00:24:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/84/98/9c18e47c889be58339ff2157c63b91a219272503ee32b49d926eea2337f2/coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8", size = 251346, upload-time = "2025-08-17T00:24:40.507Z" }, - { url = "https://files.pythonhosted.org/packages/6d/07/00a6c0d53e9a22d36d8e95ddd049b860eef8f4b9fd299f7ce34d8e323356/coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2", size = 249037, upload-time = "2025-08-17T00:24:41.904Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0e/1e1b944d6a6483d07bab5ef6ce063fcf3d0cc555a16a8c05ebaab11f5607/coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7", size = 247090, upload-time = "2025-08-17T00:24:43.193Z" }, - { url = "https://files.pythonhosted.org/packages/62/43/2ce5ab8a728b8e25ced077111581290ffaef9efaf860a28e25435ab925cf/coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0", size = 247732, upload-time = "2025-08-17T00:24:44.906Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f3/706c4a24f42c1c5f3a2ca56637ab1270f84d9e75355160dc34d5e39bb5b7/coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af", size = 218961, upload-time = "2025-08-17T00:24:46.241Z" }, - { url = "https://files.pythonhosted.org/packages/e8/aa/6b9ea06e0290bf1cf2a2765bba89d561c5c563b4e9db8298bf83699c8b67/coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52", size = 219851, upload-time = "2025-08-17T00:24:48.795Z" }, - { url = "https://files.pythonhosted.org/packages/8b/be/f0dc9ad50ee183369e643cd7ed8f2ef5c491bc20b4c3387cbed97dd6e0d1/coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0", size = 218530, upload-time = "2025-08-17T00:24:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload-time = "2025-08-17T00:24:51.528Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload-time = "2025-08-17T00:24:53.171Z" }, - { url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload-time = "2025-08-17T00:24:54.934Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload-time = "2025-08-17T00:24:56.353Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload-time = "2025-08-17T00:24:57.692Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload-time = "2025-08-17T00:24:59.293Z" }, - { url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload-time = "2025-08-17T00:25:00.676Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload-time = "2025-08-17T00:25:02.354Z" }, - { url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload-time = "2025-08-17T00:25:03.79Z" }, - { url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload-time = "2025-08-17T00:25:05.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload-time = "2025-08-17T00:25:06.897Z" }, - { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" }, - { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload-time = "2025-08-17T00:25:13.483Z" }, - { url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload-time = "2025-08-17T00:25:15.27Z" }, - { url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload-time = "2025-08-17T00:25:16.759Z" }, - { url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload-time = "2025-08-17T00:25:18.232Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload-time = "2025-08-17T00:25:20.141Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload-time = "2025-08-17T00:25:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload-time = "2025-08-17T00:25:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload-time = "2025-08-17T00:25:25.295Z" }, - { url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload-time = "2025-08-17T00:25:27.129Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload-time = "2025-08-17T00:25:28.641Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload-time = "2025-08-17T00:25:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload-time = "2025-08-17T00:25:31.517Z" }, - { url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload-time = "2025-08-17T00:25:33.339Z" }, - { url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload-time = "2025-08-17T00:25:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload-time = "2025-08-17T00:25:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload-time = "2025-08-17T00:25:37.754Z" }, - { url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload-time = "2025-08-17T00:25:39.252Z" }, - { url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload-time = "2025-08-17T00:25:40.772Z" }, - { url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload-time = "2025-08-17T00:25:42.229Z" }, - { url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload-time = "2025-08-17T00:25:43.875Z" }, - { url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload-time = "2025-08-17T00:25:45.405Z" }, - { url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload-time = "2025-08-17T00:25:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload-time = "2025-08-17T00:25:48.613Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload-time = "2025-08-17T00:25:50.881Z" }, - { url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload-time = "2025-08-17T00:25:52.422Z" }, - { url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload-time = "2025-08-17T00:25:54.316Z" }, - { url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload-time = "2025-08-17T00:25:55.833Z" }, - { url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload-time = "2025-08-17T00:25:57.984Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload-time = "2025-08-17T00:25:59.868Z" }, - { url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload-time = "2025-08-17T00:26:01.44Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload-time = "2025-08-17T00:26:03.014Z" }, - { url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload-time = "2025-08-17T00:26:04.606Z" }, - { url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload-time = "2025-08-17T00:26:06.159Z" }, - { url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload-time = "2025-08-17T00:26:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload-time = "2025-08-17T00:26:11.045Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload-time = "2025-08-17T00:26:12.693Z" }, - { url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload-time = "2025-08-17T00:26:14.261Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload-time = "2025-08-17T00:26:15.899Z" }, - { url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload-time = "2025-08-17T00:26:17.566Z" }, - { url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload-time = "2025-08-17T00:26:19.311Z" }, - { url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload-time = "2025-08-17T00:26:21.011Z" }, - { url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload-time = "2025-08-17T00:26:41.479Z" }, +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] [package.optional-dependencies] @@ -507,11 +521,11 @@ wheels = [ [[package]] name = "dnspython" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] @@ -557,23 +571,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/a2/e90242f53f7ae41554419b1695b4820b364df87c8350aa420b60b20cab92/duckdb_engine-0.17.0-py3-none-any.whl", hash = "sha256:3aa72085e536b43faab635f487baf77ddc5750069c16a2f8d9c6c3cb6083e979", size = 49676, upload-time = "2025-03-29T09:49:15.564Z" }, ] -[[package]] -name = "dummy" -version = "0.1.0" -source = { directory = "../../.config/ccproxy/plugins/dummy.ccproxy" } -dependencies = [ - { name = "httpx" }, - { name = "structlog" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiosqlite", marker = "extra == 'ccproxy-runtime'", specifier = ">=0.20" }, - { name = "httpx", specifier = ">=0.27" }, - { name = "structlog", specifier = ">=24.1" }, -] -provides-extras = ["ccproxy-runtime"] - [[package]] name = "editorconfig" version = "0.17.1" @@ -585,15 +582,15 @@ wheels = [ [[package]] name = "email-validator" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] @@ -607,16 +604,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.116.1" +version = "0.118.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/3c/2b9345a6504e4055eaa490e0b41c10e338ad61d9aeaae41d97807873cdf2/fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8", size = 310536, upload-time = "2025-09-29T03:37:23.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/54e2bdaad22ca91a59455251998d43094d5c3d3567c52c7c04774b3f43f2/fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855", size = 97694, upload-time = "2025-09-29T03:37:21.338Z" }, ] [package.optional-dependencies] @@ -631,16 +628,16 @@ standard = [ [[package]] name = "fastapi-cli" -version = "0.0.8" +version = "0.0.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/94/3ef75d9c7c32936ecb539b9750ccbdc3d2568efd73b1cb913278375f4533/fastapi_cli-0.0.8.tar.gz", hash = "sha256:2360f2989b1ab4a3d7fc8b3a0b20e8288680d8af2e31de7c38309934d7f8a0ee", size = 16884, upload-time = "2025-07-07T14:44:09.326Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/4e/3f61850012473b097fc5297d681bd85788e186fadb8555b67baf4c7707f4/fastapi_cli-0.0.13.tar.gz", hash = "sha256:312addf3f57ba7139457cf0d345c03e2170cc5a034057488259c33cd7e494529", size = 17780, upload-time = "2025-09-20T16:37:31.089Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/3f/6ad3103c5f59208baf4c798526daea6a74085bb35d1c161c501863470476/fastapi_cli-0.0.8-py3-none-any.whl", hash = "sha256:0ea95d882c85b9219a75a65ab27e8da17dac02873e456850fa0a726e96e985eb", size = 10770, upload-time = "2025-07-07T14:44:08.255Z" }, + { url = "https://files.pythonhosted.org/packages/08/36/7432750f3638324b055496d2c952000bea824259fca70df5577a6a3c172f/fastapi_cli-0.0.13-py3-none-any.whl", hash = "sha256:219b73ccfde7622559cef1d43197da928516acb4f21f2ec69128c4b90057baba", size = 11142, upload-time = "2025-09-20T16:37:29.695Z" }, ] [package.optional-dependencies] @@ -651,7 +648,7 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.1.5" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -662,14 +659,14 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/2e/3b6e5016affc310e5109bc580f760586eabecea0c8a7ab067611cd849ac0/fastapi_cloud_cli-0.1.5.tar.gz", hash = "sha256:341ee585eb731a6d3c3656cb91ad38e5f39809bf1a16d41de1333e38635a7937", size = 22710, upload-time = "2025-07-28T13:30:48.216Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/5f/17b403148a23dd708e3166f534136f4d3918942e168aca66659311eb0678/fastapi_cloud_cli-0.3.0.tar.gz", hash = "sha256:17c7f8baa16b2f907696bf77d49df4a04e8715bbf5233024f273870f3ff1ca4d", size = 24388, upload-time = "2025-10-02T13:25:52.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/a6/5aa862489a2918a096166fd98d9fe86b7fd53c607678b3fa9d8c432d88d5/fastapi_cloud_cli-0.1.5-py3-none-any.whl", hash = "sha256:d80525fb9c0e8af122370891f9fa83cf5d496e4ad47a8dd26c0496a6c85a012a", size = 18992, upload-time = "2025-07-28T13:30:47.427Z" }, + { url = "https://files.pythonhosted.org/packages/58/59/7d12c5173fe2eed21e99bb1a6eb7e4f301951db870a4d915d126e0b6062d/fastapi_cloud_cli-0.3.0-py3-none-any.whl", hash = "sha256:572677dbe38b6d4712d30097a8807b383d648ca09eb58e4a07cef4a517020832", size = 19921, upload-time = "2025-10-02T13:25:51.164Z" }, ] [[package]] name = "fastapi-mcp" -version = "0.3.7" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -683,9 +680,9 @@ dependencies = [ { name = "typer" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/b6/dbad5a717d909562905a24fa78551b899df582276ff9b5f88c5494c9acf6/fastapi_mcp-0.3.7.tar.gz", hash = "sha256:35de3333355e4d0f44116a4fe70613afecd5e5428bb6ddbaa041b39b33781af8", size = 165767, upload-time = "2025-07-14T16:19:51.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/1e/e3ba42f2e240dc67baabc431c68a82e380bcdae4e8b7d1310a756b2033fc/fastapi_mcp-0.4.0.tar.gz", hash = "sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d", size = 184201, upload-time = "2025-07-28T12:11:05.652Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/4f/d622aa42273f79719a986caf585f956b6c70008a1d8ac45081274e3e5690/fastapi_mcp-0.3.7-py3-none-any.whl", hash = "sha256:1d4561959d4cd6df0ed8836d380b74fd9969fd9400cb6f7ed5cbd2db2f39090c", size = 23278, upload-time = "2025-07-14T16:19:49.994Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/6bf02ff9e3ca1d24765050e3b51dceae9bb69909cc5385623cf6f3fd7c23/fastapi_mcp-0.4.0-py3-none-any.whl", hash = "sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec", size = 25085, upload-time = "2025-07-28T12:11:04.472Z" }, ] [[package]] @@ -774,15 +771,15 @@ wheels = [ [[package]] name = "h2" -version = "4.2.0" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682, upload-time = "2025-02-02T07:43:51.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957, upload-time = "2025-02-01T11:02:26.481Z" }, + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, ] [[package]] @@ -884,11 +881,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.14" +version = "2.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] [[package]] @@ -942,7 +939,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f [[package]] name = "jsonschema" -version = "4.25.0" +version = "4.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -950,21 +947,21 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] [[package]] name = "jsonschema-specifications" -version = "2025.4.1" +version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] @@ -1010,55 +1007,81 @@ plugins = [ [[package]] name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +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/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 = "mcp" -version = "1.13.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1073,9 +1096,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/a8/564c094de5d6199f727f5d9f5672dbec3b00dfafd0f67bf52d995eaa5951/mcp-1.13.0.tar.gz", hash = "sha256:70452f56f74662a94eb72ac5feb93997b35995e389b3a3a574e078bed2aa9ab3", size = 434709, upload-time = "2025-08-14T15:03:58.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918, upload-time = "2025-10-02T16:58:20.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/6b/46b8bcefc2ee9e2d2e8d2bd25f1c2512f5a879fac4619d716b194d6e7ccc/mcp-1.13.0-py3-none-any.whl", hash = "sha256:8b1a002ebe6e17e894ec74d1943cc09aa9d23cb931bf58d49ab2e9fa6bb17e4b", size = 160226, upload-time = "2025-08-14T15:03:56.641Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266, upload-time = "2025-10-02T16:58:19.039Z" }, ] [[package]] @@ -1346,40 +1369,40 @@ wheels = [ [[package]] name = "mypy" -version = "1.17.1" +version = "1.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, ] [[package]] @@ -1429,11 +1452,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] @@ -1463,16 +1486,16 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.22.1" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746, upload-time = "2025-06-02T14:29:01.152Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, ] [[package]] name = "pydantic" -version = "2.11.7" +version = "2.11.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1480,9 +1503,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, ] [package.optional-dependencies] @@ -1557,16 +1580,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.10.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, ] [[package]] @@ -1614,7 +1637,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1623,35 +1646,36 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] [[package]] name = "pytest-cov" -version = "6.2.1" +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/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +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/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { 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]] @@ -1755,37 +1779,57 @@ wheels = [ [[package]] name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +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/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]] @@ -1828,7 +1872,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1836,9 +1880,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +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/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { 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]] @@ -1856,220 +1900,224 @@ wheels = [ [[package]] name = "rich-toolkit" -version = "0.15.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/36/cdb3d51371ad0cccbf1541506304783bd72d55790709b8eb68c0d401a13a/rich_toolkit-0.15.0.tar.gz", hash = "sha256:3f5730e9f2d36d0bfe01cf723948b7ecf4cc355d2b71e2c00e094f7963128c09", size = 115118, upload-time = "2025-08-11T10:55:37.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/33/1a18839aaa8feef7983590c05c22c9c09d245ada6017d118325bbfcc7651/rich_toolkit-0.15.1.tar.gz", hash = "sha256:6f9630eb29f3843d19d48c3bd5706a086d36d62016687f9d0efa027ddc2dd08a", size = 115322, upload-time = "2025-09-04T09:28:11.789Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/e4/b0794eefb3cf78566b15e5bf576492c1d4a92ce5f6da55675bc11e9ef5d8/rich_toolkit-0.15.0-py3-none-any.whl", hash = "sha256:ddb91008283d4a7989fd8ff0324a48773a7a2276229c6a3070755645538ef1bb", size = 29062, upload-time = "2025-08-11T10:55:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/c8/49/42821d55ead7b5a87c8d121edf323cb393d8579f63e933002ade900b784f/rich_toolkit-0.15.1-py3-none-any.whl", hash = "sha256:36a0b1d9a135d26776e4b78f1d5c2655da6e0ef432380b5c6b523c8d8ab97478", size = 29412, upload-time = "2025-09-04T09:28:10.587Z" }, ] [[package]] name = "rignore" -version = "0.6.4" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/46/05a94dc55ac03cf931d18e43b86ecee5ee054cb88b7853fffd741e35009c/rignore-0.6.4.tar.gz", hash = "sha256:e893fdd2d7fdcfa9407d0b7600ef2c2e2df97f55e1c45d4a8f54364829ddb0ab", size = 11633, upload-time = "2025-07-19T19:24:46.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/11/66992d271dbc44eac33f3b6b871855bc17e511b9279a2a0982b44c2b0c01/rignore-0.6.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:85f684dfc2c497e35ad34ffd6744a3bcdcac273ec1dbe7d0464bfa20f3331434", size = 888239, upload-time = "2025-07-19T19:23:51.835Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1b/a9bde714e474043f97a06097925cf11e4597f9453adc267427d05ff9f38e/rignore-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23954acc6debc852dbccbffbb70f0e26b12d230239e1ad0638eb5540694d0308", size = 824348, upload-time = "2025-07-19T19:23:45.54Z" }, - { url = "https://files.pythonhosted.org/packages/db/58/dabba227fee6553f9be069f58128419b6d4954c784c4cd566cfe59955c1f/rignore-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2bf793bd58dbf3dee063a758b23ea446b5f037370405ecefc78e1e8923fc658", size = 892419, upload-time = "2025-07-19T19:22:33.763Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fa/e3c16368ee32d6d1146cf219b127fd5c7e6baf22cad7a7a5967782ff3b20/rignore-0.6.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1eaeaa5a904e098604ea2012383a721de06211c8b4013abf0d41c3cfeb982f4f", size = 873285, upload-time = "2025-07-19T19:22:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/78/9d/ef43d760dc3d18011d8482692b478785a846bba64157844b3068e428739c/rignore-0.6.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a48bdbeb03093e3fac2b40d62a718c59b5bb4f29cfdc8e7cbb360e1ea7bf0056", size = 1160457, upload-time = "2025-07-19T19:22:59.457Z" }, - { url = "https://files.pythonhosted.org/packages/95/de/eca1b035705e0b4e6c630fd1fcec45d14cf354a4acea88cf29ea0a322fea/rignore-0.6.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c5f9452d116be405f0967160b449c46ac929b50eaf527f33ee4680e3716e39", size = 938833, upload-time = "2025-07-19T19:23:11.657Z" }, - { url = "https://files.pythonhosted.org/packages/d4/2d/58912efa4137e989616d679a5390b53e93d5150be47217dd686ff60cd4cd/rignore-0.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf1039bfbdaa0f9710a6fb75436c25ca26d364881ec4d1e66d466bb36a7fb98", size = 950603, upload-time = "2025-07-19T19:23:35.245Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3d/9827cc1c7674d8d884d3d231a224a2db8ea8eae075a1611dfdcd0c301e20/rignore-0.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:136629eb0ec2b6ac6ab34e71ce8065a07106fe615a53eceefc30200d528a4612", size = 976867, upload-time = "2025-07-19T19:23:24.919Z" }, - { url = "https://files.pythonhosted.org/packages/75/47/9dcee35e24897b62d66f7578f127bc91465c942a9d702d516d3fe7dcaa00/rignore-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35e3d0ebaf01086e6454c3fecae141e2db74a5ddf4a97c72c69428baeff0b7d4", size = 1067603, upload-time = "2025-07-19T19:23:58.765Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/f66e7c0b0fc009f3e19ba8e6c3078a227285e3aecd9f6498d39df808cdfd/rignore-0.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ed1f9010fa1ef5ea0b69803d1dfb4b7355921779e03a30396034c52691658bc", size = 1136289, upload-time = "2025-07-19T19:24:11.136Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b7/6fff161fe3ae5c0e0a0dded9a428e41d31c7fefc4e57c7553b9ffb064139/rignore-0.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c16e9e898ed0afe2e20fa8d6412e02bd13f039f7e0d964a289368efd4d9ad320", size = 1111566, upload-time = "2025-07-19T19:24:23.065Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c5/a5978ad65074a08dad46233a3333d154ae9cb9339325f3c181002a174746/rignore-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e6bc0bdcd404a7a8268629e8e99967127bb41e02d9eb09a471364c4bc25e215", size = 1121142, upload-time = "2025-07-19T19:24:35.151Z" }, - { url = "https://files.pythonhosted.org/packages/e8/af/91f084374b95dc2477a4bd066957beb3b61b551f2364b4f7f5bc52c9e4c7/rignore-0.6.4-cp311-cp311-win32.whl", hash = "sha256:fdd59bd63d2a49cc6d4f3598f285552ccb1a41e001df1012e0e0345cf2cabf79", size = 643031, upload-time = "2025-07-19T19:24:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/31672aa957aebba8903005313697127bbbad9db3afcfc9857150301fab1d/rignore-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:7bf5be0e8a01845e57b5faa47ef9c623bb2070aa2f743c2fc73321ffaae45701", size = 721003, upload-time = "2025-07-19T19:24:48.867Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6c/e5af4383cdd7829ef9aa63ac82a6507983e02dbc7c2e7b9aa64b7b8e2c7a/rignore-0.6.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:74720d074b79f32449d5d212ce732e0144a294a184246d1f1e7bcc1fc5c83b69", size = 885885, upload-time = "2025-07-19T19:23:53.236Z" }, - { url = "https://files.pythonhosted.org/packages/89/3e/1b02a868830e464769aa417ee195ac352fe71ff818df8ce50c4b998edb9c/rignore-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a8184fcf567bd6b6d7b85a0c138d98dd40f63054141c96b175844414c5530d7", size = 819736, upload-time = "2025-07-19T19:23:46.565Z" }, - { url = "https://files.pythonhosted.org/packages/e0/75/b9be0c523d97c09f3c6508a67ce376aba4efe41c333c58903a0d7366439a/rignore-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcb0d7d7ecc3fbccf6477bb187c04a091579ea139f15f139abe0b3b48bdfef69", size = 892779, upload-time = "2025-07-19T19:22:35.167Z" }, - { url = "https://files.pythonhosted.org/packages/91/f4/3064b06233697f2993485d132f06fe95061fef71631485da75aed246c4fd/rignore-0.6.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feac73377a156fb77b3df626c76f7e5893d9b4e9e886ac8c0f9d44f1206a2a91", size = 872116, upload-time = "2025-07-19T19:22:47.828Z" }, - { url = "https://files.pythonhosted.org/packages/99/94/cb8e7af9a3c0a665f10e2366144e0ebc66167cf846aca5f1ac31b3661598/rignore-0.6.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:465179bc30beb1f7a3439e428739a2b5777ed26660712b8c4e351b15a7c04483", size = 1163345, upload-time = "2025-07-19T19:23:00.557Z" }, - { url = "https://files.pythonhosted.org/packages/86/6b/49faa7ad85ceb6ccef265df40091d9992232d7f6055fa664fe0a8b13781c/rignore-0.6.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a4877b4dca9cf31a4d09845b300c677c86267657540d0b4d3e6d0ce3110e6e9", size = 939967, upload-time = "2025-07-19T19:23:13.494Z" }, - { url = "https://files.pythonhosted.org/packages/80/c8/b91afda10bd5ca1e3a80463340b899c0dc26a7750a9f3c94f668585c7f40/rignore-0.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456456802b1e77d1e2d149320ee32505b8183e309e228129950b807d204ddd17", size = 949717, upload-time = "2025-07-19T19:23:36.404Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f1/88bfdde58ae3fb1c1a92bb801f492eea8eafcdaf05ab9b75130023a4670b/rignore-0.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c1ff2fc223f1d9473d36923160af37bf765548578eb9d47a2f52e90da8ae408", size = 975534, upload-time = "2025-07-19T19:23:25.988Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/a80b4a2e48ceba56ba19e096d41263d844757e10aa36ede212571b5d8117/rignore-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e445fbc214ae18e0e644a78086ea5d0f579e210229a4fbe86367d11a4cd03c11", size = 1067837, upload-time = "2025-07-19T19:23:59.888Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/0905597af0e78748909ef58418442a480ddd93e9fc89b0ca9ab170c357c0/rignore-0.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e07d9c5270fc869bc431aadcfb6ed0447f89b8aafaa666914c077435dc76a123", size = 1134959, upload-time = "2025-07-19T19:24:12.396Z" }, - { url = "https://files.pythonhosted.org/packages/cc/7d/0fa29adf9183b61947ce6dc8a1a9779a8ea16573f557be28ec893f6ddbaa/rignore-0.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a6ccc0ea83d2c0c6df6b166f2acacedcc220a516436490f41e99a5ae73b6019", size = 1109708, upload-time = "2025-07-19T19:24:24.176Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a7/92892ed86b2e36da403dd3a0187829f2d880414cef75bd612bfdf4dedebc/rignore-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:536392c5ec91755db48389546c833c4ab1426fe03e5a8522992b54ef8a244e7e", size = 1120546, upload-time = "2025-07-19T19:24:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/31/1b/d29ae1fe901d523741d6d1d3ffe0d630734dd0ed6b047628a69c1e15ea44/rignore-0.6.4-cp312-cp312-win32.whl", hash = "sha256:f5f9dca46fc41c0a1e236767f68be9d63bdd2726db13a0ae3a30f68414472969", size = 642005, upload-time = "2025-07-19T19:24:56.671Z" }, - { url = "https://files.pythonhosted.org/packages/1a/41/a224944824688995374e4525115ce85fecd82442fc85edd5bcd81f4f256d/rignore-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:e02eecb9e1b9f9bf7c9030ae73308a777bed3b2486204cc74dfcfbe699ab1497", size = 720358, upload-time = "2025-07-19T19:24:49.959Z" }, - { url = "https://files.pythonhosted.org/packages/db/a3/edd7d0d5cc0720de132b6651cef95ee080ce5fca11c77d8a47db848e5f90/rignore-0.6.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2b3b1e266ce45189240d14dfa1057f8013ea34b9bc8b3b44125ec8d25fdb3985", size = 885304, upload-time = "2025-07-19T19:23:54.268Z" }, - { url = "https://files.pythonhosted.org/packages/93/a1/d8d2fb97a6548307507d049b7e93885d4a0dfa1c907af5983fd9f9362a21/rignore-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45fe803628cc14714df10e8d6cdc23950a47eb9eb37dfea9a4779f4c672d2aa0", size = 818799, upload-time = "2025-07-19T19:23:47.544Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cd/949981fcc180ad5ba7b31c52e78b74b2dea6b7bf744ad4c0c4b212f6da78/rignore-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e439f034277a947a4126e2da79dbb43e33d73d7c09d3d72a927e02f8a16f59aa", size = 892024, upload-time = "2025-07-19T19:22:36.18Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d3/9042d701a8062d9c88f87760bbc2695ee2c23b3f002d34486b72a85f8efe/rignore-0.6.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b5121650ae24621154c7bdba8b8970b0739d8146505c9f38e0cda9385d1004", size = 871430, upload-time = "2025-07-19T19:22:49.62Z" }, - { url = "https://files.pythonhosted.org/packages/eb/50/3370249b984212b7355f3d9241aa6d02e706067c6d194a2614dfbc0f5b27/rignore-0.6.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b0957b585ab48a445cf8ac1dbc33a272ab060835e583b4f95aa8c67c23fb2b", size = 1160559, upload-time = "2025-07-19T19:23:01.629Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6f/2ad7f925838091d065524f30a8abda846d1813eee93328febf262b5cda21/rignore-0.6.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50359e0d5287b5e2743bd2f2fbf05df619c8282fd3af12f6628ff97b9675551d", size = 939947, upload-time = "2025-07-19T19:23:14.608Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/626ec94d62475ae7ef8b00ef98cea61cbea52a389a666703c97c4673d406/rignore-0.6.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe18096dcb1596757dfe0b412aab6d32564473ae7ee58dea0a8b4be5b1a2e3b", size = 949471, upload-time = "2025-07-19T19:23:37.521Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c3/699c4f03b3c46f4b5c02f17a0a339225da65aad547daa5b03001e7c6a382/rignore-0.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b79c212d9990a273ad91e8d9765e1766ef6ecedd3be65375d786a252762ba385", size = 974912, upload-time = "2025-07-19T19:23:27.13Z" }, - { url = "https://files.pythonhosted.org/packages/cd/35/04626c12f9f92a9fc789afc2be32838a5d9b23b6fa8b2ad4a8625638d15b/rignore-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6ffa7f2a8894c65aa5dc4e8ac8bbdf39a326c0c6589efd27686cfbb48f0197d", size = 1067281, upload-time = "2025-07-19T19:24:01.016Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/8f17baf3b984afea151cb9094716f6f1fb8e8737db97fc6eb6d494bd0780/rignore-0.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a63f5720dffc8d8fb0a4d02fafb8370a4031ebf3f99a4e79f334a91e905b7349", size = 1134414, upload-time = "2025-07-19T19:24:13.534Z" }, - { url = "https://files.pythonhosted.org/packages/10/88/ef84ffa916a96437c12cefcc39d474122da9626d75e3a2ebe09ec5d32f1b/rignore-0.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ce33982da47ac5dc09d19b04fa8d7c9aa6292fc0bd1ecf33076989faa8886094", size = 1109330, upload-time = "2025-07-19T19:24:25.303Z" }, - { url = "https://files.pythonhosted.org/packages/27/43/2ada5a2ec03b82e903610a1c483f516f78e47700ee6db9823f739e08b3af/rignore-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d899621867aa266824fbd9150e298f19d25b93903ef0133c09f70c65a3416eca", size = 1120381, upload-time = "2025-07-19T19:24:37.798Z" }, - { url = "https://files.pythonhosted.org/packages/3b/99/e7bcc643085131cb14dbea772def72bf1f6fe9037171ebe177c4f228abc8/rignore-0.6.4-cp313-cp313-win32.whl", hash = "sha256:d0615a6bf4890ec5a90b5fb83666822088fbd4e8fcd740c386fcce51e2f6feea", size = 641761, upload-time = "2025-07-19T19:24:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/d9/25/7798908044f27dea1a8abdc75c14523e33770137651e5f775a15143f4218/rignore-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:145177f0e32716dc2f220b07b3cde2385b994b7ea28d5c96fbec32639e9eac6f", size = 719876, upload-time = "2025-07-19T19:24:51.125Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e3/ae1e30b045bf004ad77bbd1679b9afff2be8edb166520921c6f29420516a/rignore-0.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e55bf8f9bbd186f58ab646b4a08718c77131d28a9004e477612b0cbbd5202db2", size = 891776, upload-time = "2025-07-19T19:22:37.78Z" }, - { url = "https://files.pythonhosted.org/packages/45/a9/1193e3bc23ca0e6eb4f17cf4b99971237f97cfa6f241d98366dff90a6d09/rignore-0.6.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2521f7bf3ee1f2ab22a100a3a4eed39a97b025804e5afe4323528e9ce8f084a5", size = 871442, upload-time = "2025-07-19T19:22:50.972Z" }, - { url = "https://files.pythonhosted.org/packages/20/83/4c52ae429a0b2e1ce667e35b480e9a6846f9468c443baeaed5d775af9485/rignore-0.6.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cc35773a8a9c119359ef974d0856988d4601d4daa6f532c05f66b4587cf35bc", size = 1159844, upload-time = "2025-07-19T19:23:02.751Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2f/c740f5751f464c937bfe252dc15a024ae081352cfe80d94aa16d6a617482/rignore-0.6.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b665b1ea14457d7b49e834baabc635a3b8c10cfb5cca5c21161fabdbfc2b850e", size = 939456, upload-time = "2025-07-19T19:23:15.72Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/68dbb08ac0edabf44dd144ff546a3fb0253c5af708e066847df39fc9188f/rignore-0.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c7fd339f344a8548724f289495b835bed7b81174a0bc1c28c6497854bd8855db", size = 1067070, upload-time = "2025-07-19T19:24:02.803Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3a/7e7ea6f0d31d3f5beb0f2cf2c4c362672f5f7f125714458673fc579e2bed/rignore-0.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:91dc94b1cc5af8d6d25ce6edd29e7351830f19b0a03b75cb3adf1f76d00f3007", size = 1134598, upload-time = "2025-07-19T19:24:15.039Z" }, - { url = "https://files.pythonhosted.org/packages/7e/06/1b3307f6437d29bede5a95738aa89e6d910ba68d4054175c9f60d8e2c6b1/rignore-0.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4d1918221a249e5342b60fd5fa513bf3d6bf272a8738e66023799f0c82ecd788", size = 1108862, upload-time = "2025-07-19T19:24:26.765Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/b37c82519f335f2c472a63fc6215c6f4c51063ecf3166e3acf508011afbd/rignore-0.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:240777332b859dc89dcba59ab6e3f1e062bc8e862ffa3e5f456e93f7fd5cb415", size = 1120002, upload-time = "2025-07-19T19:24:38.952Z" }, - { url = "https://files.pythonhosted.org/packages/ac/72/2f05559ed5e69bdfdb56ea3982b48e6c0017c59f7241f7e1c5cae992b347/rignore-0.6.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b0e548753e55cc648f1e7b02d9f74285fe48bb49cec93643d31e563773ab3f", size = 949454, upload-time = "2025-07-19T19:23:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/0b/92/186693c8f838d670510ac1dfb35afbe964320fbffb343ba18f3d24441941/rignore-0.6.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6971ac9fdd5a0bd299a181096f091c4f3fd286643adceba98eccc03c688a6637", size = 974663, upload-time = "2025-07-19T19:23:28.24Z" }, - { url = "https://files.pythonhosted.org/packages/33/a1/daaa2df10dfa6d87c896a5783c8407c284530d5a056307d1f55a8ef0c533/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b3da26d5a35ab15525b68d30b7352ad2247321f5201fc7e50ba6d547f78d5ea", size = 895772, upload-time = "2025-07-19T19:22:43.423Z" }, - { url = "https://files.pythonhosted.org/packages/35/e6/65130a50cd3ed11c967034dfd653e160abb7879fb4ee338a1cccaeda7acd/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43028f3587558231d9fa68accff58c901dc50fd7bbc5764d3ee3df95290f6ebf", size = 873093, upload-time = "2025-07-19T19:22:55.745Z" }, - { url = "https://files.pythonhosted.org/packages/32/c4/02ead1274ce935c59f2bb3deaaaa339df9194bc40e3c2d8d623e31e47ec4/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc56f1fcab7740751b98fead67b98ba64896424d8c834ea22089568db4e36dfa", size = 1162199, upload-time = "2025-07-19T19:23:08.376Z" }, - { url = "https://files.pythonhosted.org/packages/78/0c/94a4edce0e80af69f200cc35d8da4c727c52d28f0c9d819b388849ae8ef6/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6033f2280898535a5f69935e08830a4e49ff1e29ef2c3f9a2b9ced59de06fdbf", size = 940176, upload-time = "2025-07-19T19:23:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/43/92/21ec579c999a3ed4d1b2a5926a9d0edced7c65d8ac353bc9120d49b05a64/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f5ac0c4e6a24be88f3821e101ef4665e9e1dc015f9e45109f32fed71dbcdafa", size = 951632, upload-time = "2025-07-19T19:23:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/67/c4/72e7ba244222b9efdeb18f9974d6f1e30cf5a2289e1b482a1e8b3ebee90f/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8906ac8dd585ece83b1346e0470260a1951058cc0ef5a17542069bde4aa3f42f", size = 976923, upload-time = "2025-07-19T19:23:32.678Z" }, - { url = "https://files.pythonhosted.org/packages/8e/14/e754c12bc953c7fa309687cd30a6ea95e5721168fb0b2a99a34bff24be5c/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:14d095622969504a2e56f666286202dad583f08d3347b7be2d647ddfd7a9bf47", size = 1069861, upload-time = "2025-07-19T19:24:07.671Z" }, - { url = "https://files.pythonhosted.org/packages/a6/24/ba2bdaf04a19b5331c051b9d480e8daca832bed4aeaa156d6d679044c06c/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:30f3d688df7eb4850318f1b5864d14f2c5fe5dbf3803ed0fc8329d2a7ad560dc", size = 1136368, upload-time = "2025-07-19T19:24:19.68Z" }, - { url = "https://files.pythonhosted.org/packages/83/48/7cf52353299e02aa629150007fa75f4b91d99b4f2fa536f2e24ead810116/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:028f62a7b0a6235bb3f03c9e7f342352e7fa4b3f08c761c72f9de8faee40ed9c", size = 1111714, upload-time = "2025-07-19T19:24:31.717Z" }, - { url = "https://files.pythonhosted.org/packages/84/9c/3881ad34f01942af0cf713e25e476bf851e04e389cc3ff146c3b459ab861/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7e6c425603db2c147eace4f752ca3cd4551e7568c9d332175d586c68bcbe3d8d", size = 1122433, upload-time = "2025-07-19T19:24:43.973Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ae/46/e5ef3423a3746f91d3a3d9a68c499fde983be7dbab7d874efa8d3bb139ba/rignore-0.7.0.tar.gz", hash = "sha256:cfe6a2cbec855b440d7550d53e670246fce43ca5847e46557b6d4577c9cdb540", size = 12796, upload-time = "2025-10-02T13:26:22.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c4/c6fe75a64c9499b1d01c6e00054a9564900aaee3cb8d99cce7b9d853aba3/rignore-0.7.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a83923fd4adff85737c54aecbdb8b7c8f1bba913af019ffebcf6d65d3903cefd", size = 883839, upload-time = "2025-10-02T13:25:04.814Z" }, + { url = "https://files.pythonhosted.org/packages/95/cf/90db9c137bebce283f6fad00b032b9953ee4239f4f67e53e993550e0740b/rignore-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f029f6b8f66310659d4e8616a0adaf0de79b7b076b1e37261d532b24e000eff2", size = 815865, upload-time = "2025-10-02T13:24:53.482Z" }, + { url = "https://files.pythonhosted.org/packages/31/08/d64298cec32d5df121968b3ab75d17d2a30ff02f080a3457893e57689809/rignore-0.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:686c162f945ede315b7b63958d83531b18226cad4fae9170a5787dd8b8b4be89", size = 891607, upload-time = "2025-10-02T13:23:18.739Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b3/602bb25ba0c862dd3f7f52af0f5e3fce4321207a1b76c0b3b7f17aed0146/rignore-0.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3c8b62a00c1b6e0ed73412ba8d37d05e214e6a8757f2779d313078d2bdec209", size = 865644, upload-time = "2025-10-02T13:23:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fc/18f5ac22714bdd0437aaa59ff2ded2ba3caff2745c89e671bc9c91c52947/rignore-0.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f115666738614cdb0ef122c2b48806043b9b6c603dc03a4708b2eb1df5a44514", size = 1167949, upload-time = "2025-10-02T13:23:54.257Z" }, + { url = "https://files.pythonhosted.org/packages/b6/1b/6409b434420995b8897c3d6b5a2701343857d2d36d159bd9305287c33634/rignore-0.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ffaf2047304b97bc648592f82c0aeba3468f43546a918994411b8f1d79d42d6", size = 935950, upload-time = "2025-10-02T13:24:10.463Z" }, + { url = "https://files.pythonhosted.org/packages/b9/56/c0a03cb643ca41091f0377ffea3a35ae3f3cff39b075ca94eec35fae6ed0/rignore-0.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04678c2f1787eb07378754d6aa50e66ce712e0b75e8b843fd9e5e4da35130617", size = 951418, upload-time = "2025-10-02T13:24:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3b/33783bc1681662789f71614dee496fb0dd96de4887eb8d5d2cb9f365d1ff/rignore-0.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53a4c4a43558f34b32732efcee9c79c7948ff26673bb764aa0e9bbe951e435fa", size = 975421, upload-time = "2025-10-02T13:24:27.049Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e2/af19c05288c2afb5b79f73c68e88a34b88245b66e5cf358417461a72c8c5/rignore-0.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:794f72ce7195cad1fb41c03b3e3484396c404498b73855004ebea965a697edd9", size = 1071989, upload-time = "2025-10-02T13:25:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ea/6ab6d1afafcd3f6e5ba898646bcfe3a6f69eb8f4ac264dd82848ab7f2c5b/rignore-0.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:989f35a152bc508c52d63d7d4527215c5dabe7981e5744bcf35f96c99f3758f7", size = 1129150, upload-time = "2025-10-02T13:25:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/a327d54cbd5f9f34ed383057ee1c9a044571878045cbd37a129f27f13ab0/rignore-0.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b945b29a995fdcf669dc098ec40237131742de2cf49484011ba3f81d0fff23a3", size = 1107917, upload-time = "2025-10-02T13:25:49.702Z" }, + { url = "https://files.pythonhosted.org/packages/86/f8/89a1269911e7895e3c4a5c1fb1abb3b9b255362035fa54c593287cf38b15/rignore-0.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e4deda4c3e5cec1ebfb714094cd9af79e8840680187537d13a216377d6aa2ed6", size = 1116013, upload-time = "2025-10-02T13:26:07.597Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/6e85f0437451777649a582b558252f671571ad044d3d14a70978d5f9070c/rignore-0.7.0-cp311-cp311-win32.whl", hash = "sha256:d0fa18c39a4f25275abeb05a7889d11b4dfed9966d5eb1d41fd13da1394863b0", size = 637212, upload-time = "2025-10-02T13:26:36.34Z" }, + { url = "https://files.pythonhosted.org/packages/e7/10/d2ac60b125b19c0ed976ce66cae4d3061c390e650d2806ac2b9e6fe17634/rignore-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac18b6fe469a3c57a92c5fc82f94f260922177b003189104eb758316b7b54d6e", size = 716632, upload-time = "2025-10-02T13:26:25.224Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0e/be002ba0cb4752b518de8487968a82c47ad2cc956af354e09f055474754b/rignore-0.7.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:df6d38f3c3903bfeec94e8a927a3656e0b95c27d3b5c29e63797dd359978aff8", size = 880602, upload-time = "2025-10-02T13:25:06.365Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7f/8a16c5d6200952a219ad8866be430ed42f488b1888449aab0eba20e8123c/rignore-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da1b9ccc2cf6df196fe3187287e7ed858e967ae56974901414031f5524ea33b8", size = 811654, upload-time = "2025-10-02T13:24:55.118Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e6/fd2cbc71f725ea10892c85ea56bd8f54426557cf5ac2924f9c27b771ee45/rignore-0.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0525ccf3e8b9ccd6f1dfc87ecc78218a83605070b247633636d144acdf6b73be", size = 892031, upload-time = "2025-10-02T13:23:20.558Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c8/0dfd755f57515d34ca26de011e016f62db86f7bef0586f2ab0d9f6e18136/rignore-0.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570bcf51fd9f78ec79ec33f2f852e6665027fae80cc3e5e2523c97d3f4220369", size = 865496, upload-time = "2025-10-02T13:23:37.965Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b9/f73af8509842d74788fc26feca25db1eade9291fae79540872c130407340/rignore-0.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32f5d3d90a520d61e43c2a23724852c689c3ed36b38264c77b613f967e2d1f68", size = 1165555, upload-time = "2025-10-02T13:23:56.009Z" }, + { url = "https://files.pythonhosted.org/packages/44/22/67d2fb589cedd7bf3a01e16617f2da10f172165b3ecdaa8fa0707043e9ed/rignore-0.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7d189cfb9059dfa497e5480c411bd2aba838124b50b93abf7e92556221b7956", size = 936631, upload-time = "2025-10-02T13:24:11.97Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6b/e0f969a1cb3ff2caa0dd342e512d7a0a6f1b737b6f5373c04606aa946e80/rignore-0.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c871a31596476ac4343f6b803ee8ddca068425e1837cf6849ebe46c498c73c5", size = 951058, upload-time = "2025-10-02T13:24:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/45/cf/ccf053fb87601332e8b2e2da707f2801bee66ee5fe843687183f45c2e768/rignore-0.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b7d8ce1efbd8fa865712d34753ce4eb8e0732874df95351244e14308fb87d0a", size = 974638, upload-time = "2025-10-02T13:24:29Z" }, + { url = "https://files.pythonhosted.org/packages/de/ae/a00181c0d2dc437a3729dbebcfffd67bb849d1c53e45850c7b4428f5fba4/rignore-0.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d261aea1a51ef93c262b52ad195a1092a8bae17577e8192473d1b5fd30379346", size = 1072970, upload-time = "2025-10-02T13:25:18.888Z" }, + { url = "https://files.pythonhosted.org/packages/81/30/3011207fc9f26f9eb21d2282dfedd8f2d66cf7a9a3053370c9b4b87601e1/rignore-0.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:034bef935e3734b4ad2dada59c96717f3e3d0b48551a0c79379c4d3280b4a397", size = 1128833, upload-time = "2025-10-02T13:25:34.987Z" }, + { url = "https://files.pythonhosted.org/packages/4b/be/4c6a860f851db6cb0b96a3ec62dd4fe95290ee36e67b845ffab58908c6cc/rignore-0.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f816b65c9bf97093d792c9b50369d5a81a5f95b4ed5f003d4091bd1db3b70d8", size = 1106909, upload-time = "2025-10-02T13:25:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/691d79e72f000968e1e3457ff53634760dac24fa6c6b5663d994362b8a99/rignore-0.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b88479f0a89828781d25a9acd485be88abf4f1f1c14e455b6530da265adb593c", size = 1115733, upload-time = "2025-10-02T13:26:09.256Z" }, + { url = "https://files.pythonhosted.org/packages/30/5b/4566f88a4ad452f94995cfca55c2509238ab94c4e191497edd1fd21dac4c/rignore-0.7.0-cp312-cp312-win32.whl", hash = "sha256:89324cffc3312ad50e43f07f51966d421dc44d7c0d219747259270ee5fbc59e3", size = 637030, upload-time = "2025-10-02T13:26:38.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6a/169ced0141a9f102a97b9de2b20d3d77043a9a0ced4ef94148f31ba02628/rignore-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:bbbbc7582d3926a250a14acf7c6b1d60b6d610275ac026856555fd12492e716e", size = 716355, upload-time = "2025-10-02T13:26:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/5e/85/cd1441043c5ed13e671153af260c5f328042ebfb87aa28849367602206f2/rignore-0.7.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:190e469db68112c4027a7a126facfd80ce353374ff208c585ca7dacc75de0472", size = 880474, upload-time = "2025-10-02T13:25:08.111Z" }, + { url = "https://files.pythonhosted.org/packages/f4/07/d5b9593cb05593718508308543a8fbee75998a7489cf4f4b489d2632bd4a/rignore-0.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0a43f6fabf46ed8e96fbf2861187362e513960c2a8200c35242981bd36ef8b96", size = 811882, upload-time = "2025-10-02T13:24:56.599Z" }, + { url = "https://files.pythonhosted.org/packages/aa/67/b82b2704660c280061d8bc90bc91092622309f78e20c9e3321f45f88cd4e/rignore-0.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b89a59e5291805eca3c3317a55fcd2a579e9ee1184511660078a398182463deb", size = 892043, upload-time = "2025-10-02T13:23:22.326Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7e/e91a1899a06882cd8a7acc3025c51b9f830971b193bd6b72e34254ed7733/rignore-0.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a155f36be847c05c800e0218e9ac04946ba44bf077e1f11dc024ca9e1f7a727", size = 865404, upload-time = "2025-10-02T13:23:40.085Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/68487538a2d2d7e0e1ca1051d143af690211314e22cbed58a245e816ebaf/rignore-0.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dba075135ac3cda5f3236b4f03f82bbcd97454a908631ad3da93aae1e7390b17", size = 1167661, upload-time = "2025-10-02T13:23:57.578Z" }, + { url = "https://files.pythonhosted.org/packages/b4/39/8498ac13fb710a1920526480f9476aaeaaaa20c522a027d07513929ba9d9/rignore-0.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8525b8c31f36dc9fbcb474ef58d654f6404b19b6110b7f5df332e58e657a4aa8", size = 936272, upload-time = "2025-10-02T13:24:13.414Z" }, + { url = "https://files.pythonhosted.org/packages/55/1a/38b92fde209931611dcff0db59bd5656a325ba58d368d4e50f1e711fdd16/rignore-0.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0428b64d8b02ad83fc0a2505ded0e9064cac97df7aa1dffc9c7558b56429912", size = 950552, upload-time = "2025-10-02T13:24:43.263Z" }, + { url = "https://files.pythonhosted.org/packages/e3/01/f59f38ae1b879309b0151b1ed0dd82880e1d3759f91bfdaa570730672308/rignore-0.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab1db960a64835ec3ed541951821bfc38f30dfbd6ebd990f7d039d0c54ff957", size = 974407, upload-time = "2025-10-02T13:24:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/de92fdc09dc1a622abb6d1b2678e940d24de2a07c60d193126eb52a7e8ea/rignore-0.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3749711b1e50fb5b28b55784e159a3b8209ecc72d01cc1511c05bc3a23b4a063", size = 1072865, upload-time = "2025-10-02T13:25:20.451Z" }, + { url = "https://files.pythonhosted.org/packages/65/bb/75fbef03cf56b0918880cb3b922da83d6546309566be60f6c6b451f7221b/rignore-0.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:57240739c786f897f89e29c05e529291ee1b477df9f6b29b774403a23a169fe2", size = 1129007, upload-time = "2025-10-02T13:25:36.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/24/4d591d45a8994fb4afaefa22e356d69948726c9ccba0cfd76c82509aedc2/rignore-0.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b70581286acd5f96ce11efd209bfe9261108586e1a948cc558fc3f58ba5bf5f", size = 1106827, upload-time = "2025-10-02T13:25:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b3/b614d54fa1f1c7621aeb20b2841cd980288ad9d7d61407fc4595d5c5f132/rignore-0.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33fb6e4cba1b798f1328e889b4bf2341894d82e3be42bb3513b4e0fe38788538", size = 1115328, upload-time = "2025-10-02T13:26:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/ea0b3e30e230b2d2222e1ee18e20316c8297088f4cc6a6ea2ee6cb34f595/rignore-0.7.0-cp313-cp313-win32.whl", hash = "sha256:119f0497fb4776cddc663ee8f35085ce00758bd423221ba1e8222a816e10cf5e", size = 636896, upload-time = "2025-10-02T13:26:40.3Z" }, + { url = "https://files.pythonhosted.org/packages/79/16/f55b3db13f6fff408fde348d2a726d3b4ba06ed55dce8ff119e374ce3005/rignore-0.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb06e11dda689be138909f53639f0baa8d7c6be4d76ca9ec316382ccf3517469", size = 716519, upload-time = "2025-10-02T13:26:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/69/db/8c20a7b59abb21d3d20d387656b6759cd5890fa68185064fe8899f942a4b/rignore-0.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2255821ab4bc34fa129a94535f5d0d88b164940b25d0a3b26ebd41d99f1a9f", size = 890684, upload-time = "2025-10-02T13:23:23.761Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/ae5ca63aed23f64dcd740f55ee6432037af5c09d25efaf79dc052a4a51ff/rignore-0.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b57efcbbc1510f8ce831a5e19fb1fe9dd329bb246c4e4f8a09bf1c06687b0331", size = 865174, upload-time = "2025-10-02T13:23:41.948Z" }, + { url = "https://files.pythonhosted.org/packages/ae/27/5aff661e792efbffda689f0d3fa91ea36f2e0d4bcca3b02f70ae95ea96da/rignore-0.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ead4bc2baceeccdfeb82cb70ba8f70fdb6dc1e58976f805f9d0d19b9ee915f0", size = 1165293, upload-time = "2025-10-02T13:23:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/cb/df/13de7ce5ba2a58c724ef202310408729941c262179389df5e90cb9a41381/rignore-0.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f0a8996437a22df0faf2844d65ec91d41176b9d4e7357abee42baa39dc996ae", size = 936093, upload-time = "2025-10-02T13:24:15.057Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/4ea42bc454db8499906c8d075a7a0053b7fd381b85f3bcc857e68a8b8b23/rignore-0.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cb17ef4a413444fccbd57e1b4a3870f1320951b81f1b7007af9c70e1a5bc2897", size = 1071518, upload-time = "2025-10-02T13:25:22.076Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a7/7400a4343d1b5a1345a98846c6fd7768ff13890d207fce79d690c7fd7798/rignore-0.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:b12b316adf6cf64f9d22bd690b2aa019a37335a1f632a0da7fb15a423cb64080", size = 1128403, upload-time = "2025-10-02T13:25:38.394Z" }, + { url = "https://files.pythonhosted.org/packages/45/8b/ce8ff27336a86bad47bbf011f8f7fb0b82b559ee4a0d6a4815ee3555ef56/rignore-0.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:dba8181d999387c17dd6cce5fd7f0009376ca8623d2d86842d034b18d83dc768", size = 1105552, upload-time = "2025-10-02T13:25:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e2/7925b564d853c7057f150a7f2f384400422ed30f7b7baf2fde5849562381/rignore-0.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:04a3d4513cdd184f4f849ae8d6407a169cca543a2c4dd69bfc42e67cb0155504", size = 1114826, upload-time = "2025-10-02T13:26:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/c4/34/c42ccdd81143d38d99e45b965e4040a1ef6c07a365ad205dd94b6d16c794/rignore-0.7.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a296bc26b713aacd0f31702e7d89426ba6240abdbf01b2b18daeeaeaa782f475", size = 879718, upload-time = "2025-10-02T13:25:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/f522adf949d2b581a0a1e488a79577631ed6661fdc12e80d4182ed655036/rignore-0.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7f71807ed0bc1542860a8fa1615a0d93f3d5a22dde1066e9f50d7270bc60686", size = 810391, upload-time = "2025-10-02T13:24:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/f2/82/935bffa4ad7d9560541daaca7ba0e4ee9b0b9a6370ab9518cf9c991087bb/rignore-0.7.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7e6ff54399ddb650f4e4dc74b325766e7607967a49b868326e9687fc3642620", size = 950261, upload-time = "2025-10-02T13:24:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0e/22abda23cc6d20901262fcfea50c25ed66ca6e1a5dc610d338df4ca10407/rignore-0.7.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09dfad3ca450b3967533c6b1a2c7c0228c63c518f619ff342df5f9c3ed978b66", size = 974258, upload-time = "2025-10-02T13:24:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8d/0ba2c712723fdda62125087d00dcdad93102876d4e3fa5adbb99f0b859c3/rignore-0.7.0-cp314-cp314-win32.whl", hash = "sha256:2850718cfb1caece6b7ac19a524c7905a8d0c6627b0d0f4e81798e20b6c75078", size = 637403, upload-time = "2025-10-02T13:26:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/1c/63/0d7df1237c6353d1a85d8a0bc1797ac766c68e8bc6fbca241db74124eb61/rignore-0.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2401637dc8ab074f5e642295f8225d2572db395ae504ffc272a8d21e9fe77b2c", size = 717404, upload-time = "2025-10-02T13:26:29.936Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/b02edbf5059f7947e375dc46583283aad579505e9e07775277e7fd6e04db/rignore-0.7.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f40142a34a7f08cd90fb4e74e43deffe3381fa3b164fb59857fa4e3996d4716d", size = 892600, upload-time = "2025-10-02T13:23:31.158Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/3caa7732a91623110bc80c30f592efc6571a1c610b94f36083601ebf2392/rignore-0.7.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccbc0b6285bb981316e5646ac96be7bca9665ee2444427d8d170fda5eda6f022", size = 866500, upload-time = "2025-10-02T13:23:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/8b/66/943300886972b2dded2e0e851c1da1ad36565d40b5e55833b049cbf9285b/rignore-0.7.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77cdf15a8b0ab80cd1d05a754b3237330e60e8731c255b7eb2a5d240a68df9f8", size = 1167255, upload-time = "2025-10-02T13:24:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/1e/26/2f8cb5a546ce7056fe0fb8afbfc887431f9ba986cd7b4c65821dac13afa8/rignore-0.7.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14e7e5ac99d60dd1993032205de7e79c36687825c45a7caa704620a0e9fde03f", size = 937991, upload-time = "2025-10-02T13:24:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/f97d581fc4d1013a42fe51154f820a7ccb97c679a2c2ea0c73072aa8935e/rignore-0.7.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fae67456f053942ccda2cb2677a55fd34397e6674eaa403ab7c1c4930dcb12", size = 951972, upload-time = "2025-10-02T13:24:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6a/06/18da8ea8fc217fce872f81de23217c7ae011dd6e396dff026a262b499a4b/rignore-0.7.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b55d2dcee6808f677ef25219ec0bb4852fbf2edb0b5010a5f18fe5feee276d6", size = 976002, upload-time = "2025-10-02T13:24:36.851Z" }, + { url = "https://files.pythonhosted.org/packages/ea/11/2f998fccb85a31f8dbd94b31123b48645067d4ca55b49c033987286475e7/rignore-0.7.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:7ff87634a648f17a9992ac4ce2fb48397696e3ab4a80154a895b9d1f6fc606cf", size = 1073180, upload-time = "2025-10-02T13:25:28.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/bf/ee6927f8dd8644f4c9c44d364380ab49629d259cc9611224512b161d7bef/rignore-0.7.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c5721daa569fae74f5bf060165f96c6fec0a963ed008213e778259945406ec53", size = 1130056, upload-time = "2025-10-02T13:25:45.019Z" }, + { url = "https://files.pythonhosted.org/packages/33/89/b231f432caced14303055c8611b34c5e2910c48b882de1c79eff4ce177d0/rignore-0.7.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:5770e783e08403b02c052b8b74a3e9431142aca93c78ccd1cc389b4dc60c2846", size = 1108603, upload-time = "2025-10-02T13:26:02.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/33/d331a0aea9e4a00ff530ad18421c46e213da1a608ad05463a2e5ae6cc572/rignore-0.7.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:504f66805fcc2a684cd1cda460d9f15b8b08997f06d9281efa221007072c53f5", size = 1117330, upload-time = "2025-10-02T13:26:18.741Z" }, ] [[package]] name = "rpds-py" -version = "0.27.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/c1/49d515434c1752e40f5e35b985260cf27af052593378580a2f139a5be6b8/rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622", size = 371577, upload-time = "2025-08-07T08:23:25.379Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6d/bf2715b2fee5087fa13b752b5fd573f1a93e4134c74d275f709e38e54fe7/rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5", size = 354959, upload-time = "2025-08-07T08:23:26.767Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5c/e7762808c746dd19733a81373c10da43926f6a6adcf4920a21119697a60a/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4", size = 381485, upload-time = "2025-08-07T08:23:27.869Z" }, - { url = "https://files.pythonhosted.org/packages/40/51/0d308eb0b558309ca0598bcba4243f52c4cd20e15fe991b5bd75824f2e61/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f", size = 396816, upload-time = "2025-08-07T08:23:29.424Z" }, - { url = "https://files.pythonhosted.org/packages/5c/aa/2d585ec911d78f66458b2c91252134ca0c7c70f687a72c87283173dc0c96/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e", size = 514950, upload-time = "2025-08-07T08:23:30.576Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ef/aced551cc1148179557aed84343073adadf252c91265263ee6203458a186/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1", size = 402132, upload-time = "2025-08-07T08:23:32.428Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/cf644803d8d417653fe2b3604186861d62ea6afaef1b2284045741baef17/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc", size = 383660, upload-time = "2025-08-07T08:23:33.829Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/caf47c55ce02b76cbaeeb2d3b36a73da9ca2e14324e3d75cf72b59dcdac5/rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85", size = 401730, upload-time = "2025-08-07T08:23:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/0b/71/c1f355afdcd5b99ffc253422aa4bdcb04ccf1491dcd1bda3688a0c07fd61/rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171", size = 416122, upload-time = "2025-08-07T08:23:36.062Z" }, - { url = "https://files.pythonhosted.org/packages/38/0f/f4b5b1eda724ed0e04d2b26d8911cdc131451a7ee4c4c020a1387e5c6ded/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d", size = 558771, upload-time = "2025-08-07T08:23:37.478Z" }, - { url = "https://files.pythonhosted.org/packages/93/c0/5f8b834db2289ab48d5cffbecbb75e35410103a77ac0b8da36bf9544ec1c/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626", size = 587876, upload-time = "2025-08-07T08:23:38.662Z" }, - { url = "https://files.pythonhosted.org/packages/d2/dd/1a1df02ab8eb970115cff2ae31a6f73916609b900dc86961dc382b8c2e5e/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e", size = 554359, upload-time = "2025-08-07T08:23:39.897Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/95a014ab0d51ab6e3bebbdb476a42d992d2bbf9c489d24cff9fda998e925/rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7", size = 218084, upload-time = "2025-08-07T08:23:41.086Z" }, - { url = "https://files.pythonhosted.org/packages/49/78/f8d5b71ec65a0376b0de31efcbb5528ce17a9b7fdd19c3763303ccfdedec/rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261", size = 230085, upload-time = "2025-08-07T08:23:42.143Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/84429745184091e06b4cc70f8597408e314c2d2f7f5e13249af9ffab9e3d/rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0", size = 222112, upload-time = "2025-08-07T08:23:43.233Z" }, - { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, - { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, - { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, - { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, - { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, - { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, - { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, - { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, - { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, - { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, - { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, - { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, - { url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" }, - { url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" }, - { url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" }, - { url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" }, - { url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" }, - { url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" }, - { url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" }, - { url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" }, - { url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" }, - { url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" }, - { url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" }, - { url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" }, - { url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" }, - { url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" }, - { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, - { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" }, - { url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" }, - { url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" }, - { url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" }, - { url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" }, - { url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" }, - { url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" }, - { url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" }, - { url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/59/64/72ab5b911fdcc48058359b0e786e5363e3fde885156116026f1a2ba9a5b5/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089", size = 371658, upload-time = "2025-08-07T08:26:02.369Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4b/90ff04b4da055db53d8fea57640d8d5d55456343a1ec9a866c0ecfe10fd1/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d", size = 355529, upload-time = "2025-08-07T08:26:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/a4/be/527491fb1afcd86fc5ce5812eb37bc70428ee017d77fee20de18155c3937/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424", size = 382822, upload-time = "2025-08-07T08:26:05.52Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a5/dcdb8725ce11e6d0913e6fcf782a13f4b8a517e8acc70946031830b98441/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8", size = 397233, upload-time = "2025-08-07T08:26:07.179Z" }, - { url = "https://files.pythonhosted.org/packages/33/f9/0947920d1927e9f144660590cc38cadb0795d78fe0d9aae0ef71c1513b7c/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859", size = 514892, upload-time = "2025-08-07T08:26:08.622Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ed/d1343398c1417c68f8daa1afce56ef6ce5cc587daaf98e29347b00a80ff2/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5", size = 402733, upload-time = "2025-08-07T08:26:10.433Z" }, - { url = "https://files.pythonhosted.org/packages/1d/0b/646f55442cd14014fb64d143428f25667a100f82092c90087b9ea7101c74/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14", size = 384447, upload-time = "2025-08-07T08:26:11.847Z" }, - { url = "https://files.pythonhosted.org/packages/4b/15/0596ef7529828e33a6c81ecf5013d1dd33a511a3e0be0561f83079cda227/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c", size = 402502, upload-time = "2025-08-07T08:26:13.537Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/986af3c42f8454a6cafff8729d99fb178ae9b08a9816325ac7a8fa57c0c0/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60", size = 416651, upload-time = "2025-08-07T08:26:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/b4ec3629b7b447e896eec574469159b5b60b7781d3711c914748bf32de05/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be", size = 559460, upload-time = "2025-08-07T08:26:16.295Z" }, - { url = "https://files.pythonhosted.org/packages/61/63/d1e127b40c3e4733b3a6f26ae7a063cdf2bc1caa5272c89075425c7d397a/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114", size = 588072, upload-time = "2025-08-07T08:26:17.776Z" }, - { url = "https://files.pythonhosted.org/packages/04/7e/8ffc71a8f6833d9c9fb999f5b0ee736b8b159fd66968e05c7afc2dbcd57e/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466", size = 555083, upload-time = "2025-08-07T08:26:19.301Z" }, +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, ] [[package]] name = "ruff" -version = "0.12.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, - { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, - { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, - { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, - { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, - { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, - { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, - { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, - { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, - { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" }, + { url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" }, + { url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" }, + { url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" }, ] [[package]] @@ -2112,15 +2160,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.35.0" +version = "2.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/83/055dc157b719651ef13db569bb8cf2103df11174478649735c1b2bf3f6bc/sentry_sdk-2.35.0.tar.gz", hash = "sha256:5ea58d352779ce45d17bc2fa71ec7185205295b83a9dbb5707273deb64720092", size = 343014, upload-time = "2025-08-14T17:11:20.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/72/43294fa4bdd75c51610b5104a3ff834459ba653abb415150aa7826a249dd/sentry_sdk-2.39.0.tar.gz", hash = "sha256:8c185854d111f47f329ab6bc35993f28f7a6b7114db64aa426b326998cfa14e9", size = 348556, upload-time = "2025-09-25T09:15:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3d/742617a7c644deb0c1628dcf6bb2d2165ab7c6aab56fe5222758994007f8/sentry_sdk-2.35.0-py2.py3-none-any.whl", hash = "sha256:6e0c29b9a5d34de8575ffb04d289a987ff3053cf2c98ede445bea995e3830263", size = 363806, upload-time = "2025-08-14T17:11:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/4356cc64246ba7b2b920f7c97a85c3c52748e213e250b512ee8152eb559d/sentry_sdk-2.39.0-py2.py3-none-any.whl", hash = "sha256:ba655ca5e57b41569b18e2a5552cb3375209760a5d332cdd87c6c3f28f729602", size = 370851, upload-time = "2025-09-25T09:15:36.35Z" }, ] [[package]] @@ -2216,15 +2264,15 @@ wheels = [ [[package]] name = "sqlmodel" -version = "0.0.24" +version = "0.0.25" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/80/d9c098a88724ee4554907939cf39590cf67e10c6683723216e228d3315f7/sqlmodel-0.0.25.tar.gz", hash = "sha256:56548c2e645975b1ed94d6c53f0d13c85593f57926a575e2bf566650b2243fa4", size = 117075, upload-time = "2025-09-17T21:44:41.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/5d175ce8de07fe694ec4e3d4d65c2dd06cc30f6c79599b31f9d2f6dd2830/sqlmodel-0.0.25-py3-none-any.whl", hash = "sha256:c98234cda701fb77e9dcbd81688c23bb251c13bb98ce1dd8d4adc467374d45b7", size = 28893, upload-time = "2025-09-17T21:44:39.764Z" }, ] [[package]] @@ -2241,15 +2289,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.47.2" +version = "0.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, ] [[package]] @@ -2272,7 +2320,7 @@ wheels = [ [[package]] name = "textual" -version = "5.3.0" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", extra = ["linkify", "plugins"] }, @@ -2281,9 +2329,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/ce/f0f938d33d9bebbf8629e0020be00c560ddfa90a23ebe727c2e5aa3f30cf/textual-5.3.0.tar.gz", hash = "sha256:1b6128b339adef2e298cc23ab4777180443240ece5c232f29b22960efd658d4d", size = 1557651, upload-time = "2025-08-07T12:36:50.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/30/38b615f7d4b16f6fdd73e4dcd8913e2d880bbb655e68a076e3d91181a7ee/textual-6.2.1.tar.gz", hash = "sha256:4699d8dfae43503b9c417bd2a6fb0da1c89e323fe91c4baa012f9298acaa83e1", size = 1570645, upload-time = "2025-10-01T16:11:24.467Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2f/f7c8a533bee50fbf5bb37ffc1621e7b2cdd8c9a6301fc51faa35fa50b09d/textual-5.3.0-py3-none-any.whl", hash = "sha256:02a6abc065514c4e21f94e79aaecea1f78a28a85d11d7bfc64abf3392d399890", size = 702671, upload-time = "2025-08-07T12:36:48.272Z" }, + { url = "https://files.pythonhosted.org/packages/c5/93/02c7adec57a594af28388d85da9972703a4af94ae1399542555cd9581952/textual-6.2.1-py3-none-any.whl", hash = "sha256:3c7190633cd4d8bfe6049ae66808b98da91ded2edb85cef54e82bf77b03d2a54", size = 710702, upload-time = "2025-10-01T16:11:22.161Z" }, ] [[package]] @@ -2327,7 +2375,7 @@ wheels = [ [[package]] name = "tox" -version = "4.30.2" +version = "4.30.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -2340,14 +2388,14 @@ dependencies = [ { name = "pyproject-api" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/b7/ba4e391cd112c18338aef270abcda2a25783f90509fa6806c8f2a1ea842e/tox-4.30.2.tar.gz", hash = "sha256:772925ad6c57fe35c7ed5ac3e958ac5ced21dff597e76fc40c1f5bf3cd1b6a2e", size = 202622, upload-time = "2025-09-04T16:24:49.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/b2/cee55172e5e10ce030b087cd3ac06641e47d08a3dc8d76c17b157dba7558/tox-4.30.3.tar.gz", hash = "sha256:f3dd0735f1cd4e8fbea5a3661b77f517456b5f0031a6256432533900e34b90bf", size = 202799, upload-time = "2025-10-02T16:24:39.974Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/28/8212e633612f959e9b61f3f1e3103e651e33d808a097623495590a42f1a4/tox-4.30.2-py3-none-any.whl", hash = "sha256:efd261a42e8c82a59f9026320a80a067f27f44cad2e72a6712010c311d31176b", size = 175527, upload-time = "2025-09-04T16:24:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e4/8bb9ce952820df4165eb34610af347665d6cb436898a234db9d84d093ce6/tox-4.30.3-py3-none-any.whl", hash = "sha256:a9f17b4b2d0f74fe0d76207236925a119095011e5c2e661a133115a8061178c9", size = 175512, upload-time = "2025-10-02T16:24:38.209Z" }, ] [[package]] name = "typer" -version = "0.16.0" +version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2355,9 +2403,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, ] [[package]] @@ -2371,32 +2419,32 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20250822" +version = "6.0.12.20250915" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/85/90a442e538359ab5c9e30de415006fb22567aa4301c908c09f19e42975c2/types_pyyaml-6.0.12.20250822.tar.gz", hash = "sha256:259f1d93079d335730a9db7cff2bcaf65d7e04b4a56b5927d49a612199b59413", size = 17481, upload-time = "2025-08-22T03:02:16.209Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/8e/8f0aca667c97c0d76024b37cffa39e76e2ce39ca54a38f285a64e6ae33ba/types_pyyaml-6.0.12.20250822-py3-none-any.whl", hash = "sha256:1fe1a5e146aa315483592d292b72a172b65b946a6d98aa6ddd8e4aa838ab7098", size = 20314, upload-time = "2025-08-22T03:02:15.002Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, ] [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +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/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { 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.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +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/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { 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]] @@ -2419,15 +2467,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.35.0" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, ] [package.optional-dependencies]