diff --git a/nemo_gym/sandbox/providers/daytona/__init__.py b/nemo_gym/sandbox/providers/daytona/__init__.py new file mode 100644 index 0000000000..086b96966b --- /dev/null +++ b/nemo_gym/sandbox/providers/daytona/__init__.py @@ -0,0 +1,40 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Daytona provider package.""" + +from nemo_gym.sandbox.providers.daytona.provider import ( + DaytonaBatchConfig, + DaytonaConnectionConfig, + DaytonaCreateConfig, + DaytonaCreateError, + DaytonaCreateTimeoutError, + DaytonaOperationConfig, + DaytonaProbeConfig, + DaytonaProvider, + DaytonaProviderOptions, +) + + +__all__ = [ + "DaytonaBatchConfig", + "DaytonaConnectionConfig", + "DaytonaCreateConfig", + "DaytonaCreateError", + "DaytonaCreateTimeoutError", + "DaytonaOperationConfig", + "DaytonaProbeConfig", + "DaytonaProvider", + "DaytonaProviderOptions", +] diff --git a/nemo_gym/sandbox/providers/daytona/configs/daytona.yaml b/nemo_gym/sandbox/providers/daytona/configs/daytona.yaml new file mode 100644 index 0000000000..8342c8f1b9 --- /dev/null +++ b/nemo_gym/sandbox/providers/daytona/configs/daytona.yaml @@ -0,0 +1,46 @@ +# Daytona sandbox provider config. +# +# `sandbox` is the instance name an agent references via `sandbox_provider: +# sandbox`; the child key `daytona` selects the provider class and its value is +# passed to the provider constructor. +# +# Every shipped provider config binds the same name `sandbox`, so swapping +# providers is swapping this config path in `+config_paths` (no agent edit): +# +# AGENT=responses_api_agents/mini_swe_agent_2/configs/mini_swe_agent_2.yaml +# MODEL=responses_api_models/vllm_model/configs/vllm_model.yaml +# gym env start \ +# --config $AGENT \ +# --config nemo_gym/sandbox/providers/daytona/configs/daytona.yaml \ +# --config $MODEL +# +# To run multiple sandboxes at once, give each block a distinct instance name +# (e.g. `daytona_foo`, `daytona_baz`) and reference each by name. +# +# `default_metadata` (optional) is merged into every sandbox's spec metadata +# (SandboxSpec.metadata); an agent's own sandbox_spec.metadata overrides it. +sandbox: + default_metadata: + sandbox-api: daytona-sdk + daytona: + connection: + api_key: ${oc.env:DAYTONA_API_KEY} + create: + timeout_s: 1200 + retries: 4 + retry_delay_s: 5.0 + retry_max_delay_s: 90.0 + auto_stop_interval: 30 + auto_delete_interval: 600 + probe: + timeout_s: 60 + deadline_s: 180 + operations: + retries: 4 + retry_delay_s: 1.0 + retry_max_delay_s: 45.0 + command_retries: 0 + close_timeout_s: 60 + file_timeout_s: 1800 + batch: + concurrency: 3 diff --git a/nemo_gym/sandbox/providers/daytona/provider.py b/nemo_gym/sandbox/providers/daytona/provider.py new file mode 100644 index 0000000000..e62d3b5528 --- /dev/null +++ b/nemo_gym/sandbox/providers/daytona/provider.py @@ -0,0 +1,1068 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Daytona provider implementation.""" + +import asyncio +import logging +import math +import re +import uuid +from collections.abc import Mapping +from dataclasses import dataclass, field, fields, replace +from pathlib import Path +from typing import Any, Awaitable, Callable + +from nemo_gym.sandbox.providers.base import ( + SandboxCreateError, + SandboxCreateVerificationError, + SandboxExecResult, + SandboxHandle, + SandboxResources, + SandboxSpec, + SandboxStatus, +) + + +LOGGER = logging.getLogger(__name__) +DAYTONA_COMMAND_ERROR_PREFIX = "Failed to execute command:" +RETRYABLE_HTTP_STATUS_CODES = {408, 409, 425, 429, 500, 502, 503, 504} +RETRYABLE_ERROR_MARKERS = ( + "connection refused", + "connection reset", + "gateway timeout", + "http 408", + "http 409", + "http 425", + "http 429", + "http 500", + "http 502", + "http 503", + "http 504", + "rate limit", + "service unavailable", + "temporarily unavailable", + "timed out", + "timeout", +) +STATUS_CODE_RE = re.compile(r"(?:status code|http)\D+(\d{3})", re.IGNORECASE) +DAYTONA_EXTENSION_PREFIX = "daytona." +PROVIDER_OPTION_EXTENSIONS = "extensions" +PROVIDER_OPTION_SNAPSHOT_ID = "snapshot_id" +PROVIDER_OPTION_VOLUMES = "volumes" +SANDBOX_ID_LABEL = "sandbox_id" + + +class DaytonaCreateError(SandboxCreateError): + """Raised when Daytona cannot create a sandbox.""" + + +class DaytonaCreateTimeoutError(DaytonaCreateError): + """Raised when Daytona sandbox creation exceeds the client timeout.""" + + +class DaytonaCreateVerificationError(SandboxCreateVerificationError): + """Raised when a newly-created Daytona sandbox cannot execute a probe command.""" + + +def _require_daytona_sdk() -> tuple[Any, Any, Any, Any, Any, Any]: + try: + from daytona import ( + AsyncDaytona, + CreateSandboxFromImageParams, + CreateSandboxFromSnapshotParams, + DaytonaConfig, + Resources, + VolumeMount, + ) + except ImportError as e: + raise ModuleNotFoundError( + "Daytona SDK is required for the daytona sandbox provider. " + "Install nemo-gym[sandbox] before using env.sandbox.provider.name=daytona." + ) from e + + return ( + AsyncDaytona, + DaytonaConfig, + CreateSandboxFromImageParams, + CreateSandboxFromSnapshotParams, + Resources, + VolumeMount, + ) + + +def _require_tenacity() -> tuple[Any, Any, Any, Any]: + try: + from tenacity import AsyncRetrying, retry_if_exception, stop_after_attempt, wait_random_exponential + except ModuleNotFoundError as e: + raise ModuleNotFoundError( + "tenacity is required for Daytona retry handling. Install nemo-gym[sandbox] before using " + "env.sandbox.provider.name=daytona." + ) from e + + return AsyncRetrying, retry_if_exception, stop_after_attempt, wait_random_exponential + + +def _daytona_error_types() -> dict[str, type[BaseException]]: + try: + from daytona import ( + DaytonaAuthenticationError, + DaytonaAuthorizationError, + DaytonaConflictError, + DaytonaConnectionError, + DaytonaError, + DaytonaNotFoundError, + DaytonaRateLimitError, + DaytonaTimeoutError, + DaytonaValidationError, + ) + except ImportError: + return {} + + return { + "authentication": DaytonaAuthenticationError, + "authorization": DaytonaAuthorizationError, + "conflict": DaytonaConflictError, + "connection": DaytonaConnectionError, + "error": DaytonaError, + "not_found": DaytonaNotFoundError, + "rate_limit": DaytonaRateLimitError, + "timeout": DaytonaTimeoutError, + "validation": DaytonaValidationError, + } + + +def _exception_status_code(exception: BaseException) -> int | None: + status_code = getattr(exception, "status_code", None) + if isinstance(status_code, int): + return status_code + match = STATUS_CODE_RE.search(str(exception)) + if match is None: + return None + return int(match.group(1)) + + +def _has_retryable_error_marker(exception: BaseException) -> bool: + message = str(exception).lower() + return any(marker in message for marker in RETRYABLE_ERROR_MARKERS) + + +def _is_daytona_command_exec_error(exception: BaseException) -> bool: + error_types = _daytona_error_types() + if not error_types or not isinstance(exception, error_types["error"]): + return False + return str(exception).strip().startswith(DAYTONA_COMMAND_ERROR_PREFIX) + + +def _is_empty_daytona_command_exec_error(exception: BaseException) -> bool: + return _is_daytona_command_exec_error(exception) and str(exception).strip() == DAYTONA_COMMAND_ERROR_PREFIX + + +def _is_retryable_create_error(exception: BaseException) -> bool: + if isinstance(exception, SandboxCreateVerificationError): + return True + if isinstance(exception, DaytonaCreateTimeoutError): + return False + if isinstance(exception, SandboxCreateError): + return True + if isinstance(exception, TimeoutError): + return False + if isinstance(exception, (ConnectionError, OSError)): + return True + + error_types = _daytona_error_types() + if error_types: + non_retryable_types = ( + error_types["authentication"], + error_types["authorization"], + error_types["not_found"], + error_types["timeout"], + error_types["validation"], + ) + retryable_types = ( + error_types["conflict"], + error_types["connection"], + error_types["rate_limit"], + ) + if isinstance(exception, non_retryable_types): + return False + if isinstance(exception, retryable_types): + return True + if isinstance(exception, error_types["error"]): + status_code = _exception_status_code(exception) + if status_code in RETRYABLE_HTTP_STATUS_CODES: + return True + if status_code is not None and status_code < 500: + return False + + status_code = _exception_status_code(exception) + if status_code in RETRYABLE_HTTP_STATUS_CODES: + return True + if status_code is not None and status_code < 500: + return False + return _has_retryable_error_marker(exception) + + +def _is_retryable_operation_error(exception: BaseException) -> bool: + if isinstance(exception, TimeoutError): + return False + if _is_empty_daytona_command_exec_error(exception): + return True + cause = exception.__cause__ + if isinstance(cause, BaseException) and _is_retryable_operation_error(cause): + return True + if isinstance(exception, (ConnectionError, OSError)): + return True + + error_types = _daytona_error_types() + if error_types: + non_retryable_types = ( + error_types["authentication"], + error_types["authorization"], + error_types["not_found"], + error_types["timeout"], + error_types["validation"], + ) + retryable_types = ( + error_types["conflict"], + error_types["connection"], + error_types["rate_limit"], + ) + if isinstance(exception, non_retryable_types): + return False + if isinstance(exception, retryable_types): + return True + return _is_retryable_create_error(exception) + + +def _is_missing_sandbox_delete_error(exception: BaseException) -> bool: + if _exception_status_code(exception) == 404: + return True + message = str(exception).lower() + return "sandbox" in message and "not found" in message + + +def _daytona_exec_error_message(exception: BaseException) -> str: + message = str(exception).strip() + if _is_empty_daytona_command_exec_error(exception): + message = f"{DAYTONA_COMMAND_ERROR_PREFIX} HTTP request timed out before Daytona returned a response" + + details = [message] + status_code = _exception_status_code(exception) + if status_code is not None: + details.append(f"status_code={status_code}") + error_code = getattr(exception, "error_code", None) + if error_code: + details.append(f"error_code={error_code}") + return "; ".join(details) + + +def _daytona_exec_error_result(exception: BaseException) -> SandboxExecResult: + message = _daytona_exec_error_message(exception) + is_timeout = "timeout" in message.lower() or "timed out" in message.lower() + error_type = "timeout" if is_timeout else "sandbox" + return_code = 124 if is_timeout else 125 + return SandboxExecResult( + stdout=None, + stderr=( + f"Daytona command execution failed before a process exit code was returned: {message}. " + "The command may not have run." + ), + return_code=return_code, + error_type=error_type, + ) + + +def _log_create_retry(retry_state: Any) -> None: + exception = retry_state.outcome.exception() if retry_state.outcome else None + sleep_s = retry_state.next_action.sleep if retry_state.next_action else None + LOGGER.warning( + "Retrying Daytona sandbox create after attempt %s; next_sleep_s=%s; error=%r", + retry_state.attempt_number, + sleep_s, + exception, + ) + + +def _log_operation_retry(retry_state: Any) -> None: + exception = retry_state.outcome.exception() if retry_state.outcome else None + sleep_s = retry_state.next_action.sleep if retry_state.next_action else None + LOGGER.warning( + "Retrying Daytona operation after attempt %s; next_sleep_s=%s; error=%r", + retry_state.attempt_number, + sleep_s, + exception, + ) + + +class DaytonaConfigBase: + """Shared dataclass construction for Daytona provider config blocks.""" + + @classmethod + def from_mapping(cls, value: Any) -> Any: + if value is None: + return cls() + if isinstance(value, cls): + return value + if not isinstance(value, Mapping): + raise TypeError(f"{cls.__name__} must be a mapping or {cls.__name__} instance") + field_names = {field.name for field in fields(cls) if field.init} + unsupported_keys = sorted(str(key) for key in value if key not in field_names) + if unsupported_keys: + raise ValueError(f"Unsupported Daytona {cls.__name__} settings: {', '.join(unsupported_keys)}") + return cls(**{key: val for key, val in value.items() if key in field_names}) + + +def _supplied_config_keys(value: Any, config_cls: type[Any]) -> set[str]: + if not isinstance(value, Mapping): + return set() + field_names = {field.name for field in fields(config_cls) if field.init} + return {str(key) for key in value if key in field_names} + + +def _string_map(values: dict[str, Any]) -> dict[str, str]: + return {str(key): str(value) for key, value in values.items()} + + +@dataclass(frozen=True) +class DaytonaProviderOptions: + """Recognized per-sandbox create options read from ``SandboxSpec.provider_options``.""" + + extensions: Mapping[str, str] = field(default_factory=dict) + snapshot_id: str | None = None + volumes: tuple[Mapping[str, Any], ...] = () + + @classmethod + def from_mapping(cls, options: Mapping[str, Any] | None) -> "DaytonaProviderOptions": + if options is None: + return cls() + if not isinstance(options, Mapping): + raise TypeError("Daytona provider_options must be a mapping") + + allowed = set(cls.__dataclass_fields__) + unknown = set(options) - allowed + if unknown: + raise ValueError( + f"Unknown Daytona provider option(s): {', '.join(sorted(unknown))}. " + f"Supported: {', '.join(sorted(allowed))}" + ) + + extensions = options.get(PROVIDER_OPTION_EXTENSIONS, {}) + if not isinstance(extensions, Mapping): + raise TypeError("Daytona provider option 'extensions' must be a mapping") + snapshot_id = options.get(PROVIDER_OPTION_SNAPSHOT_ID) + if snapshot_id is not None and not isinstance(snapshot_id, str): + raise TypeError("Daytona provider option 'snapshot_id' must be a string") + volumes = options.get(PROVIDER_OPTION_VOLUMES) + if volumes is None: + volumes = () + if not isinstance(volumes, (list, tuple)) or not all(isinstance(volume, Mapping) for volume in volumes): + raise TypeError("Daytona provider option 'volumes' must be a list of mappings") + + return cls( + extensions=_string_map(dict(extensions)), + snapshot_id=snapshot_id, + volumes=tuple(dict(volume) for volume in volumes), + ) + + +def _provider_options(spec: SandboxSpec) -> DaytonaProviderOptions: + return DaytonaProviderOptions.from_mapping(spec.provider_options) + + +def _normalize_spec(spec: SandboxSpec) -> SandboxSpec: + _provider_options(spec) + return replace( + spec, + env=_string_map(spec.env), + metadata=_string_map(spec.metadata), + provider_options={} if spec.provider_options is None else dict(spec.provider_options), + ) + + +def _coerce_bool(name: str, value: Any) -> bool: + if isinstance(value, bool): + return value + normalized = str(value).strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + raise ValueError(f"{name} must be a boolean value") + + +def _coerce_int(name: str, value: Any) -> int: + if isinstance(value, bool): + raise ValueError(f"{name} must be an integer") + if isinstance(value, int): + return value + if isinstance(value, float) and value.is_integer(): + return int(value) + return int(str(value).strip()) + + +def _quantity_to_int(name: str, value: Any) -> int: + if isinstance(value, bool): + raise ValueError(f"resources.{name} must be an integer-like quantity") + if isinstance(value, int): + return value + if isinstance(value, float): + if not value.is_integer(): + raise ValueError(f"resources.{name} must be an integer-like quantity") + return int(value) + + text = str(value).strip() + match = re.fullmatch(r"(\d+(?:\.\d+)?)([A-Za-z]*)", text) + if match is None: + raise ValueError(f"resources.{name} must be an integer-like quantity") + number = float(match.group(1)) + unit = match.group(2).lower() + if name == "cpu" and unit == "m": + return max(1, math.ceil(number / 1000)) + if unit in {"mi", "mib"}: + return math.ceil(number / 1024) + if unit in {"", "g", "gb", "gi", "gib"}: + return math.ceil(number) + raise ValueError(f"resources.{name} uses unsupported unit {unit!r}") + + +def _spec_extensions(spec: SandboxSpec) -> dict[str, str]: + return dict(_provider_options(spec).extensions) + + +def _extension_value(spec: SandboxSpec, key: str) -> str | None: + return _spec_extensions(spec).get(f"{DAYTONA_EXTENSION_PREFIX}{key}") + + +def _configured_or_extension(spec: SandboxSpec, key: str, configured: Any) -> Any: + value = _extension_value(spec, key) + return configured if value is None else value + + +def _resources_requested(resources: SandboxResources | Mapping[str, Any]) -> bool: + if isinstance(resources, SandboxResources): + return any(getattr(resources, field.name) is not None for field in fields(SandboxResources)) + return bool(resources) + + +def _mib_to_gib(name: str, value: Any) -> int: + if isinstance(value, bool): + raise ValueError(f"resources.{name} must be an integer-like quantity") + return math.ceil(float(value) / 1024) + + +def _to_resources(resources: SandboxResources | Mapping[str, Any]) -> Any | None: + _, _, _, _, Resources, _ = _require_daytona_sdk() + kwargs: dict[str, int] = {} + if isinstance(resources, SandboxResources): + if resources.cpu is not None: + kwargs["cpu"] = math.ceil(resources.cpu) + if resources.memory_mib is not None: + kwargs["memory"] = _mib_to_gib("memory_mib", resources.memory_mib) + if resources.disk_gib is not None: + kwargs["disk"] = resources.disk_gib + if resources.gpu is not None: + kwargs["gpu"] = resources.gpu + if resources.gpu_type is not None: + raise ValueError("Daytona resource overrides do not support gpu_type") + else: + if "cpu" in resources: + kwargs["cpu"] = _quantity_to_int("cpu", resources["cpu"]) + if "memory" in resources: + kwargs["memory"] = _quantity_to_int("memory", resources["memory"]) + if "memory_mib" in resources: + kwargs["memory"] = _mib_to_gib("memory_mib", resources["memory_mib"]) + disk_value = resources.get( + "disk", + resources.get("disk_gib", resources.get("ephemeral-storage", resources.get("ephemeral_storage"))), + ) + if disk_value is not None: + kwargs["disk"] = _quantity_to_int("disk", disk_value) + if "gpu" in resources: + kwargs["gpu"] = _quantity_to_int("gpu", resources["gpu"]) + if resources.get("gpu_type") is not None: + raise ValueError("Daytona resource overrides do not support gpu_type") + if not kwargs: + return None + return Resources(**kwargs) + + +def _to_volume_mounts(volumes: list[dict[str, Any]]) -> list[Any]: + _, _, _, _, _, VolumeMount = _require_daytona_sdk() + return [VolumeMount(**volume) for volume in volumes] + + +def _spec_volumes(spec: SandboxSpec) -> list[dict[str, Any]] | None: + volumes = _provider_options(spec).volumes + if not volumes: + return None + return [dict(volume) for volume in volumes] + + +def _spec_snapshot_id(spec: SandboxSpec) -> str | None: + return _provider_options(spec).snapshot_id + + +def _to_sandbox_status(value: Any) -> SandboxStatus: + normalized = str(value or "").lower() + if normalized in {"active", "ready", "running", "started"}: + return SandboxStatus.RUNNING + if normalized in {"creating", "initializing", "pending", "starting"}: + return SandboxStatus.STARTING + if normalized in {"archived", "completed", "deleted", "exited", "stopped", "terminated"}: + return SandboxStatus.STOPPED + if normalized in {"crashed", "error", "failed", "unhealthy"}: + return SandboxStatus.ERROR + return SandboxStatus.UNKNOWN + + +@dataclass(frozen=True) +class DaytonaConnectionConfig(DaytonaConfigBase): + """Daytona API client connection settings.""" + + api_key: str | None = None + jwt_token: str | None = None + organization_id: str | None = None + api_url: str | None = None + server_url: str | None = None + target: str | None = None + connection_pool_maxsize: int | None = None + otel_enabled: bool | None = None + + def __post_init__(self) -> None: + if self.connection_pool_maxsize is not None and self.connection_pool_maxsize <= 0: + raise ValueError("connection.connection_pool_maxsize must be > 0") + + +@dataclass(frozen=True) +class DaytonaCreateConfig(DaytonaConfigBase): + """Daytona sandbox creation settings.""" + + timeout_s: float = 60.0 + retries: int = 2 + retry_delay_s: float = 1.0 + retry_max_delay_s: float = 30.0 + language: str | None = None + os_user: str | None = None + public: bool | None = None + auto_stop_interval: int | None = None + auto_archive_interval: int | None = None + auto_delete_interval: int | None = None + ephemeral: bool | None = None + network_block_all: bool | None = None + network_allow_list: str | None = None + + def __post_init__(self) -> None: + if self.timeout_s < 0: + raise ValueError("create.timeout_s must be >= 0") + if self.retries < 0: + raise ValueError("create.retries must be >= 0") + if self.retry_delay_s < 0: + raise ValueError("create.retry_delay_s must be >= 0") + if self.retry_max_delay_s < 0: + raise ValueError("create.retry_max_delay_s must be >= 0") + + +@dataclass(frozen=True) +class DaytonaProbeConfig(DaytonaConfigBase): + """Post-create probe settings.""" + + command: str | None = "printf nemo-rl-sandbox-ready" + expected_stdout: str | None = "nemo-rl-sandbox-ready" + timeout_s: int = 30 + deadline_s: float | None = None + + def __post_init__(self) -> None: + if self.command is not None and self.timeout_s <= 0: + raise ValueError("probe.timeout_s must be > 0") + if self.deadline_s is not None and self.deadline_s <= 0: + raise ValueError("probe.deadline_s must be > 0") + + +@dataclass(frozen=True) +class DaytonaOperationConfig(DaytonaConfigBase): + """Retry and timeout settings for Daytona operations after create.""" + + retries: int = 3 + retry_delay_s: float = 1.0 + retry_max_delay_s: float = 15.0 + command_retries: int = 0 + command_timeout_margin_s: float = 60.0 + file_timeout_s: int | None = 30 * 60 + close_timeout_s: float | None = 60.0 + + def __post_init__(self) -> None: + if self.retries < 0: + raise ValueError("operations.retries must be >= 0") + if self.retry_delay_s < 0: + raise ValueError("operations.retry_delay_s must be >= 0") + if self.retry_max_delay_s < 0: + raise ValueError("operations.retry_max_delay_s must be >= 0") + if self.command_retries < 0: + raise ValueError("operations.command_retries must be >= 0") + if self.command_timeout_margin_s < 0: + raise ValueError("operations.command_timeout_margin_s must be >= 0") + if self.file_timeout_s is not None and self.file_timeout_s <= 0: + raise ValueError("operations.file_timeout_s must be > 0") + if self.close_timeout_s is not None and self.close_timeout_s <= 0: + raise ValueError("operations.close_timeout_s must be > 0") + + +@dataclass(frozen=True) +class DaytonaBatchConfig(DaytonaConfigBase): + """Client-side batch fanout settings.""" + + concurrency: int = 4 + + def __post_init__(self) -> None: + if self.concurrency < 1: + raise ValueError("batch.concurrency must be >= 1") + + +class DaytonaProvider: + """Provider backed by the Daytona Python SDK.""" + + name = "daytona" + + def __init__( + self, + *, + connection: DaytonaConnectionConfig | Mapping[str, Any] | None = None, + create: DaytonaCreateConfig | Mapping[str, Any] | None = None, + probe: DaytonaProbeConfig | Mapping[str, Any] | None = None, + operations: DaytonaOperationConfig | Mapping[str, Any] | None = None, + batch: DaytonaBatchConfig | Mapping[str, Any] | None = None, + ) -> None: + self._connection = DaytonaConnectionConfig.from_mapping(connection) + self._create = DaytonaCreateConfig.from_mapping(create) + self._probe = DaytonaProbeConfig.from_mapping(probe) + self._operations = DaytonaOperationConfig.from_mapping(operations) + self._batch = DaytonaBatchConfig.from_mapping(batch) + self._connection_supplied_keys = _supplied_config_keys(connection, DaytonaConnectionConfig) + self._daytona: Any | None = None + + def _client(self) -> Any: + if self._daytona is not None: + return self._daytona + AsyncDaytona, DaytonaConfig, _, _, _, _ = _require_daytona_sdk() + kwargs: dict[str, Any] = {} + for key in ( + "api_key", + "jwt_token", + "organization_id", + "api_url", + "server_url", + "target", + "connection_pool_maxsize", + "otel_enabled", + ): + value = getattr(self._connection, key) + if value is not None or key in self._connection_supplied_keys: + kwargs[key] = value + self._daytona = AsyncDaytona() if not kwargs else AsyncDaytona(DaytonaConfig(**kwargs)) + return self._daytona + + async def aclose(self) -> None: + if self._daytona is None: + return + daytona = self._daytona + self._daytona = None + await daytona.close() + + async def _await_operation( + self, + operation_factory: Callable[[], Awaitable[Any]], + *, + operation: str, + sandbox_id: str, + timeout_s: float | None, + retries: int | None = None, + ) -> Any: + AsyncRetrying, retry_if_exception, stop_after_attempt, wait_random_exponential = _require_tenacity() + retry_count = self._operations.retries if retries is None else retries + max_attempts = retry_count + 1 + + retry_policy = AsyncRetrying( + retry=retry_if_exception(_is_retryable_operation_error), + stop=stop_after_attempt(max_attempts), + wait=wait_random_exponential( + multiplier=self._operations.retry_delay_s, + max=self._operations.retry_max_delay_s, + ), + before_sleep=_log_operation_retry, + reraise=True, + ) + async for attempt in retry_policy: + with attempt: + awaitable = operation_factory() + if timeout_s is None or timeout_s == 0: + return await awaitable + try: + return await asyncio.wait_for(awaitable, timeout=timeout_s) + except asyncio.TimeoutError as e: + raise TimeoutError(f"Timed out during Daytona {operation}; sandbox_id={sandbox_id!r}") from e + + raise RuntimeError("Daytona operation retry loop did not run") + + def _base_create_kwargs(self, spec: SandboxSpec) -> dict[str, Any]: + kwargs: dict[str, Any] = {} + if (name := _extension_value(spec, "name")) is not None: + kwargs["name"] = name + if (language := _configured_or_extension(spec, "language", self._create.language)) is not None: + kwargs["language"] = language + if (os_user := _configured_or_extension(spec, "os_user", self._create.os_user)) is not None: + kwargs["os_user"] = os_user + if spec.env: + kwargs["env_vars"] = spec.env + if spec.metadata: + kwargs["labels"] = spec.metadata + configured_values = { + "public": self._create.public, + "auto_stop_interval": self._create.auto_stop_interval, + "auto_archive_interval": self._create.auto_archive_interval, + "auto_delete_interval": self._create.auto_delete_interval, + "ephemeral": self._create.ephemeral, + "network_block_all": self._create.network_block_all, + "network_allow_list": self._create.network_allow_list, + } + for key, configured in configured_values.items(): + value = _configured_or_extension(spec, key, configured) + if value is None: + continue + if key in {"public", "ephemeral", "network_block_all"}: + kwargs[key] = _coerce_bool(f"daytona.{key}", value) + elif key in {"auto_stop_interval", "auto_archive_interval", "auto_delete_interval"}: + kwargs[key] = _coerce_int(f"daytona.{key}", value) + else: + kwargs[key] = str(value) + volumes = _spec_volumes(spec) + if volumes is not None: + kwargs["volumes"] = _to_volume_mounts(volumes) + return kwargs + + def _to_create_params(self, spec: SandboxSpec) -> Any | None: + _, _, ImageParams, SnapshotParams, _, _ = _require_daytona_sdk() + _provider_options(spec) + snapshot_id = _spec_snapshot_id(spec) + if spec.image is not None and snapshot_id is not None: + raise ValueError("Daytona provider does not support both image and snapshot_id") + kwargs = self._base_create_kwargs(spec) + if spec.image is not None: + kwargs["image"] = spec.image + resources = _to_resources(spec.resources) if _resources_requested(spec.resources) else None + if resources is not None: + kwargs["resources"] = resources + return ImageParams(**kwargs) + if snapshot_id is not None: + if _resources_requested(spec.resources): + raise ValueError("Daytona snapshot creation does not support resource overrides") + kwargs["snapshot"] = snapshot_id + return SnapshotParams(**kwargs) + return SnapshotParams(**kwargs) if kwargs else None + + def _with_generated_sandbox_id(self, spec: SandboxSpec) -> SandboxSpec: + if _extension_value(spec, "name") is not None: + return spec + sandbox_id = f"nemo-gym-{uuid.uuid4().hex}" + metadata = dict(spec.metadata) + metadata.setdefault(SANDBOX_ID_LABEL, sandbox_id) + provider_options = dict(spec.provider_options) + extensions = _spec_extensions(spec) + extensions[f"{DAYTONA_EXTENSION_PREFIX}name"] = sandbox_id + provider_options[PROVIDER_OPTION_EXTENSIONS] = extensions + return replace(spec, metadata=metadata, provider_options=provider_options) + + async def _verify_created_handle(self, handle: SandboxHandle) -> None: + if self._probe.command is None: + return + deadline_s = self._probe.deadline_s or float(self._probe.timeout_s) + loop = asyncio.get_running_loop() + deadline = loop.time() + deadline_s + last_exception: BaseException | None = None + while loop.time() < deadline: + try: + result = await self._exec(handle, self._probe.command, timeout_s=self._probe.timeout_s, retries=0) + except asyncio.CancelledError: + raise + except Exception as e: + last_exception = e + await asyncio.sleep(min(1.0, max(deadline - loop.time(), 0.0))) + continue + stdout = result.stdout or "" + if result.return_code == 0 and ( + self._probe.expected_stdout is None or self._probe.expected_stdout in stdout + ): + return + last_exception = DaytonaCreateVerificationError( + f"Daytona create probe failed; sandbox_id={handle.sandbox_id!r}, " + f"return_code={result.return_code}, stdout={stdout[:200]!r}, stderr={(result.stderr or '')[:200]!r}" + ) + await asyncio.sleep(min(1.0, max(deadline - loop.time(), 0.0))) + raise DaytonaCreateVerificationError( + f"Daytona sandbox failed create probe before deadline; sandbox_id={handle.sandbox_id!r}" + ) from last_exception + + async def _create_once(self, spec: SandboxSpec) -> SandboxHandle: + params = self._to_create_params(spec) + timeout_s = float(spec.ready_timeout_s if spec.ready_timeout_s is not None else self._create.timeout_s) + try: + sandbox = await self._await_operation( + lambda: self._client().create(params, timeout=timeout_s), + operation="create", + sandbox_id="", + timeout_s=timeout_s + 5.0 if timeout_s > 0 else None, + retries=0, + ) + except TimeoutError as e: + raise DaytonaCreateTimeoutError( + f"Timed out creating Daytona sandbox after {timeout_s:g}s; image={spec.image!r}" + ) from e + handle = SandboxHandle(sandbox_id=str(sandbox.id), provider_name=self.name, raw=sandbox) + try: + await self._verify_created_handle(handle) + except Exception: + try: + await self.close(handle, delete=True) + except Exception as cleanup_error: + LOGGER.warning( + "Failed to delete Daytona sandbox after create probe failure; sandbox_id=%s; error=%r", + handle.sandbox_id, + cleanup_error, + ) + raise + return handle + + async def _create_with_retries(self, spec: SandboxSpec) -> SandboxHandle: + AsyncRetrying, retry_if_exception, stop_after_attempt, wait_random_exponential = _require_tenacity() + retry_policy = AsyncRetrying( + retry=retry_if_exception(_is_retryable_create_error), + stop=stop_after_attempt(self._create.retries + 1), + wait=wait_random_exponential( + multiplier=self._create.retry_delay_s, + max=self._create.retry_max_delay_s, + ), + before_sleep=_log_create_retry, + reraise=True, + ) + async for attempt in retry_policy: + with attempt: + return await self._create_once(self._with_generated_sandbox_id(spec)) + + raise RuntimeError("Daytona create retry loop did not run") + + async def create(self, spec: SandboxSpec) -> SandboxHandle: + return await self._create_with_retries(_normalize_spec(spec)) + + async def create_batch(self, spec: SandboxSpec, count: int, *, allow_partial: bool = False) -> list[SandboxHandle]: + if count < 1: + raise ValueError("count must be >= 1") + semaphore = asyncio.Semaphore(self._batch.concurrency) + + async def _create_one() -> SandboxHandle: + async with semaphore: + return await self.create(spec) + + results = await asyncio.gather(*(_create_one() for _ in range(count)), return_exceptions=True) + errors = [result for result in results if isinstance(result, Exception)] + handles = [result for result in results if isinstance(result, SandboxHandle)] + if not errors: + return handles + if allow_partial: + return handles + cleanup_errors: list[tuple[str, BaseException]] = [] + for handle in handles: + try: + await self.close(handle, delete=True) + except Exception as cleanup_error: + cleanup_errors.append((handle.sandbox_id, cleanup_error)) + cleanup_suffix = "" + if cleanup_errors: + cleanup_suffix = "; cleanup_errors=" + ", ".join( + f"{sandbox_id}: {type(error).__name__}: {error}" for sandbox_id, error in cleanup_errors + ) + raise DaytonaCreateError( + "One or more Daytona sandboxes failed during batch create; " + f"failed={len(errors)}, requested={count}{cleanup_suffix}" + ) from errors[0] + + async def connect(self, sandbox_id: str) -> SandboxHandle: + sandbox = await self._client().get(sandbox_id) + return SandboxHandle(sandbox_id=str(sandbox.id), provider_name=self.name, raw=sandbox) + + async def status(self, handle: SandboxHandle) -> SandboxStatus: + refresh_data = getattr(handle.raw, "refresh_data", None) + if refresh_data is not None: + await self._await_operation( + refresh_data, + operation="refresh_data", + sandbox_id=handle.sandbox_id, + timeout_s=self._operations.close_timeout_s, + ) + return _to_sandbox_status(getattr(handle.raw, "state", None)) + + status_value = getattr(handle.raw, "status", None) + if status_value is not None: + state = getattr(status_value, "state", status_value) + return _to_sandbox_status(state) + + get_info = getattr(handle.raw, "get_info", None) + if get_info is None: + return SandboxStatus.UNKNOWN + info = await self._await_operation( + get_info, + operation="get_info", + sandbox_id=handle.sandbox_id, + timeout_s=self._operations.close_timeout_s, + ) + info_status = getattr(info, "status", None) + return _to_sandbox_status(getattr(info_status, "state", info_status)) + + def _command_retry_count(self) -> int: + return self._operations.command_retries + + @staticmethod + def _effective_command(command: str, user: str | int | None) -> str: + if user is None or user == "root" or user == 0: + return command + raise NotImplementedError( + "Daytona provider does not support per-command user switching. " + "Configure create.os_user for sandbox-level user selection until Daytona exposes native per-command users." + ) + + async def _exec( + self, + handle: SandboxHandle, + command: str, + *, + cwd: str | None = None, + env: dict[str, str] | None = None, + timeout_s: int | None = None, + user: str | int | None = None, + retries: int | None = None, + ) -> SandboxExecResult: + try: + response = await self._await_operation( + lambda: handle.raw.process.exec( + self._effective_command(command, user), + cwd=cwd, + env=env, + timeout=timeout_s, + ), + operation="process.exec", + sandbox_id=handle.sandbox_id, + timeout_s=float(timeout_s) + self._operations.command_timeout_margin_s + if timeout_s is not None + else None, + retries=self._command_retry_count() if retries is None else retries, + ) + except asyncio.CancelledError: + raise + except Exception as e: + if not _is_daytona_command_exec_error(e): + raise + LOGGER.warning( + "Daytona process.exec failed without a process result; sandbox_id=%s; error=%r", + handle.sandbox_id, + e, + ) + return _daytona_exec_error_result(e) + artifacts = getattr(response, "artifacts", None) + stdout = getattr(response, "result", None) + if stdout is None and artifacts is not None: + stdout = getattr(artifacts, "stdout", None) + stderr = getattr(response, "stderr", None) + if stderr is None and artifacts is not None: + stderr = getattr(artifacts, "stderr", None) + return_code = getattr(response, "exit_code", None) + return SandboxExecResult(stdout=stdout, stderr=stderr, return_code=0 if return_code is None else return_code) + + async def exec( + self, + handle: SandboxHandle, + command: str, + *, + cwd: str | None = None, + env: dict[str, str] | None = None, + timeout_s: int | None = None, + user: str | int | None = None, + ) -> SandboxExecResult: + return await self._exec(handle, command, cwd=cwd, env=env, timeout_s=timeout_s, user=user) + + async def write_file(self, handle: SandboxHandle, target_path: str, data: str | bytes) -> None: + payload = data.encode() if isinstance(data, str) else data + timeout_s = self._operations.file_timeout_s + await self._await_operation( + lambda: handle.raw.fs.upload_file(payload, target_path) + if timeout_s is None + else handle.raw.fs.upload_file(payload, target_path, timeout=timeout_s), + operation=f"upload_file({target_path})", + sandbox_id=handle.sandbox_id, + timeout_s=float(timeout_s) if timeout_s is not None else None, + ) + + async def read_file(self, handle: SandboxHandle, source_path: str) -> bytes: + timeout_s = self._operations.file_timeout_s + result = await self._await_operation( + lambda: handle.raw.fs.download_file(source_path) + if timeout_s is None + else handle.raw.fs.download_file(source_path, timeout_s), + operation=f"download_file({source_path})", + sandbox_id=handle.sandbox_id, + timeout_s=float(timeout_s) if timeout_s is not None else None, + ) + return result.encode() if isinstance(result, str) else result + + async def upload_file(self, handle: SandboxHandle, source_path: Path, target_path: str) -> None: + timeout_s = self._operations.file_timeout_s + await self._await_operation( + lambda: handle.raw.fs.upload_file(str(source_path), target_path) + if timeout_s is None + else handle.raw.fs.upload_file(str(source_path), target_path, timeout=timeout_s), + operation=f"upload_file({target_path})", + sandbox_id=handle.sandbox_id, + timeout_s=float(timeout_s) if timeout_s is not None else None, + ) + + async def download_file(self, handle: SandboxHandle, source_path: str, target_path: Path) -> None: + target_path.parent.mkdir(parents=True, exist_ok=True) + timeout_s = self._operations.file_timeout_s + await self._await_operation( + lambda: handle.raw.fs.download_file(source_path, str(target_path)) + if timeout_s is None + else handle.raw.fs.download_file(source_path, str(target_path), timeout_s), + operation=f"download_file({source_path})", + sandbox_id=handle.sandbox_id, + timeout_s=float(timeout_s) if timeout_s is not None else None, + ) + + async def close(self, handle: SandboxHandle, *, delete: bool = True) -> None: + if not delete: + LOGGER.warning( + "Retaining Daytona sandbox %r because close(delete=False) was requested; " + "the remote sandbox remains active until Daytona lifecycle policies stop or delete it.", + handle.sandbox_id, + ) + return + try: + await self._await_operation( + lambda: self._client().delete(handle.raw, timeout=self._operations.close_timeout_s or 0), + operation="delete", + sandbox_id=handle.sandbox_id, + timeout_s=self._operations.close_timeout_s, + ) + except Exception as e: + if _is_missing_sandbox_delete_error(e): + LOGGER.info("Daytona sandbox %r was already deleted during close", handle.sandbox_id) + return + raise diff --git a/nemo_gym/sandbox/providers/registry.py b/nemo_gym/sandbox/providers/registry.py index 3958ee5e26..98a81a7658 100644 --- a/nemo_gym/sandbox/providers/registry.py +++ b/nemo_gym/sandbox/providers/registry.py @@ -125,6 +125,12 @@ def list_providers() -> list[str]: return sorted({*_PROVIDER_REGISTRY, *_BUILTIN_PROVIDER_LOADERS, *_entry_point_loaders()}) +def _load_daytona_provider() -> ProviderClass: + from nemo_gym.sandbox.providers.daytona import DaytonaProvider + + return DaytonaProvider + + def _load_opensandbox_provider() -> ProviderClass: from nemo_gym.sandbox.providers.opensandbox import OpenSandboxProvider @@ -150,7 +156,7 @@ def _load_ecs_fargate_provider() -> ProviderClass: _BUILTIN_PROVIDER_LOADERS["apptainer"] = _load_apptainer_provider +_BUILTIN_PROVIDER_LOADERS["daytona"] = _load_daytona_provider _BUILTIN_PROVIDER_LOADERS["docker"] = _load_docker_provider -_BUILTIN_PROVIDER_LOADERS["opensandbox"] = _load_opensandbox_provider -_BUILTIN_PROVIDER_LOADERS["apptainer"] = _load_apptainer_provider _BUILTIN_PROVIDER_LOADERS["ecs_fargate"] = _load_ecs_fargate_provider +_BUILTIN_PROVIDER_LOADERS["opensandbox"] = _load_opensandbox_provider diff --git a/pyproject.toml b/pyproject.toml index e9e074dbca..fcafe23f1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -249,6 +249,10 @@ sandbox = [ # License: Apache 2.0 "opensandbox>=0.1.9", + # Daytona SDK: used by the Daytona sandbox provider for create/exec/delete and file operations. + # License: Apache 2.0 https://pypi.org/project/daytona/ + "daytona>=0.179.0", + # boto3: AWS SDK used by the ECS Fargate sandbox provider for ECS/EC2/ECR/ # CodeBuild/S3/SSM/Secrets Manager calls. # Updated: Tue Jun 03, 2026 with boto3>=1.34 diff --git a/responses_api_agents/mini_swe_agent_2/configs/mini_swe_agent_2.yaml b/responses_api_agents/mini_swe_agent_2/configs/mini_swe_agent_2.yaml index 4c3b6ce505..00d61d4489 100644 --- a/responses_api_agents/mini_swe_agent_2/configs/mini_swe_agent_2.yaml +++ b/responses_api_agents/mini_swe_agent_2/configs/mini_swe_agent_2.yaml @@ -5,7 +5,10 @@ # # AGENT=responses_api_agents/mini_swe_agent_2/configs/mini_swe_agent_2.yaml # MODEL=responses_api_models/vllm_model/configs/vllm_model.yaml -# ng_run "+config_paths=[$AGENT, nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml, $MODEL]" +# gym env start \ +# --config $AGENT \ +# --config nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml \ +# --config $MODEL mini_swe_agent_2: responses_api_agents: mini_swe_agent_2: @@ -30,10 +33,7 @@ mini_swe_agent_2: # 30 GiB works on every provider: within Fargate's 21-200 GiB ephemeral range # (an explicit 20 is rejected there) and fine as an ephemeral request elsewhere. disk_gib: 30 - provider_options: - platform: - os: linux - arch: amd64 + provider_options: {} metadata: benchmark: swebench-verified harness: mini-swe-agent diff --git a/tests/unit_tests/test_daytona_provider.py b/tests/unit_tests/test_daytona_provider.py new file mode 100644 index 0000000000..0a823bc3c6 --- /dev/null +++ b/tests/unit_tests/test_daytona_provider.py @@ -0,0 +1,925 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import builtins +import importlib.util +import logging +import sys +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +from nemo_gym.sandbox.providers.base import SandboxHandle, SandboxSpec, SandboxStatus +from nemo_gym.sandbox.providers.daytona import provider as daytona_provider + + +pytestmark = [ + pytest.mark.anyio, + pytest.mark.skipif( + importlib.util.find_spec("tenacity") is None, + reason="tenacity optional sandbox dependency is not installed", + ), +] + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +class FakeParams: + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + + +class FakeImageParams(FakeParams): + pass + + +class FakeSnapshotParams(FakeParams): + pass + + +class FakeResources(FakeParams): + pass + + +class FakeVolumeMount(FakeParams): + pass + + +class FakeDaytonaConfig(FakeParams): + pass + + +class FakeAsyncDaytona: + instances: list["FakeAsyncDaytona"] = [] + + def __init__(self, config: FakeDaytonaConfig | None = None) -> None: + self.config = config + self.closed = False + FakeAsyncDaytona.instances.append(self) + + async def close(self) -> None: + self.closed = True + + +@pytest.fixture +def fake_daytona_sdk(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + daytona_provider, + "_require_daytona_sdk", + lambda: ( + FakeAsyncDaytona, + FakeDaytonaConfig, + FakeImageParams, + FakeSnapshotParams, + FakeResources, + FakeVolumeMount, + ), + ) + + +@pytest.fixture +def fake_daytona_module(monkeypatch: pytest.MonkeyPatch) -> None: + class DaytonaError(Exception): + pass + + module = SimpleNamespace( + AsyncDaytona=FakeAsyncDaytona, + CreateSandboxFromImageParams=FakeImageParams, + CreateSandboxFromSnapshotParams=FakeSnapshotParams, + DaytonaConfig=FakeDaytonaConfig, + Resources=FakeResources, + VolumeMount=FakeVolumeMount, + DaytonaAuthenticationError=type("DaytonaAuthenticationError", (DaytonaError,), {}), + DaytonaAuthorizationError=type("DaytonaAuthorizationError", (DaytonaError,), {}), + DaytonaConflictError=type("DaytonaConflictError", (DaytonaError,), {}), + DaytonaConnectionError=type("DaytonaConnectionError", (DaytonaError,), {}), + DaytonaError=DaytonaError, + DaytonaNotFoundError=type("DaytonaNotFoundError", (DaytonaError,), {}), + DaytonaRateLimitError=type("DaytonaRateLimitError", (DaytonaError,), {}), + DaytonaTimeoutError=type("DaytonaTimeoutError", (DaytonaError,), {}), + DaytonaValidationError=type("DaytonaValidationError", (DaytonaError,), {}), + ) + monkeypatch.setitem(sys.modules, "daytona", module) + + +def test_sdk_import_helpers_and_retry_classification( + fake_daytona_module: None, + monkeypatch: pytest.MonkeyPatch, +) -> None: + assert len(daytona_provider._require_daytona_sdk()) == 6 + assert len(daytona_provider._require_tenacity()) == 4 + error_types = daytona_provider._daytona_error_types() + assert set(error_types) == { + "authentication", + "authorization", + "conflict", + "connection", + "error", + "not_found", + "rate_limit", + "timeout", + "validation", + } + + class StatusCodeError(Exception): + status_code = 429 + + assert daytona_provider._exception_status_code(StatusCodeError("busy")) == 429 + assert daytona_provider._exception_status_code(RuntimeError("HTTP status code: 503")) == 503 + assert daytona_provider._exception_status_code(RuntimeError("plain")) is None + assert daytona_provider._has_retryable_error_marker(RuntimeError("gateway timeout")) is True + assert daytona_provider._is_retryable_create_error(StatusCodeError("busy")) is True + not_retryable_status = RuntimeError("bad request") + not_retryable_status.status_code = 400 + assert daytona_provider._is_retryable_create_error(not_retryable_status) is False + + assert daytona_provider._is_retryable_create_error(daytona_provider.DaytonaCreateVerificationError("probe")) + assert daytona_provider._is_retryable_create_error(daytona_provider.DaytonaCreateTimeoutError("timeout")) is False + assert daytona_provider._is_retryable_create_error(daytona_provider.DaytonaCreateError("create failed")) + assert daytona_provider._is_retryable_create_error(TimeoutError("ambiguous timeout")) is False + assert daytona_provider._is_retryable_create_error(ConnectionError("reset")) is True + assert daytona_provider._is_retryable_create_error(error_types["validation"]("bad")) is False + assert daytona_provider._is_retryable_create_error(error_types["conflict"]("busy")) is True + assert daytona_provider._is_retryable_create_error(error_types["timeout"]("ambiguous")) is False + + retryable_api_error = error_types["error"]("server failed") + retryable_api_error.status_code = 503 + assert daytona_provider._is_retryable_create_error(retryable_api_error) is True + nonretryable_api_error = error_types["error"]("bad request") + nonretryable_api_error.status_code = 400 + assert daytona_provider._is_retryable_create_error(nonretryable_api_error) is False + assert daytona_provider._is_retryable_create_error(RuntimeError("rate limit")) is True + + wrapped = RuntimeError("wrapper") + wrapped.__cause__ = ConnectionError("connection reset") + assert daytona_provider._is_retryable_operation_error(wrapped) is True + assert daytona_provider._is_retryable_operation_error(TimeoutError("command timeout")) is False + assert daytona_provider._is_retryable_operation_error(error_types["rate_limit"]("slow down")) is True + assert daytona_provider._is_retryable_operation_error(error_types["not_found"]("gone")) is False + + assert daytona_provider._is_missing_sandbox_delete_error(RuntimeError("sandbox abc not found")) is True + missing = RuntimeError("missing") + missing.status_code = 404 + assert daytona_provider._is_missing_sandbox_delete_error(missing) is True + assert daytona_provider._is_missing_sandbox_delete_error(RuntimeError("delete failed")) is False + + real_import = builtins.__import__ + + def fake_import( + name: str, + globals_: dict[str, Any] | None = None, + locals_: dict[str, Any] | None = None, + fromlist: tuple[str, ...] = (), + level: int = 0, + ) -> Any: + if name == "daytona": + raise ImportError("daytona missing") + if name == "tenacity": + raise ModuleNotFoundError("tenacity missing") + return real_import(name, globals_, locals_, fromlist, level) + + monkeypatch.delitem(sys.modules, "daytona", raising=False) + monkeypatch.delitem(sys.modules, "tenacity", raising=False) + monkeypatch.setattr(builtins, "__import__", fake_import) + with pytest.raises(ModuleNotFoundError, match="Daytona SDK is required"): + daytona_provider._require_daytona_sdk() + with pytest.raises(ModuleNotFoundError, match="tenacity is required"): + daytona_provider._require_tenacity() + assert daytona_provider._daytona_error_types() == {} + + +def test_config_unknown_keys_fail_fast() -> None: + with pytest.raises(ValueError, match="Unsupported Daytona DaytonaCreateConfig settings: typo"): + daytona_provider.DaytonaProvider(create={"typo": "value"}) + + +def test_conversion_helpers_and_config_validation(fake_daytona_sdk: None) -> None: + create_config = daytona_provider.DaytonaCreateConfig(timeout_s=1) + assert daytona_provider.DaytonaCreateConfig.from_mapping(create_config) is create_config + with pytest.raises(TypeError, match="must be a mapping"): + daytona_provider.DaytonaCreateConfig.from_mapping(object()) + + with pytest.raises(ValueError, match="must be a boolean"): + daytona_provider._coerce_bool("daytona.public", "maybe") + assert daytona_provider._coerce_bool("daytona.public", "yes") is True + assert daytona_provider._coerce_bool("daytona.public", "0") is False + with pytest.raises(ValueError, match="must be an integer"): + daytona_provider._coerce_int("daytona.auto_stop_interval", True) + assert daytona_provider._coerce_int("daytona.auto_stop_interval", 2.0) == 2 + assert daytona_provider._coerce_int("daytona.auto_stop_interval", "3") == 3 + + assert daytona_provider._quantity_to_int("cpu", 2) == 2 + assert daytona_provider._quantity_to_int("cpu", 2.0) == 2 + assert daytona_provider._quantity_to_int("cpu", "500m") == 1 + assert daytona_provider._quantity_to_int("memory", "2048Mi") == 2 + assert daytona_provider._quantity_to_int("disk", "1.1Gi") == 2 + with pytest.raises(ValueError, match="integer-like"): + daytona_provider._quantity_to_int("cpu", True) + with pytest.raises(ValueError, match="integer-like"): + daytona_provider._quantity_to_int("cpu", "abc") + with pytest.raises(ValueError, match="unsupported unit"): + daytona_provider._quantity_to_int("cpu", "1Ti") + + spec = SandboxSpec( + provider_options={ + "extensions": {"daytona.name": "sandbox-name"}, + "volumes": [{"name": "workspace"}], + "snapshot_id": "snapshot-1", + } + ) + assert daytona_provider._extension_value(spec, "name") == "sandbox-name" + assert daytona_provider._spec_snapshot_id(spec) == "snapshot-1" + assert daytona_provider._spec_volumes(spec) == [{"name": "workspace"}] + assert daytona_provider._to_volume_mounts([{"name": "workspace"}])[0].kwargs == {"name": "workspace"} + assert daytona_provider.DaytonaProviderOptions.from_mapping(None) == daytona_provider.DaytonaProviderOptions() + with pytest.raises(TypeError, match="extensions"): + daytona_provider._spec_extensions(SandboxSpec(provider_options={"extensions": []})) + with pytest.raises(TypeError, match="volumes"): + daytona_provider._spec_volumes(SandboxSpec(provider_options={"volumes": {}})) + with pytest.raises(TypeError, match="snapshot_id"): + daytona_provider.DaytonaProviderOptions.from_mapping({"snapshot_id": 123}) + with pytest.raises(TypeError, match="provider_options"): + daytona_provider.DaytonaProviderOptions.from_mapping([]) + with pytest.raises(ValueError, match="Unknown Daytona provider option.*platform"): + daytona_provider.DaytonaProviderOptions.from_mapping({"platform": {"os": "linux"}}) + + assert daytona_provider._to_resources({}) is None + assert daytona_provider._to_resources({"gpu": "1"}).kwargs == {"gpu": 1} + assert daytona_provider._to_resources(SandboxSpec(resources={"memory_mib": 2048}).resources).kwargs == { + "memory": 2 + } + with pytest.raises(ValueError, match="gpu_type"): + daytona_provider._to_resources(SandboxSpec(resources={"gpu_type": "a100"}).resources) + assert daytona_provider._to_sandbox_status("creating") == SandboxStatus.STARTING + assert daytona_provider._to_sandbox_status("deleted") == SandboxStatus.STOPPED + assert daytona_provider._to_sandbox_status("failed") == SandboxStatus.ERROR + assert daytona_provider._to_sandbox_status("mystery") == SandboxStatus.UNKNOWN + + invalid_provider_kwargs = [ + {"connection": {"connection_pool_maxsize": 0}}, + {"create": {"timeout_s": -1}}, + {"create": {"retries": -1}}, + {"create": {"retry_delay_s": -1}}, + {"create": {"retry_max_delay_s": -1}}, + {"probe": {"timeout_s": 0}}, + {"probe": {"deadline_s": 0}}, + {"operations": {"retries": -1}}, + {"operations": {"retry_delay_s": -1}}, + {"operations": {"retry_max_delay_s": -1}}, + {"operations": {"command_retries": -1}}, + {"operations": {"command_timeout_margin_s": -1}}, + {"operations": {"file_timeout_s": 0}}, + {"operations": {"close_timeout_s": 0}}, + {"batch": {"concurrency": 0}}, + ] + for kwargs in invalid_provider_kwargs: + with pytest.raises(ValueError): + daytona_provider.DaytonaProvider(**kwargs) + + with pytest.raises(ValueError, match="Unsupported Daytona DaytonaConnectionConfig settings: typo"): + daytona_provider.DaytonaProvider(connection={"typo": True}) + + +def test_snapshot_creation_rejects_ignored_resources(fake_daytona_sdk: None) -> None: + provider = daytona_provider.DaytonaProvider(probe={"command": None}) + spec = SandboxSpec( + provider_options={"snapshot_id": "snapshot-1"}, + resources={"cpu": 2}, + ) + + with pytest.raises(ValueError, match="snapshot creation does not support resource overrides"): + provider._to_create_params(spec) + + +def test_image_creation_keeps_resource_overrides(fake_daytona_sdk: None) -> None: + provider = daytona_provider.DaytonaProvider(probe={"command": None}) + params = provider._to_create_params( + SandboxSpec( + image="python:3.12", + resources={"cpu": 2, "memory_mib": 2048, "disk_gib": 9}, + ) + ) + + assert isinstance(params, FakeImageParams) + assert params.kwargs["resources"].kwargs == {"cpu": 2, "memory": 2, "disk": 9} + + +async def test_client_lifecycle_create_kwargs_and_status_fallbacks(fake_daytona_sdk: None) -> None: + FakeAsyncDaytona.instances.clear() + provider = daytona_provider.DaytonaProvider( + connection={ + "api_key": "key", # pragma: allowlist secret + "jwt_token": "jwt", + "organization_id": "org", + "api_url": "https://api.example", + "server_url": "https://server.example", + "target": "us", + "connection_pool_maxsize": 8, + "otel_enabled": False, + }, + create={ + "language": "python", + "os_user": "agent", + "public": False, + "auto_stop_interval": 10, + "auto_archive_interval": 20, + "auto_delete_interval": 30, + "ephemeral": True, + "network_block_all": False, + "network_allow_list": "github.com", + }, + ) + client = provider._client() + assert provider._client() is client + assert client.config.kwargs == { + "api_key": "key", # pragma: allowlist secret + "jwt_token": "jwt", + "organization_id": "org", + "api_url": "https://api.example", + "server_url": "https://server.example", + "target": "us", + "connection_pool_maxsize": 8, + "otel_enabled": False, + } + await provider.aclose() + assert client.closed is True + assert await provider.aclose() is None + + FakeAsyncDaytona.instances.clear() + unlimited_provider = daytona_provider.DaytonaProvider(connection={"connection_pool_maxsize": None}) + unlimited_client = unlimited_provider._client() + assert unlimited_client.config.kwargs == {"connection_pool_maxsize": None} + await unlimited_provider.aclose() + + kwargs = provider._base_create_kwargs( + SandboxSpec( + env={"A": "B"}, + metadata={"label": "value"}, + provider_options={ + "extensions": { + "daytona.name": "sandbox-name", + "daytona.language": "go", + "daytona.os_user": "root", + "daytona.public": "true", + "daytona.auto_stop_interval": "11", + "daytona.auto_archive_interval": "22", + "daytona.auto_delete_interval": "33", + "daytona.ephemeral": "false", + "daytona.network_block_all": "true", + "daytona.network_allow_list": "example.com", + }, + "volumes": [{"name": "workspace"}], + }, + ) + ) + assert {key: value for key, value in kwargs.items() if key != "volumes"} == { + "name": "sandbox-name", + "language": "go", + "os_user": "root", + "env_vars": {"A": "B"}, + "labels": {"label": "value"}, + "public": True, + "auto_stop_interval": 11, + "auto_archive_interval": 22, + "auto_delete_interval": 33, + "ephemeral": False, + "network_block_all": True, + "network_allow_list": "example.com", + } + assert kwargs["volumes"][0].kwargs == {"name": "workspace"} + + with pytest.raises(ValueError, match="both image and snapshot_id"): + provider._to_create_params(SandboxSpec(image="image", provider_options={"snapshot_id": "snapshot"})) + assert isinstance( + provider._to_create_params(SandboxSpec(provider_options={"snapshot_id": "snapshot"})), FakeSnapshotParams + ) + assert daytona_provider.DaytonaProvider()._to_create_params(SandboxSpec()) is None + + status_handle = SandboxHandle( + sandbox_id="sandbox-status", + provider_name="daytona", + raw=SimpleNamespace(status=SimpleNamespace(state="terminated")), + ) + assert await provider.status(status_handle) == SandboxStatus.STOPPED + + class InfoRaw: + async def get_info(self) -> Any: + return SimpleNamespace(status=SimpleNamespace(state="failed")) + + assert await provider.status(SandboxHandle(sandbox_id="sandbox-info", provider_name="daytona", raw=InfoRaw())) == ( + SandboxStatus.ERROR + ) + assert await provider.status(SandboxHandle(sandbox_id="sandbox-bare", provider_name="daytona", raw=object())) == ( + SandboxStatus.UNKNOWN + ) + + +async def test_status_uses_daytona_refresh_data() -> None: + class FakeRaw: + def __init__(self) -> None: + self.state = "creating" + self.refresh_calls = 0 + + async def refresh_data(self) -> None: + self.refresh_calls += 1 + self.state = "started" + + provider = daytona_provider.DaytonaProvider(operations={"retries": 0}) + raw = FakeRaw() + status = await provider.status(SandboxHandle(sandbox_id="sandbox-1", provider_name="daytona", raw=raw)) + + assert status == SandboxStatus.RUNNING + assert raw.refresh_calls == 1 + + +async def test_await_operation_retry_and_timeout_paths(monkeypatch: pytest.MonkeyPatch) -> None: + provider = daytona_provider.DaytonaProvider(operations={"retries": 1, "retry_delay_s": 0, "retry_max_delay_s": 0}) + assert ( + await provider._await_operation( + lambda: _return_value("ok"), + operation="op", + sandbox_id="sandbox-1", + timeout_s=None, + retries=0, + ) + == "ok" + ) + assert ( + await provider._await_operation( + lambda: _return_value("ok"), + operation="op", + sandbox_id="sandbox-1", + timeout_s=0, + retries=0, + ) + == "ok" + ) + + async def never() -> None: + await asyncio.get_running_loop().create_future() + + with pytest.raises(TimeoutError, match="Timed out during Daytona op"): + await provider._await_operation( + never, + operation="op", + sandbox_id="sandbox-1", + timeout_s=0.01, + retries=0, + ) + + sleeps: list[float] = [] + + async def record_sleep(seconds: float) -> None: + sleeps.append(seconds) + + monkeypatch.setattr(daytona_provider.asyncio, "sleep", record_sleep) + attempts = 0 + + async def retry_then_succeed() -> str: + nonlocal attempts + attempts += 1 + if attempts == 1: + raise ConnectionError("temporary") + return "ok" + + assert ( + await provider._await_operation( + retry_then_succeed, + operation="exec", + sandbox_id="sandbox-1", + timeout_s=None, + ) + == "ok" + ) + assert attempts == 2 + assert sleeps == [0] + + with pytest.raises(ValueError, match="bad"): + await provider._await_operation( + lambda: _raise(ValueError("bad")), + operation="exec", + sandbox_id="sandbox-1", + timeout_s=None, + ) + + +async def test_exec_maps_stderr_only_output() -> None: + class FakeProcess: + async def exec(self, *_args: Any, **_kwargs: Any) -> Any: + return SimpleNamespace( + result=None, + artifacts=SimpleNamespace(stdout=None, stderr="err\n"), + exit_code=3, + ) + + raw = SimpleNamespace(process=FakeProcess()) + provider = daytona_provider.DaytonaProvider(operations={"retries": 0}) + result = await provider.exec(SandboxHandle(sandbox_id="sandbox-1", provider_name="daytona", raw=raw), "cmd") + + assert result.stdout is None + assert result.stderr == "err\n" + assert result.return_code == 3 + + +async def test_exec_maps_daytona_command_error_to_sandbox_result(fake_daytona_module: None) -> None: + error_types = daytona_provider._daytona_error_types() + + class FakeProcess: + async def exec(self, *_args: Any, **_kwargs: Any) -> Any: + raise error_types["error"]("Failed to execute command:") + + raw = SimpleNamespace(process=FakeProcess()) + provider = daytona_provider.DaytonaProvider(operations={"retries": 0}) + result = await provider.exec(SandboxHandle(sandbox_id="sandbox-1", provider_name="daytona", raw=raw), "cmd") + + assert result.stdout is None + assert result.return_code == 124 + assert result.error_type == "timeout" + assert result.stderr is not None + assert "HTTP request timed out before Daytona returned a response" in result.stderr + assert "The command may not have run" in result.stderr + + +async def test_exec_maps_daytona_command_timeout_to_timeout_result(fake_daytona_module: None) -> None: + error_types = daytona_provider._daytona_error_types() + + class FakeProcess: + async def exec(self, *_args: Any, **_kwargs: Any) -> Any: + raise error_types["error"]("Failed to execute command: request timeout: command execution timeout") + + raw = SimpleNamespace(process=FakeProcess()) + provider = daytona_provider.DaytonaProvider(operations={"retries": 0}) + result = await provider.exec(SandboxHandle(sandbox_id="sandbox-1", provider_name="daytona", raw=raw), "cmd") + + assert result.return_code == 124 + assert result.error_type == "timeout" + assert result.stderr is not None + assert "command execution timeout" in result.stderr + + +async def test_exec_retries_empty_daytona_command_error_when_enabled( + fake_daytona_module: None, + monkeypatch: pytest.MonkeyPatch, +) -> None: + error_types = daytona_provider._daytona_error_types() + sleeps: list[float] = [] + + async def record_sleep(seconds: float) -> None: + sleeps.append(seconds) + + class FakeProcess: + def __init__(self) -> None: + self.calls = 0 + + async def exec(self, *_args: Any, **_kwargs: Any) -> Any: + self.calls += 1 + if self.calls == 1: + raise error_types["error"]("Failed to execute command:") + return SimpleNamespace(result="ok", exit_code=0) + + monkeypatch.setattr(daytona_provider.asyncio, "sleep", record_sleep) + process = FakeProcess() + raw = SimpleNamespace(process=process) + provider = daytona_provider.DaytonaProvider( + operations={ + "retries": 0, + "retry_delay_s": 0, + "retry_max_delay_s": 0, + "command_retries": 1, + } + ) + + result = await provider.exec(SandboxHandle(sandbox_id="sandbox-1", provider_name="daytona", raw=raw), "cmd") + + assert result.stdout == "ok" + assert result.return_code == 0 + assert process.calls == 2 + assert sleeps == [0] + + +async def test_probe_create_connect_file_and_close_paths( + fake_daytona_sdk: None, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + class FakeSandbox: + def __init__(self, sandbox_id: str) -> None: + self.id = sandbox_id + self.process = SimpleNamespace(exec=lambda *_args, **_kwargs: _return_value(SimpleNamespace(result="ok"))) + + class FakeClient: + def __init__(self) -> None: + self.create_calls = 0 + self.deleted: list[str] = [] + + async def create(self, _params: Any, *, timeout: float) -> FakeSandbox: + self.create_calls += 1 + assert timeout == 2 + return FakeSandbox("sandbox-created") + + async def get(self, sandbox_id: str) -> FakeSandbox: + return FakeSandbox(sandbox_id) + + async def delete(self, raw: Any, *, timeout: float) -> None: + del timeout + if raw == "missing": + raise RuntimeError("sandbox missing not found") + if raw == "failed": + raise RuntimeError("delete failed") + self.deleted.append(raw.id) + + client = FakeClient() + provider = daytona_provider.DaytonaProvider(create={"timeout_s": 2}, probe={"command": None}) + monkeypatch.setattr(provider, "_client", lambda: client) + + handle = await provider._create_once(SandboxSpec(image="python:3.12")) + assert handle.sandbox_id == "sandbox-created" + assert client.create_calls == 1 + connected = await provider.connect("sandbox-existing") + assert connected.sandbox_id == "sandbox-existing" + + provider = daytona_provider.DaytonaProvider( + probe={"command": "probe", "expected_stdout": None, "timeout_s": 1, "deadline_s": 0.01} + ) + good_probe_calls = 0 + + async def good_probe(*_args: Any, **_kwargs: Any) -> daytona_provider.SandboxExecResult: + nonlocal good_probe_calls + good_probe_calls += 1 + return daytona_provider.SandboxExecResult(stdout="ready", stderr=None, return_code=0) + + monkeypatch.setattr(provider, "_exec", good_probe) + await provider._verify_created_handle(handle) + assert good_probe_calls == 1 + + async def bad_probe(*_args: Any, **_kwargs: Any) -> daytona_provider.SandboxExecResult: + return daytona_provider.SandboxExecResult(stdout="not ready", stderr="nope", return_code=1) + + async def no_sleep(_seconds: float) -> None: + return None + + monkeypatch.setattr(provider, "_exec", bad_probe) + monkeypatch.setattr(daytona_provider.asyncio, "sleep", no_sleep) + with pytest.raises(daytona_provider.DaytonaCreateVerificationError): + await provider._verify_created_handle(handle) + + async def raising_probe(*_args: Any, **_kwargs: Any) -> daytona_provider.SandboxExecResult: + raise RuntimeError("transient probe failure") + + monkeypatch.setattr(provider, "_exec", raising_probe) + with pytest.raises(daytona_provider.DaytonaCreateVerificationError): + await provider._verify_created_handle(handle) + + async def cancelled_probe(*_args: Any, **_kwargs: Any) -> daytona_provider.SandboxExecResult: + raise asyncio.CancelledError() + + monkeypatch.setattr(provider, "_exec", cancelled_probe) + with pytest.raises(asyncio.CancelledError): + await provider._verify_created_handle(handle) + + cleanup_calls: list[tuple[str, bool]] = [] + provider = daytona_provider.DaytonaProvider(create={"timeout_s": 2}, probe={"command": "probe"}) + monkeypatch.setattr(provider, "_client", lambda: client) + + async def fail_verify(_handle: SandboxHandle) -> None: + raise RuntimeError("probe failed") + + async def close_for_cleanup(cleanup_handle: SandboxHandle, *, delete: bool) -> None: + cleanup_calls.append((cleanup_handle.sandbox_id, delete)) + + monkeypatch.setattr(provider, "_verify_created_handle", fail_verify) + monkeypatch.setattr(provider, "close", close_for_cleanup) + with pytest.raises(RuntimeError, match="probe failed"): + await provider._create_once(SandboxSpec(image="python:3.12")) + assert cleanup_calls == [("sandbox-created", True)] + + async def close_cleanup_failure(cleanup_handle: SandboxHandle, *, delete: bool) -> None: + cleanup_calls.append((cleanup_handle.sandbox_id, delete)) + raise RuntimeError("cleanup failed") + + cleanup_calls.clear() + monkeypatch.setattr(provider, "close", close_cleanup_failure) + with caplog.at_level(logging.WARNING): + with pytest.raises(RuntimeError, match="probe failed"): + await provider._create_once(SandboxSpec(image="python:3.12")) + assert cleanup_calls == [("sandbox-created", True)] + assert "Failed to delete Daytona sandbox after create probe failure" in caplog.text + assert "cleanup failed" in caplog.text + + class TimeoutClient(FakeClient): + async def create(self, _params: Any, *, timeout: float) -> FakeSandbox: + del timeout + await asyncio.get_running_loop().create_future() + return FakeSandbox("never") + + timeout_provider = daytona_provider.DaytonaProvider(create={"timeout_s": 0.01}, probe={"command": None}) + monkeypatch.setattr(timeout_provider, "_client", lambda: TimeoutClient()) + with pytest.raises(daytona_provider.DaytonaCreateTimeoutError, match="Timed out creating Daytona sandbox"): + await timeout_provider._create_once(SandboxSpec(image="python:3.12")) + + class FakeFs: + def __init__(self) -> None: + self.uploads: list[tuple[Any, ...]] = [] + self.downloads: list[tuple[Any, ...]] = [] + + async def upload_file(self, *args: Any, **kwargs: Any) -> None: + self.uploads.append((*args, kwargs)) + + async def download_file(self, *args: Any, **kwargs: Any) -> str: + self.downloads.append((*args, kwargs)) + if len(args) == 1 or (len(args) == 2 and isinstance(args[1], int)): + return "contents" + Path(args[1]).write_text("downloaded", encoding="utf-8") + return "" + + fs = FakeFs() + file_provider = daytona_provider.DaytonaProvider(operations={"retries": 0, "file_timeout_s": 5}) + file_handle = SandboxHandle(sandbox_id="sandbox-files", provider_name="daytona", raw=SimpleNamespace(fs=fs)) + await file_provider.write_file(file_handle, "/remote/write.txt", "text") + assert await file_provider.read_file(file_handle, "/remote/read.txt") == b"contents" + local_upload = tmp_path / "upload.txt" + local_upload.write_text("upload", encoding="utf-8") + await file_provider.upload_file(file_handle, local_upload, "/remote/upload.txt") + local_download = tmp_path / "nested" / "download.txt" + await file_provider.download_file(file_handle, "/remote/download.txt", local_download) + assert fs.uploads[0] == (b"text", "/remote/write.txt", {"timeout": 5}) + assert fs.uploads[1] == (str(local_upload), "/remote/upload.txt", {"timeout": 5}) + assert fs.downloads[0] == ("/remote/read.txt", 5, {}) + assert fs.downloads[1] == ("/remote/download.txt", str(local_download), 5, {}) + assert local_download.read_text(encoding="utf-8") == "downloaded" + + close_provider = daytona_provider.DaytonaProvider(operations={"close_timeout_s": 5}) + monkeypatch.setattr(close_provider, "_client", lambda: client) + await close_provider.close(SandboxHandle("sandbox-retained", "daytona", raw=FakeSandbox("retained")), delete=False) + await close_provider.close(SandboxHandle("sandbox-default-delete", "daytona", raw=FakeSandbox("default-delete"))) + await close_provider.close(SandboxHandle("sandbox-delete", "daytona", raw=FakeSandbox("delete-me")), delete=True) + await close_provider.close(SandboxHandle("sandbox-missing", "daytona", raw="missing"), delete=True) + with pytest.raises(RuntimeError, match="delete failed"): + await close_provider.close(SandboxHandle("sandbox-failed", "daytona", raw="failed"), delete=True) + assert client.deleted[-1] == "delete-me" + + +async def test_command_retries_default_to_zero() -> None: + class FakeProcess: + def __init__(self) -> None: + self.calls = 0 + + async def exec(self, *_args: Any, **_kwargs: Any) -> Any: + self.calls += 1 + raise ConnectionError("response lost after command may have run") + + process = FakeProcess() + raw = SimpleNamespace(process=process) + provider = daytona_provider.DaytonaProvider(operations={"retries": 3}) + + with pytest.raises(ConnectionError): + await provider.exec(SandboxHandle(sandbox_id="sandbox-1", provider_name="daytona", raw=raw), "cmd") + + assert process.calls == 1 + + +def test_per_command_user_switching_is_not_supported() -> None: + assert daytona_provider.DaytonaProvider._effective_command("whoami", None) == "whoami" + assert daytona_provider.DaytonaProvider._effective_command("whoami", "root") == "whoami" + assert daytona_provider.DaytonaProvider._effective_command("whoami", 0) == "whoami" + + with pytest.raises(NotImplementedError, match="per-command user switching"): + daytona_provider.DaytonaProvider._effective_command("whoami", "agent") + + +async def test_create_timeout_is_not_retried() -> None: + provider = daytona_provider.DaytonaProvider(create={"retries": 3}, probe={"command": None}) + calls = 0 + + async def create_once(_spec: SandboxSpec) -> SandboxHandle: + nonlocal calls + calls += 1 + raise daytona_provider.DaytonaCreateTimeoutError("ambiguous create timeout") + + provider._create_once = create_once # type: ignore[method-assign] + + with pytest.raises(daytona_provider.DaytonaCreateTimeoutError): + await provider.create(SandboxSpec(image="python:3.12")) + + assert calls == 1 + + +async def test_create_retries_retryable_verification_failures(monkeypatch: pytest.MonkeyPatch) -> None: + provider = daytona_provider.DaytonaProvider(create={"retries": 1, "retry_delay_s": 0}, probe={"command": None}) + calls = 0 + names: list[str | None] = [] + labels: list[str | None] = [] + sleeps: list[float] = [] + + async def create_once(spec: SandboxSpec) -> SandboxHandle: + nonlocal calls + calls += 1 + names.append(daytona_provider._extension_value(spec, "name")) + labels.append(spec.metadata.get("sandbox_id")) + if calls == 1: + raise daytona_provider.DaytonaCreateVerificationError("probe failed") + return SandboxHandle("sandbox-created", "daytona", raw=object()) + + async def record_sleep(seconds: float) -> None: + sleeps.append(seconds) + + monkeypatch.setattr(daytona_provider.asyncio, "sleep", record_sleep) + provider._create_once = create_once # type: ignore[method-assign] + + assert (await provider.create(SandboxSpec(image="python:3.12"))).sandbox_id == "sandbox-created" + assert calls == 2 + assert names[0] != names[1] + assert labels == names + assert all(name is not None and name.startswith("nemo-gym-") for name in names) + assert sleeps == [0] + + +async def test_create_retries_preserve_explicit_name(monkeypatch: pytest.MonkeyPatch) -> None: + provider = daytona_provider.DaytonaProvider(create={"retries": 1, "retry_delay_s": 0}, probe={"command": None}) + names: list[str | None] = [] + + async def create_once(spec: SandboxSpec) -> SandboxHandle: + names.append(daytona_provider._extension_value(spec, "name")) + if len(names) == 1: + raise daytona_provider.DaytonaCreateVerificationError("probe failed") + return SandboxHandle("sandbox-created", "daytona", raw=object()) + + async def no_sleep(_seconds: float) -> None: + return None + + monkeypatch.setattr(daytona_provider.asyncio, "sleep", no_sleep) + provider._create_once = create_once # type: ignore[method-assign] + + spec = SandboxSpec( + image="python:3.12", + provider_options={"extensions": {"daytona.name": "custom-sandbox"}}, + ) + assert (await provider.create(spec)).sandbox_id == "sandbox-created" + assert names == ["custom-sandbox", "custom-sandbox"] + + +async def test_create_batch_returns_all_partial_successes_and_aggregates_cleanup_errors() -> None: + provider = daytona_provider.DaytonaProvider(batch={"concurrency": 4}, probe={"command": None}) + handles = [ + SandboxHandle(sandbox_id="sandbox-1", provider_name="daytona", raw=object()), + SandboxHandle(sandbox_id="sandbox-2", provider_name="daytona", raw=object()), + ] + responses: list[SandboxHandle | Exception] = [handles[0], RuntimeError("create failed"), handles[1]] + + async def create(_spec: SandboxSpec) -> SandboxHandle: + response = responses.pop(0) + if isinstance(response, Exception): + raise response + return response + + provider.create = create # type: ignore[method-assign] + + with pytest.raises(ValueError, match="count must be >= 1"): + await provider.create_batch(SandboxSpec(image="python:3.12"), 0) + assert await provider.create_batch(SandboxSpec(image="python:3.12"), 3, allow_partial=True) == handles + + responses = [handles[0], handles[1]] + assert await provider.create_batch(SandboxSpec(image="python:3.12"), 2) == handles + + responses = [handles[0], RuntimeError("create failed"), handles[1]] + cleanup_calls: list[str] = [] + + async def close(handle: SandboxHandle, *, delete: bool) -> None: + assert delete is True + cleanup_calls.append(handle.sandbox_id) + raise RuntimeError(f"cleanup failed for {handle.sandbox_id}") + + provider.create = create # type: ignore[method-assign] + provider.close = close # type: ignore[method-assign] + + with pytest.raises(daytona_provider.DaytonaCreateError, match="cleanup_errors=.*sandbox-1.*sandbox-2"): + await provider.create_batch(SandboxSpec(image="python:3.12"), 3) + + assert cleanup_calls == ["sandbox-1", "sandbox-2"] + + +async def _return_value(value: Any) -> Any: + return value + + +async def _raise(error: BaseException) -> None: + raise error diff --git a/uv.lock b/uv.lock index 778dae4a1e..4acdea225e 100644 --- a/uv.lock +++ b/uv.lock @@ -45,6 +45,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -166,6 +175,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, ] +[[package]] +name = "aiohttp-retry" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/61/ebda4d8e3d8cfa1fd3db0fb428db2dd7461d5742cea35178277ad180b033/aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1", size = 13608, upload-time = "2024-11-06T10:44:54.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", size = 9981, upload-time = "2024-11-06T10:44:52.917Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -613,6 +634,111 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5e/6f8d874366788ad5d549e9ba258037d974dda6e004843be1bda794571701/datasets-4.4.1-py3-none-any.whl", hash = "sha256:c1163de5211e42546079ab355cc0250c7e6db16eb209ac5ac6252f801f596c44", size = 511591, upload-time = "2025-11-05T16:00:36.365Z" }, ] +[[package]] +name = "daytona" +version = "0.183.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "daytona-api-client" }, + { name = "daytona-api-client-async" }, + { name = "daytona-toolbox-api-client" }, + { name = "daytona-toolbox-api-client-async" }, + { name = "deprecated" }, + { name = "httpx" }, + { name = "httpx-ws" }, + { name = "obstore" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-aiohttp-client" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "toml" }, + { name = "urllib3" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/dd/3914a2ae3dc4ecf3fd861eff221f4222e48ba4e5530ff7e585d4c4aa7965/daytona-0.183.0.tar.gz", hash = "sha256:2f0020537712ae67693fd2341d3008d464ada06dcd8e89baa6183846271e2f76", size = 141636, upload-time = "2026-05-29T10:59:50.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/ae/b23f7e5381ca5d66f2be0f802ebef0110b5e21022946fa7b9d6e873ec6ad/daytona-0.183.0-py3-none-any.whl", hash = "sha256:89af1bcfc9b37c580329fcad01167bcfab786330e2d0ae74ad3fae8cb40397d2", size = 172192, upload-time = "2026-05-29T10:59:48.838Z" }, +] + +[[package]] +name = "daytona-api-client" +version = "0.183.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/40/a5090268e734f804489b8ca3dcdbd4de7c392b1b4d4b02a60c5d85c7198f/daytona_api_client-0.183.0.tar.gz", hash = "sha256:659fff84a6461d1f51986fb814bf6e3fbc51ac203e6502eb6ae6f26029d69814", size = 147714, upload-time = "2026-05-29T10:59:01.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/6a/8890e773ecf1cd9c1b70ddb0fda57e4941f5a8a44ced1c91574253797f63/daytona_api_client-0.183.0-py3-none-any.whl", hash = "sha256:c319ae3b4666ec15402cacb8ec3aaffbf55802c0dbc33f6826ef07afceae5085", size = 406274, upload-time = "2026-05-29T10:59:00.427Z" }, +] + +[[package]] +name = "daytona-api-client-async" +version = "0.183.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiohttp-retry" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/aa/c9de46d48ee45228566a53bb699bc4bdab693dc8ce300516ce334bfec82f/daytona_api_client_async-0.183.0.tar.gz", hash = "sha256:4f5b397e375635fe3614c342cf34faa236d5a0918c53588d7059b5061d881407", size = 148190, upload-time = "2026-05-29T10:59:14.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/db/dde2e2478faf010b048af24227e5ff8b4c635d8104ace26c66ed1f29dd49/daytona_api_client_async-0.183.0-py3-none-any.whl", hash = "sha256:8be2b0bac344893294a8ffd8df7a4703427ec48376f6948975aca85f09dba541", size = 409552, upload-time = "2026-05-29T10:59:12.964Z" }, +] + +[[package]] +name = "daytona-toolbox-api-client" +version = "0.183.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/ee/1c8b9785c4028a0932f655e131e44417380a1e3d6ff4eb58b4e10b9aac18/daytona_toolbox_api_client-0.183.0.tar.gz", hash = "sha256:7cf4eb8ab36ec488f436695822b0e4c06e82d12d8fb2fe28e3239151dd43c4b7", size = 77732, upload-time = "2026-05-29T10:58:40.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/3a/904bbf95eac89036775051410dbaf55414a73a1e6d074e1486ca29ee3dbb/daytona_toolbox_api_client-0.183.0-py3-none-any.whl", hash = "sha256:bc0c2fb986f50cad23791afa181c04f27d975b59301d7ce59ce519c1f518483c", size = 205626, upload-time = "2026-05-29T10:58:39.381Z" }, +] + +[[package]] +name = "daytona-toolbox-api-client-async" +version = "0.183.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiohttp-retry" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/0f/85e2ad6511ae228e1c423944d6d90ba3ace2153685ba8aa3b4035c5ccc6f/daytona_toolbox_api_client_async-0.183.0.tar.gz", hash = "sha256:31984e9fc5c440efbb5d23426f187046fa419c075c42d8691b127304692c4e6b", size = 71347, upload-time = "2026-05-29T10:59:03.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/66/af01390a47ba626454fd2ba44766282ffb7174ecc60c1742ab4859bd074f/daytona_toolbox_api_client_async-0.183.0-py3-none-any.whl", hash = "sha256:54a25297cfaf68568d85cecfd5c07da2e7fd862c8ab445df97b76a360951f757", size = 203902, upload-time = "2026-05-29T10:59:02.046Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + [[package]] name = "devtools" version = "0.12.2" @@ -1021,6 +1147,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "httpx-ws" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore" }, + { name = "httpx" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/cd/ca91a07ae446451f7476bf3fcc909e98cb942ff032ebfda0e3fe449aca7b/httpx_ws-0.9.0.tar.gz", hash = "sha256:797373326f70eec1ae96f6e43ae9f12002fd7d73aee139a4985eaab964338a08", size = 107105, upload-time = "2026-03-28T14:11:10.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/f8/a6bc80313a9e93c888fa10534dfce2ad76ff86911b6f485777ce6de6a073/httpx_ws-0.9.0-py3-none-any.whl", hash = "sha256:71640d2fb1bf9a225775015b33cd755cfd4c5f7e21c885192fe3adc4c387b248", size = 15759, upload-time = "2026-03-28T14:11:11.887Z" }, +] + [[package]] name = "huggingface-hub" version = "0.34.4" @@ -1753,6 +1894,7 @@ dependencies = [ all = [ { name = "boto3" }, { name = "coverage" }, + { name = "daytona" }, { name = "mypy" }, { name = "opensandbox" }, { name = "pre-commit" }, @@ -1777,6 +1919,7 @@ dev = [ ] sandbox = [ { name = "boto3" }, + { name = "daytona" }, { name = "opensandbox" }, { name = "tenacity" }, ] @@ -1802,6 +1945,7 @@ requires-dist = [ { name = "boto3", marker = "extra == 'sandbox'", specifier = ">=1.34" }, { name = "coverage", extras = ["toml"], marker = "extra == 'dev'" }, { name = "datasets" }, + { name = "daytona", marker = "extra == 'sandbox'", specifier = ">=0.179.0" }, { name = "devtools" }, { name = "fastapi" }, { name = "fonttools", specifier = ">=4.60.2" }, @@ -1940,6 +2084,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/74/996dbc314da8ed670cd5e040d0b4b5be79ff1fc3db3fe25e63134deebe9a/nvidia_sphinx_theme-0.0.8-py3-none-any.whl", hash = "sha256:18f117aa154a3a156251a75647279c541464f3e75f7df2ae283e720cc7d0bc2c", size = 140678, upload-time = "2025-03-24T21:56:25.621Z" }, ] +[[package]] +name = "obstore" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/8c/9ec984edd0f3b72226adfaa19b1c61b15823b35b52f311ca4af36d009d15/obstore-0.8.2.tar.gz", hash = "sha256:a467bc4e97169e2ba749981b4fd0936015428d9b8f3fb83a5528536b1b6f377f", size = 168852, upload-time = "2025-09-16T15:34:55.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/dc/60fefbb5736e69eab56657bca04ca64dc07fdeccb3814164a31b62ad066b/obstore-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bb70ce297a47392b1d9a3e310f18d59cd5ebbb9453428210fef02ed60e4d75d1", size = 3612955, upload-time = "2025-09-16T15:33:29.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/844e8f382e5a12b8a3796a05d76a03e12c7aedc13d6900419e39207d7868/obstore-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1619bf618428abf1f607e0b219b2e230a966dcf697b717deccfa0983dd91f646", size = 3346564, upload-time = "2025-09-16T15:33:30.698Z" }, + { url = "https://files.pythonhosted.org/packages/89/73/8537f99e09a38a54a6a15ede907aa25d4da089f767a808f0b2edd9c03cec/obstore-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4605c3ed7c9515aeb4c619b5f7f2c9986ed4a79fe6045e536b5e59b804b1476", size = 3460809, upload-time = "2025-09-16T15:33:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/b4/99/7714dec721e43f521d6325a82303a002cddad089437640f92542b84e9cc8/obstore-0.8.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce42670417876dd8668cbb8659e860e9725e5f26bbc86449fd259970e2dd9d18", size = 3692081, upload-time = "2025-09-16T15:33:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/4ac4175fe95a24c220a96021c25c432bcc0c0212f618be0737184eebbaad/obstore-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a3e893b2a06585f651c541c1972fe1e3bf999ae2a5fda052ee55eb7e6516f5", size = 3957466, upload-time = "2025-09-16T15:33:34.528Z" }, + { url = "https://files.pythonhosted.org/packages/4e/04/caa288fb735484fc5cb019bdf3d896eaccfae0ac4622e520d05692c46790/obstore-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08462b32f95a9948ed56ed63e88406e2e5a4cae1fde198f9682e0fb8487100ed", size = 3951293, upload-time = "2025-09-16T15:33:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/d380239da2d6a1fda82e17df5dae600a404e8a93a065784518ff8325d5f6/obstore-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a0bf7763292a8fc47d01cd66e6f19002c5c6ad4b3ed4e6b2729f5e190fa8a0d", size = 3766199, upload-time = "2025-09-16T15:33:36.904Z" }, + { url = "https://files.pythonhosted.org/packages/28/41/d391be069d3da82969b54266948b2582aeca5dd735abeda4d63dba36e07b/obstore-0.8.2-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:bcd47f8126cb192cbe86942b8f73b1c45a651ce7e14c9a82c5641dfbf8be7603", size = 3529678, upload-time = "2025-09-16T15:33:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4c/4862fdd1a3abde459ee8eea699b1797df638a460af235b18ca82c8fffb72/obstore-0.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57eda9fd8c757c3b4fe36cf3918d7e589cc1286591295cc10b34122fa36dd3fd", size = 3698079, upload-time = "2025-09-16T15:33:39.696Z" }, + { url = "https://files.pythonhosted.org/packages/68/ca/014e747bc53b570059c27e3565b2316fbe5c107d4134551f4cd3e24aa667/obstore-0.8.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ea44442aad8992166baa69f5069750979e4c5d9ffce772e61565945eea5774b9", size = 3687154, upload-time = "2025-09-16T15:33:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/6db5f8edd93028e5b8bfbeee15e6bd3e56f72106107d31cb208b57659de4/obstore-0.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:41496a3ab8527402db4142aaaf0d42df9d7d354b13ba10d9c33e0e48dd49dd96", size = 3773444, upload-time = "2025-09-16T15:33:42.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/e5/c9e2cc540689c873beb61246e1615d6e38301e6a34dec424f5a5c63c1afd/obstore-0.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43da209803f052df96c7c3cbec512d310982efd2407e4a435632841a51143170", size = 3939315, upload-time = "2025-09-16T15:33:43.252Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c9/bb53280ca50103c1ffda373cdc9b0f835431060039c2897cbc87ddd92e42/obstore-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:1836f5dcd49f9f2950c75889ab5c51fb290d3ea93cdc39a514541e0be3af016e", size = 3978234, upload-time = "2025-09-16T15:33:44.393Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5d/8c3316cc958d386d5e6ab03e9db9ddc27f8e2141cee4a6777ae5b92f3aac/obstore-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:212f033e53fe6e53d64957923c5c88949a400e9027f7038c705ec2e9038be563", size = 3612027, upload-time = "2025-09-16T15:33:45.6Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4d/699359774ce6330130536d008bfc32827fab0c25a00238d015a5974a3d1d/obstore-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bee21fa4ba148d08fa90e47a96df11161661ed31e09c056a373cb2154b0f2852", size = 3344686, upload-time = "2025-09-16T15:33:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/82/37/55437341f10512906e02fd9fa69a8a95ad3f2f6a916d3233fda01763d110/obstore-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c66594b59832ff1ced4c72575d9beb8b5f9b4e404ac1150a42bfb226617fd50", size = 3459860, upload-time = "2025-09-16T15:33:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/7a/51/4245a616c94ee4851965e33f7a563ab4090cc81f52cc73227ff9ceca2e46/obstore-0.8.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:089f33af5c2fe132d00214a0c1f40601b28f23a38e24ef9f79fb0576f2730b74", size = 3691648, upload-time = "2025-09-16T15:33:49.524Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/4e2fb24171e3ca3641a4653f006be826e7e17634b11688a5190553b00b83/obstore-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d87f658dfd340d5d9ea2d86a7c90d44da77a0db9e00c034367dca335735110cf", size = 3956867, upload-time = "2025-09-16T15:33:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/42/f5/b703115361c798c9c1744e1e700d5908d904a8c2e2bd38bec759c9ffb469/obstore-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e2e4fa92828c4fbc2d487f3da2d3588701a1b67d9f6ca3c97cc2afc912e9c63", size = 3950599, upload-time = "2025-09-16T15:33:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/53/20/08c6dc0f20c1394e2324b9344838e4e7af770cdcb52c30757a475f50daeb/obstore-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab440e89c5c37a8ec230857dd65147d4b923e0cada33297135d05e0f937d696a", size = 3765865, upload-time = "2025-09-16T15:33:53.291Z" }, + { url = "https://files.pythonhosted.org/packages/77/20/77907765e29b2eba6bd8821872284d91170d7084f670855b2dfcb249ea14/obstore-0.8.2-cp313-cp313-manylinux_2_24_aarch64.whl", hash = "sha256:b9beed107c5c9cd995d4a73263861fcfbc414d58773ed65c14f80eb18258a932", size = 3529807, upload-time = "2025-09-16T15:33:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/f629d39cc30d050f52b1bf927e4d65c1cc7d7ffbb8a635cd546b5c5219a0/obstore-0.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b75b4e7746292c785e31edcd5aadc8b758238372a19d4c5e394db5c305d7d175", size = 3693629, upload-time = "2025-09-16T15:33:56.016Z" }, + { url = "https://files.pythonhosted.org/packages/30/ff/106763fd10f2a1cb47f2ef1162293c78ad52f4e73223d8d43fc6b755445d/obstore-0.8.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f33e6c366869d05ab0b7f12efe63269e631c5450d95d6b4ba4c5faf63f69de70", size = 3686176, upload-time = "2025-09-16T15:33:57.247Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/d2ccb6f32feeca906d5a7c4255340df5262af8838441ca06c9e4e37b67d5/obstore-0.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:12c885a9ce5ceb09d13cc186586c0c10b62597eff21b985f6ce8ff9dab963ad3", size = 3773081, upload-time = "2025-09-16T15:33:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/40d1cc504cefc89c9b3dd8874287f3fddc7d963a8748d6dffc5880222013/obstore-0.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4accc883b93349a81c9931e15dd318cc703b02bbef2805d964724c73d006d00e", size = 3938589, upload-time = "2025-09-16T15:33:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/916c6777222db3271e9fb3cf9a97ed92b3a9b3e465bdeec96de9ab809d53/obstore-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ec850adf9980e5788a826ccfd5819989724e2a2f712bfa3258e85966c8d9981e", size = 3977768, upload-time = "2025-09-16T15:34:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/66f8dc98bbf5613bbfe5bf21747b4c8091442977f4bd897945895ab7325c/obstore-0.8.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1431e40e9bb4773a261e51b192ea6489d0799b9d4d7dbdf175cdf813eb8c0503", size = 3623364, upload-time = "2025-09-16T15:34:02.957Z" }, + { url = "https://files.pythonhosted.org/packages/1a/66/6d527b3027e42f625c8fc816ac7d19b0d6228f95bfe7666e4d6b081d2348/obstore-0.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ddb39d4da303f50b959da000aa42734f6da7ac0cc0be2d5a7838b62c97055bb9", size = 3347764, upload-time = "2025-09-16T15:34:04.236Z" }, + { url = "https://files.pythonhosted.org/packages/0d/79/c00103302b620192ea447a948921ad3fed031ce3d19e989f038e1183f607/obstore-0.8.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e01f4e13783db453e17e005a4a3ceff09c41c262e44649ba169d253098c775e8", size = 3460981, upload-time = "2025-09-16T15:34:05.595Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d9/bfe4ed4b1aebc45b56644dd5b943cf8e1673505cccb352e66878a457e807/obstore-0.8.2-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df0fc2d0bc17caff9b538564ddc26d7616f7e8b7c65b1a3c90b5048a8ad2e797", size = 3692711, upload-time = "2025-09-16T15:34:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/13/47/cd6c2cbb18e1f40c77e7957a4a03d2d83f1859a2e876a408f1ece81cad4c/obstore-0.8.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e439d06c99a140348f046c9f598ee349cc2dcd9105c15540a4b231f9cc48bbae", size = 3958362, upload-time = "2025-09-16T15:34:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ea/5ee82bf23abd71c7d6a3f2d008197ae8f8f569d41314c26a8f75318245be/obstore-0.8.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e37d9046669fcc59522d0faf1d105fcbfd09c84cccaaa1e809227d8e030f32c", size = 3957082, upload-time = "2025-09-16T15:34:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ee/46650405e50fdaa8d95f30375491f9c91fac9517980e8a28a4a6af66927f/obstore-0.8.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2646fdcc4bbe92dc2bb5bcdff15574da1211f5806c002b66d514cee2a23c7cb8", size = 3775539, upload-time = "2025-09-16T15:34:10.726Z" }, + { url = "https://files.pythonhosted.org/packages/35/d6/348a7ebebe2ca3d94dfc75344ea19675ae45472823e372c1852844078307/obstore-0.8.2-cp314-cp314-manylinux_2_24_aarch64.whl", hash = "sha256:e31a7d37675056d93dfc244605089dee67f5bba30f37c88436623c8c5ad9ba9d", size = 3535048, upload-time = "2025-09-16T15:34:12.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/b7a16cc0da91a4b902d47880ad24016abfe7880c63f7cdafda45d89a2f91/obstore-0.8.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:656313dd8170dde0f0cd471433283337a63912e8e790a121f7cc7639c83e3816", size = 3699035, upload-time = "2025-09-16T15:34:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/3269a3a58347e0b019742d888612c4b765293c9c75efa44e144b1e884c0d/obstore-0.8.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329038c9645d6d1741e77fe1a53e28a14b1a5c1461cfe4086082ad39ebabf981", size = 3687307, upload-time = "2025-09-16T15:34:14.501Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/4fd4819ad6a49d2f462a45be453561f4caebded0dc40112deeffc34b89b1/obstore-0.8.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1e4df99b369790c97c752d126b286dc86484ea49bff5782843a265221406566f", size = 3776076, upload-time = "2025-09-16T15:34:16.207Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/7c4f958fa0b9fc4778fb3d232e38b37db8c6b260f641022fbba48b049d7e/obstore-0.8.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9e1c65c65e20cc990414a8a9af88209b1bbc0dd9521b5f6b0293c60e19439bb7", size = 3947445, upload-time = "2025-09-16T15:34:17.423Z" }, +] + [[package]] name = "omegaconf" version = "2.3.0" @@ -2012,29 +2205,90 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.36.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/d2/c782c88b8afbf961d6972428821c302bd1e9e7bc361352172f0ca31296e2/opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0", size = 64780, upload-time = "2025-07-29T15:12:06.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c", size = 65564, upload-time = "2025-07-29T15:11:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, ] [[package]] name = "opentelemetry-exporter-prometheus" -version = "0.57b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "prometheus-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/d8/5f04c6d51c0823c3d8ac973a2a38db6fcf2d040ca3f08fc66b3c14b6e164/opentelemetry_exporter_prometheus-0.57b0.tar.gz", hash = "sha256:9eb15bdc189235cf03c3f93abf56f8ff0ab57a493a189263bd7fe77a4249e689", size = 14906, upload-time = "2025-07-29T15:12:09.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/07/39370ec7eacfca10462121a0e036b66ccea3a616bf6ae6ea5fdb72e5009d/opentelemetry_exporter_prometheus-0.59b0.tar.gz", hash = "sha256:d64f23c49abb5a54e271c2fbc8feacea0c394a30ec29876ab5ef7379f08cf3d7", size = 14972, upload-time = "2025-10-16T08:35:55.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/ea/3005a732002242fd86203989520bdd5a752e1fd30dc225d5d45751ea19fb/opentelemetry_exporter_prometheus-0.59b0-py3-none-any.whl", hash = "sha256:71ced23207abd15b30d1fe4e7e910dcaa7c2ff1f24a6ffccbd4fdded676f541b", size = 13017, upload-time = "2025-10-16T08:35:37.253Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-aiohttp-client" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/cb/e135fad3edfd8492bbf3e50ce6fa288f730b8ef8700d93e4c2e49cc1bf6c/opentelemetry_instrumentation_aiohttp_client-0.59b0.tar.gz", hash = "sha256:665ee520f2fb5f44d6d600918eb7bbd2c29b355e0d0deda49991db857adee51f", size = 15037, upload-time = "2025-10-16T08:39:33.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/1c/40fb93a7b7e495985393bbc734104d5d20e470811644dd56c2402d683739/opentelemetry_exporter_prometheus-0.57b0-py3-none-any.whl", hash = "sha256:c5b893d1cdd593fb022af2c7de3258c2d5a4d04402ae80d9fa35675fed77f05c", size = 12922, upload-time = "2025-07-29T15:11:54.055Z" }, + { url = "https://files.pythonhosted.org/packages/bd/db/99376c8f0d3141e92b9902ae1e5b51dc4f458fc23d03e3815722d64afbcb/opentelemetry_instrumentation_aiohttp_client-0.59b0-py3-none-any.whl", hash = "sha256:8e7f234d2b6b385d5b1757ba7f13baa71c45ec020e1a08ba7f7649da7958470a", size = 12378, upload-time = "2025-10-16T08:38:33.56Z" }, ] [[package]] @@ -2051,29 +2305,38 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.36.0" +version = "1.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/85/8567a966b85a2d3f971c4d42f781c305b2b91c043724fa08fd37d158e9dc/opentelemetry_sdk-1.36.0.tar.gz", hash = "sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581", size = 162557, upload-time = "2025-07-29T15:12:16.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/59/7bed362ad1137ba5886dac8439e84cd2df6d087be7c09574ece47ae9b22c/opentelemetry_sdk-1.36.0-py3-none-any.whl", hash = "sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb", size = 119995, upload-time = "2025-07-29T15:12:03.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.57b0" +version = "0.59b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/31/67dfa252ee88476a29200b0255bda8dfc2cf07b56ad66dc9a6221f7dc787/opentelemetry_semantic_conventions-0.57b0.tar.gz", hash = "sha256:609a4a79c7891b4620d64c7aac6898f872d790d75f22019913a660756f27ff32", size = 124225, upload-time = "2025-07-29T15:12:17.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412, upload-time = "2025-10-16T08:40:11.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/75/7d591371c6c39c73de5ce5da5a2cc7b72d1d1cd3f8f4638f553c01c37b11/opentelemetry_semantic_conventions-0.57b0-py3-none-any.whl", hash = "sha256:757f7e76293294f124c827e514c2a3144f191ef175b069ce8d1211e1e38e9e78", size = 201627, upload-time = "2025-07-29T15:12:04.174Z" }, + { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648, upload-time = "2025-10-16T08:39:25.706Z" }, ] [[package]] @@ -3391,6 +3654,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -3618,71 +3890,63 @@ wheels = [ [[package]] name = "wrapt" -version = "2.0.0" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "wsproto" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/19/5e5bcd855d808892fe02d49219f97a50f64cd6d8313d75df3494ee97b1a3/wrapt-2.0.0.tar.gz", hash = "sha256:35a542cc7a962331d0279735c30995b024e852cf40481e384fd63caaa391cbb9", size = 81722, upload-time = "2025-10-19T23:47:54.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/28/7f266b5bf50c3ad0c99c524d99faa0f7d6eecb045d950e7d2c9e1f0e1338/wrapt-2.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73c6f734aecb1a030d9a265c13a425897e1ea821b73249bb14471445467ca71c", size = 78078, upload-time = "2025-10-19T23:45:58.855Z" }, - { url = "https://files.pythonhosted.org/packages/06/0c/bbdcad7eb535fae9d6b0fcfa3995c364797cd8e2b423bba5559ab2d88dcf/wrapt-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4a7f8023b8ce8a36370154733c747f8d65c8697cb977d8b6efeb89291fff23e", size = 61158, upload-time = "2025-10-19T23:46:00.096Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/bba3e7a4ebf4d1624103ee59d97b78a1fbb08fb5753ff5d1b69f5ef5e863/wrapt-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cb62f686c50e9dab5983c68f6c8e9cbf14a6007935e683662898a7d892fa69", size = 61646, upload-time = "2025-10-19T23:46:01.279Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0c/0f565294897a72493dbafe7b46229b5f09f3776795a894d6b737e98387de/wrapt-2.0.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:43dc0550ae15e33e6bb45a82a5e1b5495be2587fbaa996244b509921810ee49f", size = 121442, upload-time = "2025-10-19T23:46:04.287Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/7f03501a8a078ad79b19b1a888f9192a9494e62ddf8985267902766a4f30/wrapt-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39c5b45b056d630545e40674d1f5e1b51864b3546f25ab6a4a331943de96262e", size = 123018, upload-time = "2025-10-19T23:46:06.052Z" }, - { url = "https://files.pythonhosted.org/packages/37/6b/ad0e1ff98359f13b4b0c2c52848e792841146fe79ac5f56899b9a028fc0d/wrapt-2.0.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:804e88f824b76240a1b670330637ccfd2d18b9efa3bb4f02eb20b2f64880b324", size = 117369, upload-time = "2025-10-19T23:46:02.53Z" }, - { url = "https://files.pythonhosted.org/packages/ac/6c/a90437bba8cb1ce2ed639af979515e09784678c2a7f4ffc79f2cf7de809e/wrapt-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c2c476aa3fc2b9899c3f7b20963fac4f952e7edb74a31fc92f7745389a2e3618", size = 121453, upload-time = "2025-10-19T23:46:07.747Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a9/b3982f9bd15bd45857a23c48b7c36e47d05db4a4dcc5061c31f169238845/wrapt-2.0.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8d851e526891216f89fcb7a1820dad9bd503ba3468fb9635ee28e93c781aa98e", size = 116250, upload-time = "2025-10-19T23:46:09.385Z" }, - { url = "https://files.pythonhosted.org/packages/73/e2/b7a8b1afac9f791d8f5eac0d9726559f1d7ec4a2b5a6b4e67ac145b007a5/wrapt-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b95733c2360c4a8656ee93c7af78e84c0bd617da04a236d7a456c8faa34e7a2d", size = 120575, upload-time = "2025-10-19T23:46:11.882Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/37920eeea96094f450ae35505d39f1135df951a2cdee0d4e01d4f843396a/wrapt-2.0.0-cp312-cp312-win32.whl", hash = "sha256:ea56817176834edf143df1109ae8fdaa087be82fdad3492648de0baa8ae82bf2", size = 58175, upload-time = "2025-10-19T23:46:15.678Z" }, - { url = "https://files.pythonhosted.org/packages/f0/db/b395f3b0c7f2c60d9219afacc54ceb699801ccf2d3d969ba556dc6d3af20/wrapt-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c7d3bee7be7a2665286103f4d1f15405c8074e6e1f89dac5774f9357c9a3809", size = 60415, upload-time = "2025-10-19T23:46:12.913Z" }, - { url = "https://files.pythonhosted.org/packages/86/22/33d660214548af47fc59d9eec8c0e0693bcedc5b3a0b52e8cbdd61f3b646/wrapt-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:680f707e1d26acbc60926659799b15659f077df5897a6791c7c598a5d4a211c4", size = 58911, upload-time = "2025-10-19T23:46:13.889Z" }, - { url = "https://files.pythonhosted.org/packages/18/0a/dd88abfe756b1aa79f0777e5ee4ce9e4b5dc4999bd805e9b04b52efc7b18/wrapt-2.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e2ea096db28d5eb64d381af0e93464621ace38a7003a364b6b5ffb7dd713aabe", size = 78083, upload-time = "2025-10-19T23:46:16.937Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/8afebc1655a863bb2178b23c2d699b8743f3a7dab466904adc6155f3c858/wrapt-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c92b5a82d28491e3f14f037e1aae99a27a5e6e0bb161e65f52c0445a3fa7c940", size = 61156, upload-time = "2025-10-19T23:46:17.927Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8b/f710a6528ccc52e21943f42c8cf64814cde90f9adbd3bcd58c7c274b4f75/wrapt-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81d234718aabe632d179fac52c7f69f0f99fbaac4d4bcd670e62462bbcbfcad7", size = 61641, upload-time = "2025-10-19T23:46:19.229Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5f/e4eabd0cc6684c5b208c2abc5c3459449c4d15be1694a9bbcf51e0e135fd/wrapt-2.0.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db2eea83c43f84e4e41dbbb4c1de371a53166e55f900a6b130c3ef51c6345c1a", size = 121454, upload-time = "2025-10-19T23:46:21.808Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c4/ec31ee17cc7866960d323609ba7402be786d211a6d713a59f776c4270bb3/wrapt-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65f50e356c425c061e1e17fe687ff30e294fed9bf3441dc1f13ef73859c2a817", size = 123063, upload-time = "2025-10-19T23:46:23.545Z" }, - { url = "https://files.pythonhosted.org/packages/b0/2b/a4b10c3c0022e40aeae9bec009bafb049f440493f0575ebb27ecf61c32f8/wrapt-2.0.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:887f2a667e3cbfb19e204032d42ad7dedaa43972e4861dc7a3d51ae951d9b578", size = 117401, upload-time = "2025-10-19T23:46:20.433Z" }, - { url = "https://files.pythonhosted.org/packages/2a/4a/ade23a76967e1f148e461076a4d0e24a7950a5f18b394c9107fe60224ae2/wrapt-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9054829da4be461e3ad3192e4b6bbf1fc18af64c9975ce613aec191924e004dc", size = 121485, upload-time = "2025-10-19T23:46:24.85Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ba/33b5f3e2edede4e1cfd259f0d9c203cf370f259bb9b215dd58fc6cbb94e9/wrapt-2.0.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b952ffd77133a5a2798ee3feb18e51b0a299d2f440961e5bb7737dbb02e57289", size = 116276, upload-time = "2025-10-19T23:46:27.006Z" }, - { url = "https://files.pythonhosted.org/packages/eb/bf/b7f95bb4529a35ca11eb95d48f9d1a563b495471f7cf404c644566fb4293/wrapt-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e25fde03c480061b8234d8ee4863eb5f40a9be4fb258ce105b364de38fc6bcf9", size = 120578, upload-time = "2025-10-19T23:46:28.679Z" }, - { url = "https://files.pythonhosted.org/packages/f8/71/984849df6f052592474a44aafd6b847e1cffad39b0debc5390a04aa46331/wrapt-2.0.0-cp313-cp313-win32.whl", hash = "sha256:49e982b7860d325094978292a49e0418833fc7fc42c0dc7cd0b7524d7d06ee74", size = 58178, upload-time = "2025-10-19T23:46:32.372Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3b/4e1fc0f2e1355fbc55ab248311bf4c958dbbd96bd9183b9e96882cc16213/wrapt-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e5c86389d9964050ce50babe247d172a5e3911d59a64023b90db2b4fa00ae7c", size = 60423, upload-time = "2025-10-19T23:46:30.041Z" }, - { url = "https://files.pythonhosted.org/packages/20/0a/9384e0551f56fe361f41bb8f209a13bb9ef689c3a18264225b249849b12c/wrapt-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:b96fdaa4611e05c7231937930567d3c16782be9dbcf03eb9f60d83e57dd2f129", size = 58918, upload-time = "2025-10-19T23:46:31.056Z" }, - { url = "https://files.pythonhosted.org/packages/68/70/37b90d3ee5bf0d0dc4859306383da08b685c9a51abff6fd6b0a7c052e117/wrapt-2.0.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f2c7b7fead096dbf1dcc455b7f59facb05de3f5bfb04f60a69f98cdfe6049e5f", size = 81980, upload-time = "2025-10-19T23:46:33.368Z" }, - { url = "https://files.pythonhosted.org/packages/95/23/0ce69cc90806b90b3ee4cfd9ad8d2ee9becc3a1aab7df3c3bfc7d0904cb6/wrapt-2.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:04c7c8393f25b11c0faa5d907dd9eb462e87e4e7ba55e308a046d7ed37f4bbe2", size = 62900, upload-time = "2025-10-19T23:46:34.415Z" }, - { url = "https://files.pythonhosted.org/packages/54/76/03ec08170c02f38f3be3646977920976b968e0b704a0693a98f95d02f4d2/wrapt-2.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a93e0f8b376c0735b2f4daf58018b4823614d2b896cb72b6641c4d3dbdca1d75", size = 63636, upload-time = "2025-10-19T23:46:35.643Z" }, - { url = "https://files.pythonhosted.org/packages/75/c1/04ce0511e504cdcd84cdb6980bc7d4efa38ac358e8103d6dd0cd278bfc6d/wrapt-2.0.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b42d13603da4416c43c430dbc6313c8d7ff745c40942f146ed4f6dd02c7d2547", size = 152650, upload-time = "2025-10-19T23:46:38.717Z" }, - { url = "https://files.pythonhosted.org/packages/17/06/cd2e32b5f744701189c954f9ab5eee449c86695b13f414bb8ea7a83f6d48/wrapt-2.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8bbd2472abf8c33480ad2314b1f8fac45d592aba6cc093e8839a7b2045660e6", size = 158811, upload-time = "2025-10-19T23:46:40.875Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a2/a6d920695cca62563c1b969064e5cd2051344a6e330c184b6f80383d87e4/wrapt-2.0.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e64a3a1fd9a308ab9b815a2ad7a65b679730629dbf85f8fc3f7f970d634ee5df", size = 146033, upload-time = "2025-10-19T23:46:37.351Z" }, - { url = "https://files.pythonhosted.org/packages/c6/90/7fd2abe4ec646bc43cb6b0d05086be6fcf15e64f06f51fc4198804396d68/wrapt-2.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d61214525eaf88e0d0edf3d1ad5b5889863c6f88e588c6cdc6aa4ee5d1f10a4a", size = 155673, upload-time = "2025-10-19T23:46:42.582Z" }, - { url = "https://files.pythonhosted.org/packages/5f/8d/6cce7f8c41633e677ac8aa34e84b53a22a645ec2a680deb991785ca2798d/wrapt-2.0.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:04f7a5f92c5f7324a1735043cc467b1295a1c5b4e0c1395472b7c44706e3dc61", size = 144364, upload-time = "2025-10-19T23:46:44.381Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/9570349e03afa9d83daf7f33ffb17e8cdc62d7e84c0d09005d0f51912efa/wrapt-2.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2356f76cb99b3de5b4e5b8210367fbbb81c7309fe39b622f5d199dd88eb7f765", size = 150275, upload-time = "2025-10-19T23:46:45.662Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d8/448728e6fe030e5c4f1022c82cd3af1de1c672fa53d2d5b36b32a55ce7bf/wrapt-2.0.0-cp313-cp313t-win32.whl", hash = "sha256:0a921b657a224e40e4bc161b5d33934583b34f0c9c5bdda4e6ac66f9d2fcb849", size = 59867, upload-time = "2025-10-19T23:46:49.593Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b1/ad812b1fe1cd85f6498dc3a3c9809a1e880d6108283b1735119bec217041/wrapt-2.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c16f6d4eea98080f6659a8a7fc559d4a0a337ee66960659265cad2c8a40f7c0f", size = 63170, upload-time = "2025-10-19T23:46:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/c105b1e76650c82823c491952a7a8eafe09b78944f7a43f22d37ed860229/wrapt-2.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:52878edc13dc151c58a9966621d67163a80654bc6cff4b2e1c79fa62d0352b26", size = 60339, upload-time = "2025-10-19T23:46:47.862Z" }, - { url = "https://files.pythonhosted.org/packages/f8/38/0dd39f83163fd28326afba84e3e416656938df07e60a924ac4d992b30220/wrapt-2.0.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:79a53d86c2aff7b32cc77267e3a308365d1fcb881e74bc9cbe26f63ee90e37f0", size = 78242, upload-time = "2025-10-19T23:46:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/08/ef/fa7a5c1d73f8690c712f9d2e4615700c6809942536dd3f441b9ba650a310/wrapt-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d731a4f22ed6ffa4cb551b4d2b0c24ff940c27a88edaf8e3490a5ee3a05aef71", size = 61207, upload-time = "2025-10-19T23:46:52.558Z" }, - { url = "https://files.pythonhosted.org/packages/23/d9/67cb93da492eb0a1cb17b7ed18220d059e58f00467ce6728b674d3441b3d/wrapt-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e02ab8c0ac766a5a6e81cd3b6cc39200c69051826243182175555872522bd5a", size = 61748, upload-time = "2025-10-19T23:46:54.468Z" }, - { url = "https://files.pythonhosted.org/packages/e5/be/912bbd70cc614f491b526a1d7fe85695b283deed19287b9f32460178c54d/wrapt-2.0.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:895870602d65d7338edb3b6a717d856632ad9f14f7ff566214e4fb11f0816649", size = 120424, upload-time = "2025-10-19T23:46:57.575Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e1/10df8937e7da2aa9bc3662a4b623e51a323c68f42cad7b13f0e61a700ce2/wrapt-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b9ad4fab76a0086dc364c4f17f39ad289600e73ef5c6e9ab529aff22cac1ac3", size = 122804, upload-time = "2025-10-19T23:46:59.308Z" }, - { url = "https://files.pythonhosted.org/packages/f3/60/576751b1919adab9f63168e3b5fd46c0d1565871b1cc4c2569503ccf4be6/wrapt-2.0.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7ca0562606d7bad2736b2c18f61295d61f50cd3f4bfc51753df13614dbcce1b", size = 117398, upload-time = "2025-10-19T23:46:55.814Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/243411f360cc27bae5f8e21c16f1a8d87674c5534f4558e8a97c1e0d1c6f/wrapt-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe089d9f5a4a3dea0108a8ae34bced114d0c4cca417bada1c5e8f42d98af9050", size = 121230, upload-time = "2025-10-19T23:47:01.347Z" }, - { url = "https://files.pythonhosted.org/packages/d6/23/2f21f692c3b3f0857cb82708ce0c341fbac55a489d4025ae4e3fd5d5de8c/wrapt-2.0.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e761f2d2f8dbc80384af3d547b522a80e67db3e319c7b02e7fd97aded0a8a678", size = 116296, upload-time = "2025-10-19T23:47:02.659Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ed/678957fad212cfb1b65b2359d62f5619f5087d1d1cf296c6a996be45171c/wrapt-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:17ba1bdc52d0c783481850996aa26cea5237720769197335abea2ae6b4c23bc0", size = 119602, upload-time = "2025-10-19T23:47:03.775Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e3/aeb4c3b052d3eed95e61babc20dcb1a512651e098cca4b84a6896585c06a/wrapt-2.0.0-cp314-cp314-win32.whl", hash = "sha256:f73318741b141223a4674ba96992aa2291b1b3f7a5e85cb3c2c964f86171eb45", size = 58649, upload-time = "2025-10-19T23:47:07.382Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2a/a71c51cb211798405b59172c7df5789a5b934b18317223cf22e0c6f852de/wrapt-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8e08d4edb13cafe7b3260f31d4de033f73d3205774540cf583bffaa4bec97db9", size = 60897, upload-time = "2025-10-19T23:47:04.862Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a5/acc5628035d06f69e9144cca543ca54c33b42a5a23b6f1e8fa131026db89/wrapt-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:af01695c2b7bbd8d67b869d8e3de2b123a7bfbee0185bdd138c2775f75373b83", size = 59306, upload-time = "2025-10-19T23:47:05.883Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e6/1318ca07d7fcee57e4592a78dacd9d5493b8ddd971c553a62904fb2c0cf2/wrapt-2.0.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:057f02c13cce7b26c79624c06a3e1c2353e6dc9708525232232f6768118042ca", size = 81987, upload-time = "2025-10-19T23:47:08.7Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bf/ffac358ddf61c3923d94a8b0e7620f2af1cd1b637a0fe4963a3919aa62b7/wrapt-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:79bdd84570267f3f43d609c892ae2d30b91ee4b8614c2cbfd311a2965f1c9bdb", size = 62902, upload-time = "2025-10-19T23:47:10.248Z" }, - { url = "https://files.pythonhosted.org/packages/b5/af/387c51f9e7b544fe95d852fc94f9f3866e3f7d7d39c2ee65041752f90bc2/wrapt-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:93c8b4f4d54fd401a817abbfc9bf482aa72fd447f8adf19ce81d035b3f5c762c", size = 63635, upload-time = "2025-10-19T23:47:11.746Z" }, - { url = "https://files.pythonhosted.org/packages/7c/99/d38d8c80b9cc352531d4d539a17e3674169a5cc25a7e6e5e3c27bc29893e/wrapt-2.0.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e09ffd31001dce71c2c2a4fc201bdba9a2f9f62b23700cf24af42266e784741", size = 152659, upload-time = "2025-10-19T23:47:15.344Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2a/e154432f274e22ecf2465583386c5ceffa5e0bab3947c1c5b26cc8e7b275/wrapt-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d87c285ff04e26083c4b03546e7b74df7ba4f1f32f1dcb92e9ac13c2dbb4c379", size = 158818, upload-time = "2025-10-19T23:47:17.569Z" }, - { url = "https://files.pythonhosted.org/packages/c5/7a/3a40c453300e2898e99c27495b8109ff7cd526997d12cfb8ebd1843199a4/wrapt-2.0.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e52e50ea0a72ea48d1291cf8b8aaedcc99072d9dc5baba6b820486dcf4c67da8", size = 146113, upload-time = "2025-10-19T23:47:13.026Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e2/3116a9eade8bea2bf5eedba3fa420e3c7d193d4b047440330d8eaf1098de/wrapt-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fd4c95536975895f32571073446e614d5e2810b666b64955586dcddfd438fd3", size = 155689, upload-time = "2025-10-19T23:47:19.397Z" }, - { url = "https://files.pythonhosted.org/packages/43/1c/277d3fbe9d177830ab9e54fe9253f38455b75a22d639a4bd9fa092d55ae5/wrapt-2.0.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d6ebfe9283209220ed9de80a3e9442aab8fc2be5a9bbf8491b99e02ca9349a89", size = 144403, upload-time = "2025-10-19T23:47:20.779Z" }, - { url = "https://files.pythonhosted.org/packages/d8/37/ab6ddaf182248aac5ed925725ef4c69a510594764665ecbd95bdd4481f16/wrapt-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5d3ebd784804f146b7ea55359beb138e23cc18e5a5cc2cf26ad438723c00ce3a", size = 150307, upload-time = "2025-10-19T23:47:22.604Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d7/df9e2d8040a3af618ff9496261cf90ca4f886fd226af0f4a69ac0c020c3b/wrapt-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:9b15940ae9debc8b40b15dc57e1ce4433f7fb9d3f8761c7fab1ddd94cb999d99", size = 60557, upload-time = "2025-10-19T23:47:26.73Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c2/502bd4557a3a9199ea73cc5932cf83354bd362682162f0b14164d2e90216/wrapt-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a0efbbc06d3e2077476a04f55859819d23206600b4c33f791359a8e6fa3c362", size = 63988, upload-time = "2025-10-19T23:47:23.826Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/632b13942f45db7af709f346ff38b8992c8c21b004e61ab320b0dec525fe/wrapt-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7fec8a9455c029c8cf4ff143a53b6e7c463268d42be6c17efa847ebd2f809965", size = 60584, upload-time = "2025-10-19T23:47:25.396Z" }, - { url = "https://files.pythonhosted.org/packages/00/5c/c34575f96a0a038579683c7f10fca943c15c7946037d1d254ab9db1536ec/wrapt-2.0.0-py3-none-any.whl", hash = "sha256:02482fb0df89857e35427dfb844319417e14fae05878f295ee43fa3bf3b15502", size = 43998, upload-time = "2025-10-19T23:47:52.858Z" }, +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ] [[package]]