From 5db0f12900e195bd590424584434c9ae144e32b8 Mon Sep 17 00:00:00 2001 From: Paulo Date: Sun, 4 Oct 2026 21:27:05 +0200 Subject: [PATCH] Add E2B sandbox provider with Tailscale SSH --- docs/deploy.md | 12 +- docs/e2b.md | 78 +++++++ images/e2b/build.py | 27 +++ pyproject.toml | 4 + src/providers/__init__.py | 1 + src/providers/e2b/__init__.py | 9 + src/providers/e2b/api.py | 110 ++++++++++ src/providers/e2b/provider.py | 89 ++++++++ src/providers/e2b/settings.py | 20 ++ src/providers/e2b/tests/__init__.py | 0 src/providers/e2b/tests/test_provider.py | 194 ++++++++++++++++++ uv.lock | 251 ++++++++++++++++++++++- 12 files changed, 791 insertions(+), 4 deletions(-) create mode 100644 docs/e2b.md create mode 100644 images/e2b/build.py create mode 100644 src/providers/e2b/__init__.py create mode 100644 src/providers/e2b/api.py create mode 100644 src/providers/e2b/provider.py create mode 100644 src/providers/e2b/settings.py create mode 100644 src/providers/e2b/tests/__init__.py create mode 100644 src/providers/e2b/tests/test_provider.py diff --git a/docs/deploy.md b/docs/deploy.md index 0e5dde6..a45ae7b 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -96,6 +96,7 @@ account token returns `503`. See [API](api.md#service-accounts). | Provider | Sandboxes | Where | | --- | --- | --- | +| `e2b` | E2B VMs | Remote | | `exe` | exe.dev VMs | Remote | | `aws` | EC2 instances | Remote | | `hetzner` | Hetzner Cloud VMs | Remote | @@ -111,8 +112,8 @@ all provider extras. ## Local sandboxes with Docker The `docker` provider runs each sandbox as a local container with sshd, -so you can try drukbox with no cloud account or API token. The published -image includes the Docker CLI. Set `DEFAULT_HOST_PROVIDER=docker`, +so you can try drukbox with no cloud account or API token. +Set `DEFAULT_HOST_PROVIDER=docker`, `TAILSCALE_ENABLED=false`, and `UVICORN_HOST=127.0.0.1` in `drukbox.env`, then run: @@ -687,7 +688,7 @@ Docker provider: | `DOCKER_SSH_USERNAME` | `root` | In-container user callers SSH as. The entrypoint seeds its `authorized_keys`; a derived image adds the user. | | `DOCKER_BOOTSTRAP_SSH_TIMEOUT_SECONDS` | `30.0` | ssh-keyscan retry budget for a fresh container. | -The published image includes the Docker CLI. Mount the local daemon socket +Mount the local daemon socket with its supplemental group on Linux, or use `DOCKER_HOST` for a remote or rootless daemon. Drukbox mints a per-VM ed25519 key and publishes sshd on a random port at `DOCKER_SSH_HOST`. See @@ -709,3 +710,8 @@ The published image does not contain the `sbx` CLI. Mount the binary and the sbx directories of the host, as [Local microVMs with Docker Sandboxes](#local-microvms-with-docker-sandboxes) shows. + +## E2B + +See [E2B setup](e2b.md) for image preparation, all provider settings, +Tailscale SSH, and lifecycle limits. diff --git a/docs/e2b.md b/docs/e2b.md new file mode 100644 index 0000000..ec18071 --- /dev/null +++ b/docs/e2b.md @@ -0,0 +1,78 @@ +# E2B sandboxes + +Create an E2B host through the existing API: + +```json +{"provider": "e2b"} +``` + +The `image` field selects an E2B template name or ID. The provider requires +Tailscale SSH. It does not return a public SSH address or a private key. +CPU, memory, and disk are set by the template and E2B account limits. +Per-host `instance_type` and `disk_gb` are not supported. + +## Prepare the template + +Install the optional provider and build its Ubuntu template: + +```bash +uv sync --extra e2b +uv run images/e2b/build.py --name drukbox +``` + +Set `E2B_API_KEY` in the build process environment. The build creates a +template in that E2B account and uses E2B build resources. +It installs Tailscale, Bash, jq, Git, GitHub CLI, and CA tools. +It does not join the template to a tailnet. + +Use the returned template name or ID as `E2B_DEFAULT_IMAGE`. +For a custom template, keep these tools and root command access. Never +snapshot a host that has joined Tailscale into a reusable template. + +## Configure Drukbox + +| Variable | Value | +| --- | --- | +| `E2B_API_KEY` | Required account API key; can use `/run/secrets/E2B_API_KEY` | +| `E2B_DEFAULT_IMAGE` | Required prepared template name or ID | +| `E2B_SESSION_TIMEOUT_SECONDS` | VM lifetime; default `3600`, range `600`–`86400` | +| `E2B_API_TIMEOUT` | API request timeout in seconds; default `150` | +| `E2B_BOOTSTRAP_SSH_TIMEOUT_SECONDS` | SSH readiness budget; default `120` | +| `TAILSCALE_ENABLED` | Required: `true` | + +Configure the Tailscale credentials and tailnet as described in +[Networking](networking.md). The service image includes the E2B extra. + +E2B permits up to one continuous hour on Base and 24 hours on Pro. +Set the lifetime within the account limit. Drukbox stores a lease deadline +60 seconds before this lifetime ends. Default leases and pool leases stay +within that deadline. Permanent leases and explicit leases beyond it fail. +Renewal cannot move the stored deadline. Changing the setting affects new +hosts only. + +Drukbox explicitly disables automatic pause and resume. E2B kills the VM +when its timeout ends. Drukbox deletes it earlier when its lease ends or a +caller deletes it. Keep the janitor running for DB and Tailscale cleanup. + +Bootstrap writes the host environment, installs the secrets proxy CA, and +joins Tailscale. Without systemd, Tailscale uses userspace networking. This +permits inbound Tailscale SSH but adds no kernel routes for applications. +The secrets proxy must be reachable through the VM's normal network. +Public E2B ingress is disabled. + +Deletion finds all running and paused VMs with the host name and +`SERVICE_LABEL` in their metadata. Do not change those metadata fields or +`SERVICE_LABEL` while hosts exist. A failed bootstrap triggers cleanup; +failed cleanup leaves the host record for the janitor or a deletion retry. +The adapter does not retry create requests after an uncertain response. + +`/doctor` checks API authentication. It does not provision a VM or prove +Tailscale connectivity. Live provisioning and SSH require a separate test +with an E2B account and a configured tailnet. + +## Sources + +- [E2B lifecycle and runtime limits](https://docs.e2b.dev/sandbox) +- [E2B template images](https://docs.e2b.dev/template/base-image) +- [E2B SSH transport](https://docs.e2b.dev/sandbox/ssh-access) +- [E2B API reference](https://docs.e2b.dev/api-reference/sandboxes/create-sandbox-v2) diff --git a/images/e2b/build.py b/images/e2b/build.py new file mode 100644 index 0000000..ab7ac18 --- /dev/null +++ b/images/e2b/build.py @@ -0,0 +1,27 @@ +import argparse + +from e2b import Template, default_build_logger + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Build the Drukbox E2B template") + parser.add_argument("--name", default="drukbox") + arguments = parser.parse_args() + template = ( + Template() + .from_ubuntu_image("24.04") + .apt_install( + ["bash", "ca-certificates", "curl", "sudo", "jq", "git", "gh", "openssh-server"] + ) + .run_cmd( + "curl -fsSL https://tailscale.com/install.sh -o /tmp/install-tailscale.sh" + " && sh /tmp/install-tailscale.sh && rm /tmp/install-tailscale.sh", + user="root", + ) + ) + Template.build( + template, + arguments.name, + cpu_count=2, + memory_mb=1024, + on_build_logs=default_build_logger(), + ) diff --git a/pyproject.toml b/pyproject.toml index 5e8be6b..b41b501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,9 @@ dependencies = [ aws = [ "aioboto3>=13", ] +e2b = [ + "e2b>=2.52.0,<3", +] [project.urls] Homepage = "https://github.com/czpython/drukbox" @@ -85,6 +88,7 @@ dev = [ # collection time. Keep it in the dev group so a bare `uv sync` runs the # full suite and pyright without needing --all-extras. "aioboto3>=13", + "e2b>=2.52.0,<3", "pyright>=1.1", "pytest>=8.0", "pytest-asyncio>=0.25", diff --git a/src/providers/__init__.py b/src/providers/__init__.py index 9410318..b17286d 100644 --- a/src/providers/__init__.py +++ b/src/providers/__init__.py @@ -1,6 +1,7 @@ import providers.aws import providers.docker import providers.docker_sbx +import providers.e2b import providers.exe import providers.exoscale import providers.hetzner diff --git a/src/providers/e2b/__init__.py b/src/providers/e2b/__init__.py new file mode 100644 index 0000000..0443d28 --- /dev/null +++ b/src/providers/e2b/__init__.py @@ -0,0 +1,9 @@ +try: + import e2b # noqa: F401 +except ImportError: + pass +else: + from providers.e2b.provider import E2BProvider + from providers.registry import register_vm_provider + + register_vm_provider(E2BProvider) diff --git a/src/providers/e2b/api.py b/src/providers/e2b/api.py new file mode 100644 index 0000000..1473e0f --- /dev/null +++ b/src/providers/e2b/api.py @@ -0,0 +1,110 @@ +import shlex +from collections.abc import Iterator +from contextlib import contextmanager + +import httpx +from e2b import AsyncSandbox, CommandExitException, SandboxQuery, SandboxState +from e2b import exceptions as errors + +from providers.exceptions import ( + ProviderAuthError, + ProviderCommandError, + ProviderNotFoundError, + ProviderTransportError, +) + +from .settings import E2BSettings + + +@contextmanager +def e2b_request() -> Iterator[None]: + try: + yield + except errors.AuthenticationException: + raise ProviderAuthError("E2B API authentication failed") from None + except errors.NotFoundException: + raise ProviderNotFoundError("E2B resource was not found") from None + except (errors.InvalidArgumentException, CommandExitException): + raise ProviderCommandError("E2B rejected the request or bootstrap command") from None + except errors.SandboxException as exc: + if exc.status_code == 403: + raise ProviderAuthError("E2B API authentication failed") from None + if exc.status_code == 404: + raise ProviderNotFoundError("E2B resource was not found") from None + if exc.status_code and 400 <= exc.status_code < 500 and exc.status_code != 429: + raise ProviderCommandError("E2B API rejected the request") from None + raise ProviderTransportError("E2B request failed") from None + except ( + errors.ServiceBusyException, + httpx.RequestError, + OSError, + ValueError, + KeyError, + TypeError, + ): + raise ProviderTransportError("E2B request failed") from None + + +class E2BAPI: + def __init__(self, settings: E2BSettings) -> None: + self.settings = settings + + async def create_sandbox(self, name: str, *, image: str, label: str) -> AsyncSandbox: + with e2b_request(): + return await AsyncSandbox.create( + template=image, + timeout=self.settings.session_timeout_seconds, + metadata={"name": name, "managed-by": label}, + lifecycle={"on_timeout": "kill", "auto_resume": False}, + network={"allow_public_traffic": False}, + api_key=self.settings.api_key, + request_timeout=self.settings.api_timeout, + retries=0, + ) + + async def bootstrap(self, sandbox: AsyncSandbox, script: str) -> None: + with e2b_request(): + await sandbox.commands.run( + f"bash -e -c {shlex.quote(script)}", + user="root", + timeout=120, + request_timeout=self.settings.api_timeout, + ) + + async def delete_sandbox(self, name: str, *, label: str) -> None: + metadata = {"name": name, "managed-by": label} + + with e2b_request(): + pages = AsyncSandbox.list( + query=SandboxQuery( + metadata=metadata, state=[SandboxState.RUNNING, SandboxState.PAUSED] + ), + api_key=self.settings.api_key, + request_timeout=self.settings.api_timeout, + retries=0, + ) + sandbox_ids: list[str] = [] + while pages.has_next: + for sandbox in await pages.next_items(): + if all(sandbox.metadata.get(key) == value for key, value in metadata.items()): + sandbox_ids.append(sandbox.sandbox_id) + if not sandbox_ids: + raise ProviderNotFoundError(f"E2B sandbox {name!r} was not found") + for sandbox_id in sandbox_ids: + await AsyncSandbox.kill( + sandbox_id, + api_key=self.settings.api_key, + request_timeout=self.settings.api_timeout, + retries=0, + ) + + async def diagnose(self) -> str: + with e2b_request(): + pages = AsyncSandbox.list( + limit=1, + api_key=self.settings.api_key, + request_timeout=self.settings.api_timeout, + retries=0, + ) + await pages.next_items() + return "E2B sandbox API authentication succeeded" diff --git a/src/providers/e2b/provider.py b/src/providers/e2b/provider.py new file mode 100644 index 0000000..273805b --- /dev/null +++ b/src/providers/e2b/provider.py @@ -0,0 +1,89 @@ +import asyncio +import contextlib +from datetime import timedelta +from typing import ClassVar, Self + +from core.settings import get_settings +from providers import environment +from providers.base import VMCreateResult, VMProvider +from providers.exceptions import ( + ProviderAuthError, + ProviderCommandError, + ProviderError, + ProviderNotFoundError, + ProviderTransportError, +) + +from .api import E2BAPI +from .settings import E2BSettings + + +class E2BProvider(VMProvider): + name: ClassVar[str] = "e2b" + diagnose_hint: ClassVar[str] = "check_e2b_and_tailscale_settings" + + def __init__(self, api: E2BAPI, settings: E2BSettings, *, service_label: str) -> None: + self.api = api + self.settings = settings + self.service_label = service_label + self.max_lifetime = timedelta(seconds=settings.session_timeout_seconds - 60) + + @classmethod + def from_settings(cls) -> Self: + settings = E2BSettings() # pyright: ignore[reportCallIssue] + return cls(E2BAPI(settings), settings, service_label=get_settings().service_label) + + @property + def default_image(self) -> str: + return self.settings.default_image + + @property + def bootstrap_ssh_timeout_seconds(self) -> float: + return self.settings.bootstrap_ssh_timeout_seconds + + async def create_vm( + self, + *, + name: str, + image: str, + env: dict[str, str] | None = None, + setup_script: str | None = None, + instance_type: str | None = None, + disk_gb: int | None = None, + ) -> VMCreateResult: + if not setup_script: + raise ProviderCommandError("E2B requires TAILSCALE_ENABLED=true") + try: + script = environment.get_cloud_init(setup_script, env) + except ValueError as exc: + raise ProviderCommandError("Invalid E2B environment") from exc + try: + sandbox = await self.api.create_sandbox(name, image=image, label=self.service_label) + except (ProviderAuthError, ProviderNotFoundError) as exc: + raise ProviderCommandError("E2B API key or template was rejected") from exc + except (ProviderTransportError, asyncio.CancelledError): + with contextlib.suppress(ProviderError): + await self.delete_vm(name) + raise + try: + await self.api.bootstrap(sandbox, script) + except asyncio.CancelledError: + with contextlib.suppress(ProviderError): + await self.delete_vm(name) + raise + except ProviderError as exc: + with contextlib.suppress(ProviderError): + await self.delete_vm(name) + raise ProviderTransportError("E2B sandbox bootstrap failed") from exc + return VMCreateResult(provider_id=sandbox.sandbox_id, name=name, ssh_username="root") + + async def delete_vm(self, name: str) -> None: + await self.api.delete_sandbox(name, label=self.service_label) + + async def diagnose(self) -> str: + if not get_settings().tailscale_enabled: + raise ProviderCommandError("E2B requires TAILSCALE_ENABLED=true") + return await self.api.diagnose() + + async def aclose(self) -> None: + """The E2B SDK owns its shared HTTP clients.""" diff --git a/src/providers/e2b/settings.py b/src/providers/e2b/settings.py new file mode 100644 index 0000000..99a7c64 --- /dev/null +++ b/src/providers/e2b/settings.py @@ -0,0 +1,20 @@ +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + +from core.settings import get_secrets_dir + + +class E2BSettings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + env_prefix="E2B_", + extra="ignore", + hide_input_in_errors=True, + secrets_dir=get_secrets_dir(), + ) + + api_key: str + default_image: str + session_timeout_seconds: int = Field(default=3600, ge=600, le=86400) + api_timeout: float = Field(default=150, gt=0) + bootstrap_ssh_timeout_seconds: float = Field(default=120, gt=0) diff --git a/src/providers/e2b/tests/__init__.py b/src/providers/e2b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/providers/e2b/tests/test_provider.py b/src/providers/e2b/tests/test_provider.py new file mode 100644 index 0000000..7e8851f --- /dev/null +++ b/src/providers/e2b/tests/test_provider.py @@ -0,0 +1,194 @@ +import asyncio +import json +import traceback +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +import httpx +import pytest +import respx +from e2b import CommandExitException + +from providers.e2b.api import E2BAPI +from providers.e2b.provider import E2BProvider +from providers.e2b.settings import E2BSettings +from providers.exceptions import ( + ProviderAuthError, + ProviderCommandError, + ProviderNotFoundError, + ProviderTransportError, +) +from providers.registry import get_provider_names + +BASE = "https://api.e2b.app" +COMMAND = "e2b.sandbox_async.commands.command.Commands.run" +SANDBOX = { + "sandboxID": "sandbox-1", + "templateID": "template-1", + "clientID": "client-1", + "envdVersion": "0.4.0", + "envdAccessToken": "envd-test-token", +} +LISTED = { + **SANDBOX, + "startedAt": "2026-10-04T12:00:00Z", + "endAt": "2026-10-04T13:00:00Z", + "cpuCount": 2, + "memoryMB": 1024, + "diskSizeMB": 1024, + "state": "running", + "metadata": {"name": "sb-test", "managed-by": "drukbox"}, +} + + +@pytest.fixture +def sdk_transport(monkeypatch: pytest.MonkeyPatch): + # The SDK's Rust transport bypasses respx's httpcore interception. + monkeypatch.setattr( + "e2b.api.client_async.get_transport", + lambda *args, **kwargs: httpx.MockTransport(respx.mock.async_handler), + ) + + +@pytest.fixture +def provider(sdk_transport) -> E2BProvider: + settings = E2BSettings(api_key="e2b_test_key", default_image="drukbox:v1") + return E2BProvider(E2BAPI(settings), settings, service_label="drukbox") + + +@respx.mock +async def test_create_uses_template_and_enforces_fixed_lifetime(provider: E2BProvider): + creation = respx.post(BASE + "/v2/sandboxes").respond(201, json=SANDBOX) + with patch(COMMAND, new_callable=AsyncMock) as command: + result = await provider.create_vm( + name="sb-test", image="custom:v2", env={"FOO": "hello world"}, setup_script="true" + ) + request = creation.calls[0].request + body = json.loads(request.content) + assert request.headers["X-API-Key"] == "e2b_test_key" + assert body["templateID"] == "custom:v2" + assert body["timeout"] == 3600 + assert body["metadata"] == {"name": "sb-test", "managed-by": "drukbox"} + assert body["autoPause"] is False + assert body["autoResume"] == {"enabled": False} + assert body["network"]["allowPublicTraffic"] is False + assert command.call_args.kwargs["user"] == "root" + assert command.call_args.kwargs["timeout"] == 120 + assert "/etc/environment" in command.call_args.args[0] + assert "hello world" in command.call_args.args[0] + assert result.provider_id == "sandbox-1" + assert result.name == "sb-test" + assert result.ssh_username == "root" + assert not result.ssh_host + assert provider.max_lifetime == timedelta(seconds=3540) + + +@respx.mock +async def test_no_tailscale_or_invalid_environment_makes_no_request(provider: E2BProvider): + with pytest.raises(ProviderCommandError, match="TAILSCALE_ENABLED"): + await provider.create_vm(name="sb-test", image="template") + with pytest.raises(ProviderCommandError, match="environment"): + await provider.create_vm( + name="sb-test", image="template", setup_script="true", env={"BAD-KEY": "value"} + ) + assert not respx.calls + + +@respx.mock +@pytest.mark.parametrize("status", [400, 401, 403, 404]) +async def test_rejected_creation_does_not_delete(provider: E2BProvider, status: int): + creation = respx.post(BASE + "/v2/sandboxes").respond( + status, json={"code": status, "message": "sensitive details"} + ) + with pytest.raises(ProviderCommandError) as error: + await provider.create_vm(name="sb-test", image="template", setup_script="true") + assert "sensitive" not in str(error.value) + assert creation.call_count == 1 + assert len(respx.calls) == 1 + + +@respx.mock +async def test_lost_create_response_is_not_retried_and_owned_vm_is_deleted(provider: E2BProvider): + creation = respx.post(BASE + "/v2/sandboxes").mock(side_effect=httpx.ReadTimeout("lost")) + respx.get(BASE + "/v2/sandboxes").respond(200, json=[LISTED]) + deletion = respx.delete(BASE + "/sandboxes/sandbox-1").respond(204) + with pytest.raises(ProviderTransportError): + await provider.create_vm(name="sb-test", image="template", setup_script="true") + assert creation.call_count == 1 + assert deletion.called + + +@respx.mock +@pytest.mark.parametrize( + "failure", + [ + CommandExitException(stderr="secret", stdout="secret", exit_code=1, error=None), + asyncio.CancelledError(), + ], +) +async def test_failed_or_cancelled_bootstrap_cleans_up( + provider: E2BProvider, failure: BaseException +): + respx.post(BASE + "/v2/sandboxes").respond(201, json=SANDBOX) + respx.get(BASE + "/v2/sandboxes").respond(200, json=[LISTED]) + deletion = respx.delete(BASE + "/sandboxes/sandbox-1").respond(204) + with ( + patch(COMMAND, new_callable=AsyncMock, side_effect=failure), + pytest.raises((ProviderTransportError, asyncio.CancelledError)) as error, + ): + await provider.create_vm(name="sb-test", image="template", setup_script="true") + assert "secret" not in "".join(traceback.format_exception(error.value)) + assert deletion.called + + +@respx.mock +async def test_delete_collects_all_pages_before_deleting_and_checks_ownership( + provider: E2BProvider, +): + listing = respx.get(BASE + "/v2/sandboxes").mock( + side_effect=[ + httpx.Response(200, json=[LISTED], headers={"X-Next-Token": "page-2"}), + httpx.Response( + 200, + json=[ + {**LISTED, "sandboxID": "sandbox-2", "state": "paused"}, + {**LISTED, "sandboxID": "foreign", "metadata": {"managed-by": "other"}}, + ], + ), + ] + ) + first = respx.delete(BASE + "/sandboxes/sandbox-1").respond(204) + second = respx.delete(BASE + "/sandboxes/sandbox-2").respond( + 404, json={"code": 404, "message": "gone"} + ) + await provider.delete_vm("sb-test") + assert listing.call_count == 2 + assert listing.calls[1].request.url.params["nextToken"] == "page-2" + assert "managed-by" in listing.calls[0].request.url.params["metadata"] + assert listing.calls[0].request.url.params["state"] == "running,paused" + assert first.called and second.called + assert [call.request.method for call in respx.calls] == ["GET", "GET", "DELETE", "DELETE"] + + +@respx.mock +async def test_missing_sandbox_raises_not_found(provider: E2BProvider): + respx.get(BASE + "/v2/sandboxes").respond(200, json=[]) + with pytest.raises(ProviderNotFoundError): + await provider.delete_vm("sb-test") + + +@respx.mock +async def test_diagnose_authenticates_with_one_read(provider: E2BProvider): + listing = respx.get(BASE + "/v2/sandboxes").respond(200, json=[]) + assert "authentication succeeded" in await provider.diagnose() + assert listing.calls[0].request.url.params["limit"] == "1" + listing.respond(401, json={"code": 401, "message": "secret"}) + with pytest.raises(ProviderAuthError, match="authentication failed"): + await provider.diagnose() + + +def test_provider_registration_and_lifetime_settings(): + assert "e2b" in get_provider_names() + for timeout in [599, 86401]: + with pytest.raises(ValueError): + E2BSettings(api_key="test", default_image="template", session_timeout_seconds=timeout) diff --git a/uv.lock b/uv.lock index 2d86843..e09e857 100644 --- a/uv.lock +++ b/uv.lock @@ -312,6 +312,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, ] +[[package]] +name = "bracex" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/01/5f394b8bcd6e5b92f73130990960423bbb19711f906bd9fe9ea5557c667c/bracex-3.0.1.tar.gz", hash = "sha256:4e38e32392e4a4780fe15d644bfc7c8514057cfc3861e060b11814ce829c25e4", size = 44019, upload-time = "2026-07-20T13:43:00.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/8f/6f7273a7adb8d73fc8d21ede4376a3e475e52f98435c6007f69100dec8ca/bracex-3.0.1-py3-none-any.whl", hash = "sha256:6523ad83aeb5098a4ee597cff0f964442ff74e460bd3fafaffab6a013ff2288c", size = 11940, upload-time = "2026-07-20T13:42:59.268Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -412,6 +421,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "connectrpc" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py" }, + { name = "pyqwest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/b5/63e14ab9d4d4cc58818562db4120a35fa7454e3035633da41bdc6b712abd/connectrpc-0.11.1.tar.gz", hash = "sha256:18277f7838847b4271ca38d40c7d2387b5a2ea6a29f240689c19e1ec84aaff66", size = 46222, upload-time = "2026-07-15T06:33:31.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/ac/aa647675812cd075f2cb9acb00d62f4f2cbcbc6736049a62342b7371ce5b/connectrpc-0.11.1-py3-none-any.whl", hash = "sha256:8a52e2e92a485fa9681c1101a79a5ebeb31807e3ea3d5aabd41f484a6398bc7b", size = 64991, upload-time = "2026-07-15T06:33:29.396Z" }, +] + [[package]] name = "cryptography" version = "49.0.0" @@ -468,6 +490,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, ] +[[package]] +name = "dockerfile-parse" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/df/929ee0b5d2c8bd8d713c45e71b94ab57c7e11e322130724d54f469b2cd48/dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc", size = 24556, upload-time = "2023-07-18T13:36:07.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/6c/79cd5bc1b880d8c1a9a5550aa8dacd57353fa3bb2457227e1fb47383eb49/dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6", size = 14845, upload-time = "2023-07-18T13:36:06.052Z" }, +] + [[package]] name = "drukbox" version = "0.0.1" @@ -493,10 +524,14 @@ dependencies = [ aws = [ { name = "aioboto3" }, ] +e2b = [ + { name = "e2b" }, +] [package.dev-dependencies] dev = [ { name = "aioboto3" }, + { name = "e2b" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -512,6 +547,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.14" }, { name = "asyncssh", specifier = ">=2.14" }, { name = "cryptography", specifier = ">=43" }, + { name = "e2b", marker = "extra == 'e2b'", specifier = ">=2.52.0,<3" }, { name = "fastapi", specifier = ">=0.115" }, { name = "greenlet", specifier = ">=3.0" }, { name = "httpx", specifier = ">=0.28" }, @@ -522,11 +558,12 @@ requires-dist = [ { name = "uuid6", specifier = ">=2024.7.10" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34" }, ] -provides-extras = ["aws"] +provides-extras = ["aws", "e2b"] [package.metadata.requires-dev] dev = [ { name = "aioboto3", specifier = ">=13" }, + { name = "e2b", specifier = ">=2.52.0,<3" }, { name = "pyright", specifier = ">=1.1" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.25" }, @@ -534,6 +571,29 @@ dev = [ { name = "ruff", specifier = ">=0.9" }, ] +[[package]] +name = "e2b" +version = "2.52.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "connectrpc" }, + { name = "dockerfile-parse" }, + { name = "h2" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "protobuf-py" }, + { name = "pyqwest" }, + { name = "python-dateutil" }, + { name = "rich" }, + { name = "typing-extensions" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/c4/560e7de54cece3573086f09d304e595729964aa18593b437125d9f8bd926/e2b-2.52.0.tar.gz", hash = "sha256:58c0042609513799341d4c1837e2581a4a5e73d3d62c9f1c57495c5e23eedee2", size = 235496, upload-time = "2026-10-01T10:44:02.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/71/1a01b7e4486b5962eaade7dba7026876a1b4569e49b464e82912d8801e38/e2b-2.52.0-py3-none-any.whl", hash = "sha256:6f4b5084b09986db43e99c7405bae6b14dc56984a69bbb1dc0687f1c71515cfb", size = 396242, upload-time = "2026-10-01T10:44:00.38Z" }, +] + [[package]] name = "fastapi" version = "0.136.1" @@ -721,6 +781,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -785,6 +867,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.13" @@ -824,6 +915,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -898,6 +1001,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -1024,6 +1136,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -1153,6 +1277,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "protobuf-py" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py-ext", marker = "(platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/ed/02fd902d9c51b7ff53dfc9a745eb11490722edfd30073af889e171f07b8e/protobuf_py-0.1.1.tar.gz", hash = "sha256:6bd08ac4d8f1661965bbe2685429d79043704cdd1ee720a7a89617331742240b", size = 133525, upload-time = "2026-06-24T19:02:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/00/1b3775aca1c70e3007e06ef5996f6bb9b3a32341eb0cce3ffb6effad8dec/protobuf_py-0.1.1-py3-none-any.whl", hash = "sha256:efc4f50f275ed6dae10a1f30bb81ad1a75368557b3ff22a532b7a472050368f1", size = 181656, upload-time = "2026-06-24T19:01:29.556Z" }, +] + +[[package]] +name = "protobuf-py-ext" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/05/6dc9ccff1e8159eb9a144e6d3c4acfd2211cd4fcd20c34fe9155d17d6a7f/protobuf_py_ext-0.1.1.tar.gz", hash = "sha256:e85bfdfdb3ed50634db8ccc7429dd9286520109489c735463971a418707b4fef", size = 31912, upload-time = "2026-06-24T19:02:16.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/70/2b5d62a60a2e0d88ecde1ae98db3132bcd2672fb39c8d581b82b34ac0bd8/protobuf_py_ext-0.1.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:310039e03cb15181781a0b78017419f6d4ee302e988c3c70b87f1facdf05532d", size = 306008, upload-time = "2026-06-24T19:01:31.399Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d6/73c06bb4cac2e04c0adc154965fe8b6520224bd737fd7e73bba405056321/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ec8348d1ba045f79b2fedd14e40cca36f2a41b52f2c4fdf55a60c58add2353", size = 314769, upload-time = "2026-06-24T19:01:32.89Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/e4e6bc6096b66d1c82639a1b501147f16ee65d32567304b987d002e2c666/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:182798e4861aba72d05855bd06febe4926aa7265e6f444a1b8af5252beee4f7e", size = 328122, upload-time = "2026-06-24T19:01:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/1d792a40d0f0a0f914f1dfa8bb5e9573ca0ecd5fe5cecf80d77193212abd/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9eb25c3a329c0551cc86b209a5e5d8ecb8d834b9924a3aa019377853a703b6d3", size = 492338, upload-time = "2026-06-24T19:01:36.103Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/5ad329223c905e5c530cae38ae24cde3584d8ab7e09457f2a01afce80e60/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aaecbde82bef10c7c40578cbb61b7a19896bf7fa450972050a3bb302acb7d5d6", size = 541578, upload-time = "2026-06-24T19:01:37.481Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c7/01bd8a8bfe2df4b07aafac12ccfb7b7ebe2303f65296a9e02fcafa28ff3e/protobuf_py_ext-0.1.1-cp310-abi3-win_amd64.whl", hash = "sha256:6b0c615c48e95acc53cf33e9310eeaff8b30d2d7555bf93e7bca8fb4f40e9a5c", size = 251319, upload-time = "2026-06-24T19:01:38.946Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/914065538b5db54b6a920b5af38c1ef142252ea1eea711820435fa259fac/protobuf_py_ext-0.1.1-cp310-abi3-win_arm64.whl", hash = "sha256:72956cd0af5dee24b41c6f5ba5e42622d17e6d555002b5efc1634e27a1446de2", size = 241635, upload-time = "2026-06-24T19:01:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/0b5d73cab815fd50615cb87a02e04e8332f9e27380c890aba1459cf7e919/protobuf_py_ext-0.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f0cd58620f415a3534d195358338f4999a774e12510f91c592b38d13d37388", size = 304903, upload-time = "2026-06-24T19:01:41.796Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/14c120b9ac36a0e11bb05e482fa6ae322de583a8e1068e4859035c289947/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21572764f625d829604fc4d83635f533f35775f11c6da142345b3f3c8d64bd09", size = 316148, upload-time = "2026-06-24T19:01:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/35/54/7c00a1a9783c3ba74f6b2f5d2f049f09037bbd489c465c09a34f32fb611b/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f14f00c2678bfbe7ad46f057b9f9938c1677bcf39e405e163e272c6f9814b8", size = 326458, upload-time = "2026-06-24T19:01:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0e/81fa50c0d6c4d664e5be6d0a3c4f2c7edfef87ab12d0bac764abca1f2955/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1610865622e2e27568277ca63d8d2d23dcc55eaa767398865c21539ddf4ce24d", size = 493596, upload-time = "2026-06-24T19:01:46.344Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/3183cf6e4de62846c20549654a1d573dae51ca06aaf7fed06accdfab74f6/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8995efba9476e1ea18ef9873306871dba697308994bc2766558fec3387acc3de", size = 539656, upload-time = "2026-06-24T19:01:48.21Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6c/09841f3dbe7c3d4b3f2779f8f2832ac23fb96ac8ab71674e91af732cf9ff/protobuf_py_ext-0.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba61d49dace8f874361583030a7c48139b42eb37c9ffbb1e7e8a227a51576f44", size = 305017, upload-time = "2026-06-24T19:01:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/52/43/e450b5e202f6715274acc9acd9f9769908cbdeab861a53c798fcbd467d35/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a9c2f026096ca4c595c89a297067ef371e241d3ff8e1f6d7c779aa8419cdca7", size = 316215, upload-time = "2026-06-24T19:01:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/97/5c/23e79b35f1b5755b9bdc0c0c9b8cd8abababaef1a1639608d8a96bb61a9d/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf78707a040b9294e5e1ec4a1875f0046acfe52e92150cea27de4e9fc9db39bb", size = 326419, upload-time = "2026-06-24T19:01:52.93Z" }, + { url = "https://files.pythonhosted.org/packages/e8/de/5493de0ec12920a3b1dd72608ce0c1e8cdb7e0eb9e87accaecc5216df29e/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae4373845cbb85bdade3ef5368cc2f4b5f80bf173383afc1ab063f95644e5599", size = 493734, upload-time = "2026-06-24T19:01:54.301Z" }, + { url = "https://files.pythonhosted.org/packages/98/89/31da55b414e6332aad11d858e9a86bd36fbb324f52fa3fc6d8f1c57840a9/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2a4cc478eef7a2acc1daaebcde479a3ac2396d47e6bdc7e776ca4c4147ba8b4c", size = 539703, upload-time = "2026-06-24T19:01:55.707Z" }, + { url = "https://files.pythonhosted.org/packages/1c/71/39f231838ef06476d46ac40dd814894277a732a3688c0a0c994850b3b62f/protobuf_py_ext-0.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:359dccdc1c3eafed2a913c570bb082b8df848a1d27d548ce8385f246a7d68be2", size = 302145, upload-time = "2026-06-24T19:01:57.116Z" }, + { url = "https://files.pythonhosted.org/packages/87/f1/01c81ff5f420600366a0e6a613ce2af20834534d2b3d7523f4f3b4ddb54f/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91c38b4a10a306366443273ee03ca554537a0965bf05b7ada8e7dbdee04cc93e", size = 314541, upload-time = "2026-06-24T19:01:58.745Z" }, + { url = "https://files.pythonhosted.org/packages/86/d8/1cbf5a0298ceddc0cdbb28bf1ecaf8bd3cff54ed4ced6a1392d5226172fc/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79eee3bcbb289d6ea114eb8fe3a1469c5b59bf53e207238469f567d9c53ba56f", size = 325092, upload-time = "2026-06-24T19:02:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/af/23/8b1694616044cbfec2d18d4c6fffb03e05089c8bdb5970c6bafd60cc7fd5/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:10992141a8282a71ac3e3530d1a489efb27618d84000b9a47918cf70e5816d9b", size = 491684, upload-time = "2026-06-24T19:02:01.862Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/99997a7a6cf6989c944d9183c5deb6a045c27460add0316c7b34763891b3/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4411ffe0e06a774b83c5c71c546ce097640a25f596c45f95f53d3e3148e3f22d", size = 538048, upload-time = "2026-06-24T19:02:03.362Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/9e99ecbb68e1d5b46016d821eb1cbba9a91e6b50e71e75faf0c1e6189f0d/protobuf_py_ext-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55a17d80ea419501ff221a6627523f38a431bb33e6aa3de81ae3a7f271c49c75", size = 296337, upload-time = "2026-06-24T19:02:04.787Z" }, + { url = "https://files.pythonhosted.org/packages/af/f2/305338a28225fb54b28d6c3f5948109b9088b329a2b6f77ca610c2bdcd34/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbb518b5638403ceaa08ac4fc7dac626f45ed9b856b3517de3906cf3de4d632", size = 308961, upload-time = "2026-06-24T19:02:06.185Z" }, + { url = "https://files.pythonhosted.org/packages/60/f9/416103c93677ff2ea407704ea64fa6de9e700dc030c48360a182d91ce373/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4adc65ab6a5e4885c67fc808bcacb83d755d05b566d312a0e10c2a873f2ad4", size = 321895, upload-time = "2026-06-24T19:02:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/5a/83/eb3e81bb2f83834b7deff4cee2d56eeb1adcbc847492a56b7f910ab257db/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e97f45c49676efacfb8e95bfcb1a002bc337e618a6780be1e55df2ba5ebd2f1e", size = 486091, upload-time = "2026-06-24T19:02:09.243Z" }, + { url = "https://files.pythonhosted.org/packages/99/96/bd88fca38556b3105e4dd41d6a176e31dcc583fe979e830aeedd2f8c20ee/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:53a6b3590f6aa7f97b8ed60f62fef9b096babfeae82f7283fd3a4c405827d4f8", size = 534582, upload-time = "2026-06-24T19:02:11.227Z" }, +] + [[package]] name = "psycopg" version = "3.3.3" @@ -1371,6 +1542,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyqwest" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/ee/0ff9facfa9e7a4f6df2a770d4eaf1ad0f74165da7e8c28e888461f07604c/pyqwest-0.10.0.tar.gz", hash = "sha256:6c1a693be17d57d2c2eca4085e32c2809c53090c16719a907c90ebcf1f40dc01", size = 482248, upload-time = "2026-08-21T06:09:20.656Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ee/b1a28f57c689606cfd065d8a553841150f7daaa91d20e58dcc2c5ea191f8/pyqwest-0.10.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:aa492d5777dd145a60795ed95d9d4707a3cd1091fdcdfc93a82ac7fdc43ebacd", size = 5261059, upload-time = "2026-08-21T06:08:04.999Z" }, + { url = "https://files.pythonhosted.org/packages/dc/13/9c5046cfd6ef705bde0b620ba8a794335bcabc0839342a2a647f2427b27e/pyqwest-0.10.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:59f3f16628e518c674102e7b5fcff2101bba6abb4f6737ec5fade9b9278e6a53", size = 5134207, upload-time = "2026-08-21T06:08:06.955Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/50021dd88d82d6966ab1c27593ceaee9d1ed62fbe597c40e8dc187cfa5fd/pyqwest-0.10.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6e7db305a8318b1f3218053e87501f8f245ca8bd63e948e0282d04bf0883470", size = 5640730, upload-time = "2026-08-21T06:08:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3f/5bf6c32e9e701837a8c47ce6e3ad38978cfec8eb7bc6596181e5f9e1eaeb/pyqwest-0.10.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c757cfac5f53c8671dcb4850d5fc4c4339ea3e90636331c9318f8e3ddabc06", size = 5561462, upload-time = "2026-08-21T06:08:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/ca9ba5721461b7ce5cfaac373ab3a1723ddcc434af7430f8bd628da6e623/pyqwest-0.10.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:234b3f71e3f314d997c203d8cf829b7117edd041153f9c277d0060ab90134148", size = 5801847, upload-time = "2026-08-21T06:08:12.502Z" }, + { url = "https://files.pythonhosted.org/packages/d2/44/95593919b996a417093f598d887822b9b899e8d025588c9bfaf8c60dd812/pyqwest-0.10.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5637256a0dac0ef57e0eaa02b032014965e4a4c995e1deca1b1b97e6d1765f78", size = 5978692, upload-time = "2026-08-21T06:08:14.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/b2821ce5188457168ebb25d5ff65b1ca1bf27bc6b4a33df4bcc2357e625c/pyqwest-0.10.0-cp310-abi3-win_amd64.whl", hash = "sha256:7ea761937acf3a00d1a7e70e982949d18946e5471d1419266ab3a78bbfa19759", size = 4876627, upload-time = "2026-08-21T06:08:16.084Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/16ccef1c203fa258ce46a86aefc1a79c13b5f0b8d49627347d90eef25efd/pyqwest-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a21f1f15252a8303623b4f17b9c6de595ace11b3ade07f2adb6d07121e8191aa", size = 5274815, upload-time = "2026-08-21T06:08:17.777Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/6a87f84f571441ea43279587d4bfcad4543505918ae2b83a1ebdcfa98be5/pyqwest-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eb472c6e5d6833ebfec79db310e426eb17b01ac64c0e2c251bd9192c0d2ee0c5", size = 5123656, upload-time = "2026-08-21T06:08:19.501Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/ab69e581cf9b798b0e169f7b27fd3f8b6f9f1631bd4d3b6e22e5abaf8d8b/pyqwest-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83578e24cccd5e0dc04d60a0af7bfb43325b5f22d03ff74ff79ed0ecf553b50d", size = 5641253, upload-time = "2026-08-21T06:08:21.627Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/f1a62eebf8321ace506bd94551a01431f6a45b882225455eae3ea6e8c6d1/pyqwest-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bb511c434f79c641efb5573e5795e56dc972252f4b96e52a9636d4ece5231a4", size = 5567341, upload-time = "2026-08-21T06:08:23.346Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/51d767691973e046887f5e6d96e32142fe296823b163ccba732233a6ef72/pyqwest-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aaccd8a9db9430b2aedb5bad8ead80742cbc056b85c229516c70dc80539f906", size = 5803874, upload-time = "2026-08-21T06:08:25.096Z" }, + { url = "https://files.pythonhosted.org/packages/58/0a/d2834ccc6e59ad110718895cc65ff2a68aa6e010f0ba8fbe42a57ea33c21/pyqwest-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:73d9eb438ab4a957a1ce0619d3af8c1c1126bfb9181033b123d792fcf4224531", size = 5981518, upload-time = "2026-08-21T06:08:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/75/f3/274b4c268e9a55fbdb1b3637ac50b5bf42cd3a85d1cfbdc15c602a7b0d9c/pyqwest-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:317a74d633abe3bc5bccabf479e069c515dab9e6a755274b0ccb1d8a5bbfede3", size = 4870638, upload-time = "2026-08-21T06:08:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/8b1092f25159bf61a9470ebd35438c669b91ef553a7ee205bdec8006107b/pyqwest-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3978e794b9cfd8eaa500fb5d7aee63bc6172c605efa0abc1f62d85485bc049e1", size = 5273599, upload-time = "2026-08-21T06:08:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/75/10/54a9786123942b124c2afb9562b74e158afec7be40ef0caa0d37f615d379/pyqwest-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:715991fd4f04862cd7a9d7452daabcdbd74dff4dff55eb20c22d60382dc2a4ed", size = 5122920, upload-time = "2026-08-21T06:08:33.025Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5b/6a6bd76f91e068b9a619f62aef9fe5ef201f859ddbb6b0a11ad3875ecdda/pyqwest-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c04798bed79c1dfa0e5b0e30fb137124311083490d44d6dfbe068d3dd254349e", size = 5639577, upload-time = "2026-08-21T06:08:34.896Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/12277a24a8dd74b0a7f124c624d9ed58eccb41e2087ff1087a14d348c778/pyqwest-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b472877e73dd63fed089c2bc8fa198407f005c8c19e0a93f025ebefde01a81", size = 5565835, upload-time = "2026-08-21T06:08:36.567Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b6/b7da4a3f1fe43600154ae75e91ba7969024d886d60077dd3a1ba8e66d170/pyqwest-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:564ec360b7848b35e009038ffbca00466305a9708ab21829477f64aa8cad4c64", size = 5803078, upload-time = "2026-08-21T06:08:38.362Z" }, + { url = "https://files.pythonhosted.org/packages/0c/6f/0c9ba210f49f232289afaa8f06369c5f135ac786e1ca0cb22243b7f1fe2c/pyqwest-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5c80e88a5967c1cadb3237c450f91a84a3683f8838c8dca96f09fee3612e762", size = 5980431, upload-time = "2026-08-21T06:08:39.967Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/306eeed41a3cd3100247e6e442f4345277b70f1d24efe1641181b14839cd/pyqwest-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc3d80b402fb59dbe015e25993ac8147456fb231a4c949f92a89f31315ad50f9", size = 4870198, upload-time = "2026-08-21T06:08:41.687Z" }, + { url = "https://files.pythonhosted.org/packages/64/18/0086a408e7cbf39dab18fa5b7e42c969a98382da5a4e6debe40f05acc6a1/pyqwest-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:23a28beb55fa6d975949bffae4adfb69378f3229bb5cbd71231e95bf66f5b26c", size = 5274542, upload-time = "2026-08-21T06:08:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/e1b9aaaf7596e4faaa53cefc2efaca4e3cde721e308e6385e366361cfdcc/pyqwest-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e4415ae40b8eedb1713dab14d7f9fecc3f79d26f3206c561087b88b99d5ce24b", size = 5127922, upload-time = "2026-08-21T06:08:45.116Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/e6d68bb1de5dd26100fcfc878cbd67c402a928774edf1e8ae304c5a84f5b/pyqwest-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b875d2273212d7fa8e4b755d8d736ffd226b1c707a9c0017dfdc8393a96eca", size = 5645856, upload-time = "2026-08-21T06:08:47.055Z" }, + { url = "https://files.pythonhosted.org/packages/f7/48/8c9f9f0467c41f6a563146d57a52f8f6d60c0cb09d0fd3ef88ad5a1c442f/pyqwest-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5071491e416ea54e3b95bf9ffbed0bd065b093cb96e10a75c3d8f2cbe3c9823", size = 5569831, upload-time = "2026-08-21T06:08:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/16/ac/c85ab70c6c72078d49a82da76b820e46aaf95a3f6fe271dac955ac195d21/pyqwest-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b68b5e68d513a4c63a072f8f40e38015160cf90bfbf7e8ef7c3935ca87e9e022", size = 5806735, upload-time = "2026-08-21T06:08:50.652Z" }, + { url = "https://files.pythonhosted.org/packages/66/db/ad7375b22fb2d0807431dcc9bc2aaf840e374c298cd15071024d5f6dd6d1/pyqwest-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c48910d27820b9c46fcd001b0fe514a3cf47d4784f59512dcdb8c91c395f82e4", size = 5984786, upload-time = "2026-08-21T06:08:52.377Z" }, + { url = "https://files.pythonhosted.org/packages/17/88/c449a772afe129683fd7acc657cbc7c69bec085dc75b6e8710a50fbb44e7/pyqwest-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:d03ba2cd17948b623a6210981d342eb122546d8a8e910ec77511aff4b1acdd00", size = 4872288, upload-time = "2026-08-21T06:08:54.101Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f8/439ffc0ee12cd7d9b57ac07ccea78ad3ba66b0d6817d429dd661d73308c4/pyqwest-0.10.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:07a0eb595f4096232c2d22549b6e4612c1ecada7934e46462c2c37ce14a89cfb", size = 5256823, upload-time = "2026-08-21T06:08:55.681Z" }, + { url = "https://files.pythonhosted.org/packages/df/2b/72ecd27d104d2b4284710194cce796d607674966c3ea66436768e812a66a/pyqwest-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:26401baf7dafc71c8d12d2e8389519d141e6f7c14094d0dd4cf9ec1d3b5555bd", size = 5112624, upload-time = "2026-08-21T06:08:57.366Z" }, + { url = "https://files.pythonhosted.org/packages/a4/24/0fb89c3f7d5a0410fcb7560588b4c741ef24b19195c307d4263f04e75c2b/pyqwest-0.10.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5e3c436e041d8873ce5bb0fdcf9f9e86f5604e8f0ef9e03149efebd8cb474f6", size = 5631844, upload-time = "2026-08-21T06:08:59.165Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4f/921d14754a186f0143ad62b50108dd808328e347e98e9dafab3897eeb405/pyqwest-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09364115761579eabfc79d1e954cdb3ded508dac1903fac7285d4c6f058c683f", size = 5555869, upload-time = "2026-08-21T06:09:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/b8/70/504780417319a626fe9549a7e6f9020a3d448eddf8a09617238a3426e90c/pyqwest-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:559674a98a8b1217e1830ecd41c9905bf2b60983c6b8017063dfac199f00727c", size = 5793738, upload-time = "2026-08-21T06:09:03.324Z" }, + { url = "https://files.pythonhosted.org/packages/45/f7/8d0a5b8a3289f4300dc9005ebde75d316f2371a2671617f942036337731a/pyqwest-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f399a696392fff3db3eef0a18ef65b8a3b8396d129193487d966b8eb11006376", size = 5972889, upload-time = "2026-08-21T06:09:05.2Z" }, + { url = "https://files.pythonhosted.org/packages/89/c4/f4c781e475c451cb5f4762a2f814a1750ef375b8db4db09bb2dcea03c4e5/pyqwest-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0f9163d6dd991bf1bf27308ba38ba021af660b15fffa47ebca98e41cf6f00309", size = 4858036, upload-time = "2026-08-21T06:09:06.926Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6f/29f605665f33aab894db8daf11b3b64bdca015fb11135815c53a150191a6/pyqwest-0.10.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cfcc7ba0229baa17831582befb046ace167b368140dae022d0b89b8d586ba12c", size = 5264903, upload-time = "2026-08-21T06:09:09.183Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c8/4ce4b40f21397a6482da910fae18b32c28dcdaddf3498aef2fe38597a9e0/pyqwest-0.10.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eff9ccf427604d34c635954def07b6113d4754f968073eef2df40bdb80b05bf5", size = 5142291, upload-time = "2026-08-21T06:09:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/41/34/7205db9cddc5a2286da459ca013af629e20f09498751c60093b952379add/pyqwest-0.10.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78662158093f9d5c742368f4dd9956aa595f44c6ac0860777c98853ddd5e1610", size = 5648118, upload-time = "2026-08-21T06:09:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/1e/94/75c6cd5d01cf68c99c0cfd66b165dce3fd9d9fce97cd4973117cab68355d/pyqwest-0.10.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0879a3f0b37876372aa328b9fee956165db174c1ab72464146fe515f49399ddb", size = 5565620, upload-time = "2026-08-21T06:09:14.223Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a6/20a910d2f096908cf25ec47fa84b35b1bbb1af63dc9713f53f38ed97802e/pyqwest-0.10.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:850b8de6ade09a60bdb2f969871a177c2c304b594b2034ba3f5962c7bea75551", size = 5809262, upload-time = "2026-08-21T06:09:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f7/f92fe4004d93f08c5118e750feefdbc72a73438b7fd7083300728e999911/pyqwest-0.10.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:399802647ea646c6ac9b5460e541b7c209b7a13563c667b2690ded2060185f2e", size = 5985550, upload-time = "2026-08-21T06:09:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/47/3e/2c896e54dbe3f1ba6e3bd10e9d412f422a2e33827ff591d59f67b13e0fa5/pyqwest-0.10.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c26f3de1feb5d066d7a66802a47407a93ba043696064ad80beda4a0a4bf10056", size = 4873529, upload-time = "2026-08-21T06:09:19.161Z" }, +] + [[package]] name = "pyright" version = "1.1.409" @@ -1501,6 +1725,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a", size = 25557, upload-time = "2026-04-08T14:37:14.613Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.15.12" @@ -1828,6 +2065,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, ] +[[package]] +name = "wcmatch" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/43/30e407989e313677dbb9d5f045f966549a7254834571e342eaa4b55cc67b/wcmatch-11.0.1.tar.gz", hash = "sha256:1ea2b4fa678b8ca268253798d5963935df39132d47c3e241c0a0732224005e7d", size = 144662, upload-time = "2026-08-14T15:20:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/77/7a02b0f05b3ffcdbef9719ce3ee0b508d6a29b58e95299f1580055671db3/wcmatch-11.0.1-py3-none-any.whl", hash = "sha256:fd149ecddb9f0a88ea780017d6dde17c994e494e7f7303d4e3c9d6251f978f4b", size = 43449, upload-time = "2026-08-14T15:20:39.379Z" }, +] + [[package]] name = "websockets" version = "16.0"