From 2877c2f5bdce09b7a88138afa5bc025197faa9d7 Mon Sep 17 00:00:00 2001 From: Borys Date: Mon, 16 Feb 2026 21:53:03 +0100 Subject: [PATCH 01/23] feat(core): add support for temporalio module --- docs/modules/temporal.md | 29 ++++++++++ mkdocs.yml | 1 + modules/temporal/README.rst | 2 + modules/temporal/example_basic.py | 40 ++++++++++++++ .../testcontainers/temporal/__init__.py | 54 +++++++++++++++++++ modules/temporal/tests/test_temporal.py | 42 +++++++++++++++ pyproject.toml | 3 ++ uv.lock | 2 +- 8 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 docs/modules/temporal.md create mode 100644 modules/temporal/README.rst create mode 100644 modules/temporal/example_basic.py create mode 100644 modules/temporal/testcontainers/temporal/__init__.py create mode 100644 modules/temporal/tests/test_temporal.py diff --git a/docs/modules/temporal.md b/docs/modules/temporal.md new file mode 100644 index 000000000..5a986fd35 --- /dev/null +++ b/docs/modules/temporal.md @@ -0,0 +1,29 @@ +# Temporal + +## Introduction + +The Testcontainers module for [Temporal](https://temporal.io/) — a durable execution platform for running reliable, long-running workflows. + +This module spins up the Temporal dev server (`temporalio/auto-setup`) which includes the Temporal server, a preconfigured `default` namespace, and the Web UI. + +## Adding this module to your project dependencies + +Please run the following command to add the Temporal module to your python dependencies: + +```bash +pip install testcontainers[temporal] +``` + +To interact with the server you will also need a Temporal SDK, for example: + +```bash +pip install temporalio +``` + +## Usage example + + + +[Creating a Temporal container](../../modules/temporal/example_basic.py) + + diff --git a/mkdocs.yml b/mkdocs.yml index aca8281b7..e8f7a640b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -93,6 +93,7 @@ nav: - modules/registry.md - modules/selenium.md - modules/sftp.md + - modules/temporal.md - modules/test_module_import.md - modules/vault.md - System Requirements: diff --git a/modules/temporal/README.rst b/modules/temporal/README.rst new file mode 100644 index 000000000..f9ac1eb3f --- /dev/null +++ b/modules/temporal/README.rst @@ -0,0 +1,2 @@ +.. autoclass:: testcontainers.temporal.TemporalContainer +.. title:: testcontainers.temporal.TemporalContainer diff --git a/modules/temporal/example_basic.py b/modules/temporal/example_basic.py new file mode 100644 index 000000000..86258a29b --- /dev/null +++ b/modules/temporal/example_basic.py @@ -0,0 +1,40 @@ +import asyncio +from datetime import timedelta + +from temporalio.api.workflowservice.v1 import ListNamespacesRequest +from temporalio.client import Client + +from testcontainers.temporal import TemporalContainer + + +async def main(): + with TemporalContainer() as temporal: + print(f"Temporal gRPC address: {temporal.get_grpc_address()}") + print(f"Temporal Web UI: {temporal.get_web_ui_url()}") + + # Connect a Temporal client + client = await Client.connect(temporal.get_grpc_address()) + + # List available namespaces + resp = await client.service_client.workflow_service.list_namespaces(ListNamespacesRequest()) + for ns in resp.namespaces: + print(f"Namespace: {ns.namespace_info.name}") + + # Start a workflow (untyped — no workflow definition class needed) + handle = await client.start_workflow( + "GreetingWorkflow", + id="greeting-wf-1", + task_queue="greeting-queue", + execution_timeout=timedelta(seconds=10), + memo={"env": "example"}, + ) + print(f"Started workflow: {handle.id}") + + # Describe the workflow + desc = await handle.describe() + print(f"Workflow type: {desc.workflow_type}") + print(f"Task queue: {desc.task_queue}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/modules/temporal/testcontainers/temporal/__init__.py b/modules/temporal/testcontainers/temporal/__init__.py new file mode 100644 index 000000000..3e8be903e --- /dev/null +++ b/modules/temporal/testcontainers/temporal/__init__.py @@ -0,0 +1,54 @@ +import urllib.error +import urllib.parse +import urllib.request + +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready + + +class TemporalContainer(DockerContainer): + """Temporal dev server container for integration testing. + + Example: + + The example spins up a Temporal dev server and connects to it using the + ``temporalio`` Python SDK. + + .. doctest:: + + >>> from testcontainers.temporal import TemporalContainer + >>> with TemporalContainer() as temporal: + ... address = temporal.get_grpc_address() + """ + + GRPC_PORT = 7233 + HTTP_PORT = 8233 + + def __init__(self, image: str = "temporalio/temporal:1.5.1", **kwargs) -> None: + super().__init__(image, **kwargs) + self.with_exposed_ports(self.GRPC_PORT, self.HTTP_PORT) + self.with_command("server start-dev --ip 0.0.0.0") + + @wait_container_is_ready(urllib.error.URLError, ConnectionError) + def _healthcheck(self) -> None: + host = self.get_container_host_ip() + port = self.get_exposed_port(self.HTTP_PORT) + url = urllib.parse.urlunsplit(("http", f"{host}:{port}", "/api/v1/namespaces", "", "")) + urllib.request.urlopen(url, timeout=1) + + def start(self) -> "TemporalContainer": + super().start() + self._healthcheck() + return self + + def get_grpc_address(self) -> str: + """Returns ``host:port`` for the Temporal gRPC frontend. + + The address intentionally omits a scheme because the Temporal SDKs + expect a plain ``host:port`` string. + """ + return f"{self.get_container_host_ip()}:{self.get_exposed_port(self.GRPC_PORT)}" + + def get_web_ui_url(self) -> str: + """Returns the base URL for the Temporal Web UI / HTTP API.""" + return f"http://{self.get_container_host_ip()}:{self.get_exposed_port(self.HTTP_PORT)}" diff --git a/modules/temporal/tests/test_temporal.py b/modules/temporal/tests/test_temporal.py new file mode 100644 index 000000000..067439b4f --- /dev/null +++ b/modules/temporal/tests/test_temporal.py @@ -0,0 +1,42 @@ +from datetime import timedelta +from uuid import uuid4 + +import pytest +from temporalio.api.workflowservice.v1 import ListNamespacesRequest +from temporalio.client import Client + +from testcontainers.temporal import TemporalContainer + + +@pytest.fixture(scope="module") +def temporal_container(): + with TemporalContainer() as container: + yield container + + +@pytest.mark.asyncio +async def test_default_namespace_exists(temporal_container): + client = await Client.connect(temporal_container.get_grpc_address()) + resp = await client.service_client.workflow_service.list_namespaces(ListNamespacesRequest()) + names = [ns.namespace_info.name for ns in resp.namespaces] + assert "default" in names + + +@pytest.mark.asyncio +async def test_start_and_describe_workflow(temporal_container): + client = await Client.connect(temporal_container.get_grpc_address()) + workflow_id = str(uuid4()) + + handle = await client.start_workflow( + "MyWorkflow", + id=workflow_id, + task_queue="my-task-queue", + execution_timeout=timedelta(seconds=10), + memo={"env": "test"}, + ) + desc = await handle.describe() + assert desc.id == workflow_id + assert desc.workflow_type == "MyWorkflow" + assert desc.task_queue == "my-task-queue" + memo = await desc.memo() + assert memo is not None diff --git a/pyproject.toml b/pyproject.toml index f983a2e3e..e703f8111 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,7 @@ registry = ["bcrypt>=5"] selenium = ["selenium>=4"] scylla = ["cassandra-driver>=3"] sftp = ["cryptography"] +temporal = [] vault = [] weaviate = ["weaviate-client>=4"] chroma = ["chromadb-client>=1"] @@ -215,6 +216,7 @@ packages = [ "modules/registry/testcontainers", "modules/sftp/testcontainers", "modules/selenium/testcontainers", + "modules/temporal/testcontainers", "modules/scylla/testcontainers", "modules/trino/testcontainers", "modules/vault/testcontainers", @@ -264,6 +266,7 @@ dev-mode-dirs = [ "modules/registry", "modules/sftp", "modules/selenium", + "modules/temporal", "modules/scylla", "modules/trino", "modules/vault", diff --git a/uv.lock b/uv.lock index 5b0f9e1cc..f9d6be9a1 100644 --- a/uv.lock +++ b/uv.lock @@ -5142,7 +5142,7 @@ requires-dist = [ { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=4" }, { name = "wrapt" }, ] -provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "db2", "elasticsearch", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "vault", "weaviate", "chroma", "trino"] +provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "db2", "elasticsearch", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "temporal", "vault", "weaviate", "chroma", "trino"] [package.metadata.requires-dev] dev = [ From 57f6dd82d9e957a51e36baf145b83ebd6db3a308 Mon Sep 17 00:00:00 2001 From: Borys Date: Mon, 16 Feb 2026 21:55:19 +0100 Subject: [PATCH 02/23] corrected description --- docs/modules/temporal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/temporal.md b/docs/modules/temporal.md index 5a986fd35..ac960b86e 100644 --- a/docs/modules/temporal.md +++ b/docs/modules/temporal.md @@ -4,7 +4,7 @@ The Testcontainers module for [Temporal](https://temporal.io/) — a durable execution platform for running reliable, long-running workflows. -This module spins up the Temporal dev server (`temporalio/auto-setup`) which includes the Temporal server, a preconfigured `default` namespace, and the Web UI. +This module spins up the Temporal dev server (`temporalio/temporal`) which includes the Temporal server, a preconfigured `default` namespace, and the Web UI. ## Adding this module to your project dependencies From baa566814b22fa922094a625ff92037cbe8bd93f Mon Sep 17 00:00:00 2001 From: Konstantin Veretennicov Date: Fri, 3 Apr 2026 08:14:08 +0100 Subject: [PATCH 03/23] fix(azurite): make visible to type checkers (#927) Closes #926 by adding PEP-561 marker file `py.typed` Co-authored-by: Roy Moore --- modules/azurite/testcontainers/azurite/py.typed | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules/azurite/testcontainers/azurite/py.typed diff --git a/modules/azurite/testcontainers/azurite/py.typed b/modules/azurite/testcontainers/azurite/py.typed new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/modules/azurite/testcontainers/azurite/py.typed @@ -0,0 +1 @@ + From d48115def127644964d4d2b09a38e3f4492cc43c Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Apr 2026 20:25:12 +0300 Subject: [PATCH 04/23] feat(core): support SSH-based DOCKER_HOST (#993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #992 ## Problem When `DOCKER_HOST` is set to an SSH URL (e.g. `ssh://user@remote-host`), the Docker Python SDK rewrites `base_url` to `http+docker://ssh`, losing the original hostname. This causes `DockerClient.host()` to return `"ssh"` instead of the actual remote address, breaking container connectivity. Additionally, the SDK defaults to paramiko for SSH connections, which crashes under pytest due to stdin capture conflicts. ## Changes ### `docker_client.py` - Extract the remote hostname from `DOCKER_HOST` in `host()` instead of relying on the SDK's rewritten `base_url` - Default to `use_ssh_client=True` for SSH connections to avoid paramiko/pytest stdin conflicts - Sanitize SSH URLs with unsupported path components before passing to the SDK - Add `get_docker_host_hostname()` and `is_ssh_docker_host()` helpers ### `compose.py` - Handle SSH in `PublishedPortModel.normalize()` — replace local bind addresses (`0.0.0.0`, `127.0.0.1`, etc.) with the remote SSH hostname ### Tests - Add SSH-specific tests for `DockerClient.host()`, connection mode, compose port normalization, and URL sanitization --- Makefile | 3 + core/testcontainers/compose/compose.py | 18 ++++- core/testcontainers/core/docker_client.py | 64 +++++++++++++++-- .../port_multiple/compose.yaml | 2 - .../compose_fixtures/port_single/compose.yaml | 1 - core/tests/test_compose.py | 26 +++++++ core/tests/test_core_registry.py | 13 +++- core/tests/test_docker_client.py | 70 +++++++++++++++++-- core/tests/test_docker_in_docker.py | 12 +++- 9 files changed, 193 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 680b5d038..9292a84df 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,9 @@ ${TESTS}: %/tests: quick-core-tests: ## Run core tests excluding long_running uv run coverage run --parallel -m pytest -v -m "not long_running" core/tests +core-tests: ## Run tests for the core package + uv run coverage run --parallel -m pytest -v core/tests + coverage: ## Target to combine and report coverage. uv run coverage combine uv run coverage report diff --git a/core/testcontainers/compose/compose.py b/core/testcontainers/compose/compose.py index c959b1341..dbaa8f442 100644 --- a/core/testcontainers/compose/compose.py +++ b/core/testcontainers/compose/compose.py @@ -11,6 +11,7 @@ from types import TracebackType from typing import Any, Callable, Literal, Optional, TypeVar, Union, cast +from testcontainers.core.docker_client import get_docker_host_hostname from testcontainers.core.exceptions import ContainerIsNotRunning, NoSuchPortExposed from testcontainers.core.waiting_utils import WaitStrategy @@ -45,10 +46,21 @@ class PublishedPortModel: Protocol: Optional[str] = None def normalize(self) -> "PublishedPortModel": - url_not_usable = system() == "Windows" and self.URL == "0.0.0.0" - if url_not_usable: + url = self.URL + + # For SSH-based DOCKER_HOST, local addresses (0.0.0.0, 127.0.0.1, localhost, ::, ::1) + # refer to the remote machine, not the local one. + # Replace them with the actual remote hostname. + ssh_host = get_docker_host_hostname() + if ssh_host and url in ("0.0.0.0", "127.0.0.1", "localhost", "::", "::1"): + url = ssh_host + # On Windows, 0.0.0.0 is not usable — replace with 127.0.0.1 + elif system() == "Windows" and url == "0.0.0.0": + url = "127.0.0.1" + + if url != self.URL: self_dict = asdict(self) - self_dict.update({"URL": "127.0.0.1"}) + self_dict.update({"URL": url}) return PublishedPortModel(**self_dict) return self diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 12384c94c..74053455e 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -68,9 +68,12 @@ def __init__(self, **kwargs: Any) -> None: if docker_host: LOGGER.info(f"using host {docker_host}") os.environ["DOCKER_HOST"] = docker_host - self.client = docker.from_env(**kwargs) - else: - self.client = docker.from_env(**kwargs) + # Use shell-based SSH client instead of paramiko to avoid conflicts with pytest stdin capture + # (paramiko's invoke library fails when reading from captured stdin). + if docker_host.startswith("ssh://"): + kwargs.setdefault("use_ssh_client", True) + + self.client = docker.from_env(**kwargs) self.client.api.headers["x-tc-sid"] = SESSION_ID self.client.api.headers["User-Agent"] = "tc-python/" + importlib.metadata.version("testcontainers") @@ -234,6 +237,14 @@ def host(self) -> str: host = c.tc_host_override if host: return host + + # For SSH-based connections, the Docker SDK rewrites base_url to + # "http+docker://ssh" which loses the original hostname. + # Extract it from the original DOCKER_HOST instead. + ssh_host = get_docker_host_hostname() + if ssh_host: + return ssh_host + try: url = urllib.parse.urlparse(self.client.api.base_url) except ValueError: @@ -266,7 +277,52 @@ def client_networks_create(self, name: str, param: dict[str, Any]) -> "DockerNet def get_docker_host() -> Optional[str]: - return c.tc_properties_get_tc_host() or os.getenv("DOCKER_HOST") + host = c.tc_properties_get_tc_host() or os.getenv("DOCKER_HOST") + if host: + return _sanitize_docker_host(host) + return None + + +def get_docker_host_hostname() -> Optional[str]: + """Extract the remote hostname from an SSH-based DOCKER_HOST. + + Returns the hostname (e.g. '192.168.1.42') when DOCKER_HOST is an ssh:// URL, or None otherwise. + """ + docker_host = get_docker_host() + if docker_host and docker_host.startswith("ssh://"): + parsed = urllib.parse.urlparse(docker_host) + if parsed.hostname: + return parsed.hostname + return None + + +def is_ssh_docker_host() -> bool: + """Check if the current DOCKER_HOST is an SSH-based connection.""" + return get_docker_host_hostname() is not None + + +def _sanitize_docker_host(docker_host: str) -> str: + """ + Sanitize the DOCKER_HOST value for compatibility with the Docker SDK. + + Strips path components from ``ssh://`` URLs because the Docker SDK + does not support them. A lone trailing ``/`` is treated as + equivalent to no path and silently normalised without a warning. + """ + if docker_host.startswith("ssh://"): + parsed = urllib.parse.urlparse(docker_host) + if parsed.path and parsed.path != "/": + sanitized = urllib.parse.urlunparse(parsed._replace(path="")) + LOGGER.warning( + "Stripped path from SSH DOCKER_HOST (unsupported by Docker SDK): %s -> %s", + docker_host, + sanitized, + ) + return sanitized + if parsed.path == "/": + # Trailing slash is harmless — strip quietly. + return urllib.parse.urlunparse(parsed._replace(path="")) + return docker_host def get_docker_auth_config() -> Optional[str]: diff --git a/core/tests/compose_fixtures/port_multiple/compose.yaml b/core/tests/compose_fixtures/port_multiple/compose.yaml index 662079f5e..21a4f5e8c 100644 --- a/core/tests/compose_fixtures/port_multiple/compose.yaml +++ b/core/tests/compose_fixtures/port_multiple/compose.yaml @@ -7,7 +7,6 @@ services: - '82' - target: 80 published: "5000-5999" - host_ip: 127.0.0.1 protocol: tcp command: - sh @@ -20,7 +19,6 @@ services: ports: - target: 80 published: "5000-5999" - host_ip: 127.0.0.1 protocol: tcp command: - sh diff --git a/core/tests/compose_fixtures/port_single/compose.yaml b/core/tests/compose_fixtures/port_single/compose.yaml index 88c19ab61..362a3c6b2 100644 --- a/core/tests/compose_fixtures/port_single/compose.yaml +++ b/core/tests/compose_fixtures/port_single/compose.yaml @@ -4,7 +4,6 @@ services: init: true ports: - target: 80 - host_ip: 127.0.0.1 protocol: tcp command: - sh diff --git a/core/tests/test_compose.py b/core/tests/test_compose.py index ee39ec0c0..f1faae5c4 100644 --- a/core/tests/test_compose.py +++ b/core/tests/test_compose.py @@ -382,3 +382,29 @@ def test_compose_profile_support(profiles: Optional[list[str]], running: list[st for service in not_running: with pytest.raises(ContainerIsNotRunning): compose.get_container(service) + + +@pytest.mark.parametrize( + "docker_host_env, url, expected_url", + [ + pytest.param("ssh://user@10.0.0.5", "0.0.0.0", "10.0.0.5", id="ssh_replaces_wildcard"), + pytest.param("ssh://user@10.0.0.5", "127.0.0.1", "10.0.0.5", id="ssh_replaces_loopback"), + pytest.param("ssh://user@10.0.0.5", "::", "10.0.0.5", id="ssh_replaces_ipv6_any"), + pytest.param("tcp://localhost:2375", "0.0.0.0", "0.0.0.0", id="non_ssh_keeps_original"), + ], +) +def test_compose_normalize_rewrites_local_url_for_ssh_docker_host( + monkeypatch: pytest.MonkeyPatch, docker_host_env: str, url: str, expected_url: str +) -> None: + """When DOCKER_HOST is an SSH URL, normalize() should replace local addresses + with the remote hostname — exercising the real get_docker_host_hostname() path.""" + from testcontainers.compose.compose import PublishedPortModel + from testcontainers.core.config import testcontainers_config as tc_config + + monkeypatch.setenv("DOCKER_HOST", docker_host_env) + monkeypatch.setattr(tc_config, "tc_properties_get_tc_host", lambda: None) + + model = PublishedPortModel(URL=url, TargetPort=80, PublishedPort=9999, Protocol="tcp") + result = model.normalize() + assert result.URL == expected_url + assert result.PublishedPort == 9999 diff --git a/core/tests/test_core_registry.py b/core/tests/test_core_registry.py index 38c37b5bd..fd65fcb0b 100644 --- a/core/tests/test_core_registry.py +++ b/core/tests/test_core_registry.py @@ -3,6 +3,9 @@ Note: Using the testcontainers-python library to test the Docker registry. This could be considered a bad practice as it is not recommended to use the same library to test itself. However, it is a very good use case for DockerRegistryContainer and allows us to test it thoroughly. + +Note2: These tests are skipped on macOS and SSH-based Docker hosts because they rely on insecure HTTP registries, +which are not supported in those environments without additional configuration. """ import json @@ -14,7 +17,7 @@ from testcontainers.core.config import testcontainers_config from testcontainers.core.container import DockerContainer -from testcontainers.core.docker_client import DockerClient +from testcontainers.core.docker_client import DockerClient, is_ssh_docker_host from testcontainers.core.waiting_utils import wait_for_logs from testcontainers.registry import DockerRegistryContainer @@ -25,6 +28,10 @@ is_mac(), reason="Docker Desktop on macOS does not support insecure private registries without daemon reconfiguration", ) +@pytest.mark.skipif( + is_ssh_docker_host(), + reason="Remote Docker via SSH requires HTTPS for non-localhost registries; insecure HTTP registries are not accessible", +) def test_missing_on_private_registry(monkeypatch): username = "user" password = "pass" @@ -50,6 +57,10 @@ def test_missing_on_private_registry(monkeypatch): is_mac(), reason="Docker Desktop on macOS does not support local insecure registries over HTTP without modifying daemon settings", ) +@pytest.mark.skipif( + is_ssh_docker_host(), + reason="Remote Docker via SSH requires HTTPS for non-localhost registries; insecure HTTP registries are not accessible", +) @pytest.mark.parametrize( "image,tag,username,password,expected_output", [ diff --git a/core/tests/test_docker_client.py b/core/tests/test_docker_client.py index 3cf7facd0..23dfe0748 100644 --- a/core/tests/test_docker_client.py +++ b/core/tests/test_docker_client.py @@ -10,7 +10,7 @@ from testcontainers.core.config import testcontainers_config as c, ConnectionMode from testcontainers.core.container import DockerContainer -from testcontainers.core.docker_client import DockerClient +from testcontainers.core.docker_client import DockerClient, is_ssh_docker_host from testcontainers.core.auth import parse_docker_auth_config from testcontainers.core.image import DockerImage from testcontainers.core import utils @@ -20,13 +20,23 @@ from docker.models.networks import Network +def _expected_from_env_kwargs(**kwargs: Any) -> dict[str, Any]: + """Build the kwargs we expect ``docker.from_env`` to be called with. + + When DOCKER_HOST is SSH-based, ``use_ssh_client=True`` is added automatically. + """ + if is_ssh_docker_host(): + kwargs.setdefault("use_ssh_client", True) + return kwargs + + def test_docker_client_from_env(): test_kwargs = {"test_kw": "test_value"} mock_docker = MagicMock(spec=docker) with patch("testcontainers.core.docker_client.docker", mock_docker): DockerClient(**test_kwargs) - mock_docker.from_env.assert_called_with(**test_kwargs) + mock_docker.from_env.assert_called_with(**_expected_from_env_kwargs(**test_kwargs)) def test_docker_client_login_no_login(): @@ -111,7 +121,7 @@ def test_container_docker_client_kw(): with patch("testcontainers.core.docker_client.docker", mock_docker): DockerContainer(image="", docker_client_kw=test_kwargs) - mock_docker.from_env.assert_called_with(**test_kwargs) + mock_docker.from_env.assert_called_with(**_expected_from_env_kwargs(**test_kwargs)) def test_image_docker_client_kw(): @@ -120,7 +130,7 @@ def test_image_docker_client_kw(): with patch("testcontainers.core.docker_client.docker", mock_docker): DockerImage(name="", path="", docker_client_kw=test_kwargs) - mock_docker.from_env.assert_called_with(**test_kwargs) + mock_docker.from_env.assert_called_with(**_expected_from_env_kwargs(**test_kwargs)) def test_host_prefer_host_override(monkeypatch: pytest.MonkeyPatch) -> None: @@ -139,6 +149,8 @@ def test_host_prefer_host_override(monkeypatch: pytest.MonkeyPatch) -> None: ], ) def test_host(monkeypatch: pytest.MonkeyPatch, base_url: str, expected: str) -> None: + if is_ssh_docker_host(): + pytest.skip("base_url parsing is not exercised under SSH (host() returns SSH hostname)") client = DockerClient() monkeypatch.setattr(client.client.api, "base_url", base_url) monkeypatch.setattr(c, "tc_host_override", None) @@ -270,6 +282,8 @@ def test_run_uses_found_network(monkeypatch: pytest.MonkeyPatch) -> None: """ If a host network is found, use it """ + if is_ssh_docker_host(): + pytest.skip("Host network discovery is skipped when DOCKER_HOST is set") client = DockerClient() @@ -293,3 +307,51 @@ def __init__(self) -> None: assert client.run("test") == "CONTAINER" assert fake_client.containers.calls[0]["network"] == "new_bridge_network" + + +@pytest.mark.parametrize( + "docker_host, expected", + [ + pytest.param("ssh://user@192.168.1.42", "ssh://user@192.168.1.42", id="no_path"), + pytest.param("ssh://user@host/", "ssh://user@host", id="trailing_slash"), + pytest.param("ssh://user@host/some/path", "ssh://user@host", id="strips_path"), + pytest.param("tcp://localhost:2375", "tcp://localhost:2375", id="tcp_unchanged"), + pytest.param("unix:///var/run/docker.sock", "unix:///var/run/docker.sock", id="unix_unchanged"), + ], +) +def test_sanitize_docker_host(docker_host: str, expected: str) -> None: + from testcontainers.core.docker_client import _sanitize_docker_host + + assert _sanitize_docker_host(docker_host) == expected + + +@pytest.mark.parametrize( + "docker_host, expected_hostname", + [ + pytest.param("ssh://user@192.168.1.42", "192.168.1.42", id="ssh_ip"), + pytest.param("ssh://user@myhost.example.com", "myhost.example.com", id="ssh_fqdn"), + pytest.param("tcp://localhost:2375", None, id="tcp_returns_none"), + pytest.param(None, None, id="unset_returns_none"), + ], +) +def test_get_docker_host_hostname(monkeypatch: pytest.MonkeyPatch, docker_host: str, expected_hostname) -> None: + from testcontainers.core.docker_client import get_docker_host_hostname + + monkeypatch.setattr(c, "tc_properties_get_tc_host", lambda: None) + if docker_host: + monkeypatch.setenv("DOCKER_HOST", docker_host) + else: + monkeypatch.delenv("DOCKER_HOST", raising=False) + assert get_docker_host_hostname() == expected_hostname + + +def test_ssh_docker_host(monkeypatch: pytest.MonkeyPatch) -> None: + """Verify SSH DOCKER_HOST sets use_ssh_client and host() returns the remote hostname.""" + monkeypatch.setenv("DOCKER_HOST", "ssh://user@10.0.0.1") + monkeypatch.setattr(c, "tc_properties_get_tc_host", lambda: None) + monkeypatch.setattr(c, "tc_host_override", None) + mock_docker = MagicMock(spec=docker) + with patch("testcontainers.core.docker_client.docker", mock_docker): + client = DockerClient() + mock_docker.from_env.assert_called_once_with(use_ssh_client=True) + assert client.host() == "10.0.0.1" diff --git a/core/tests/test_docker_in_docker.py b/core/tests/test_docker_in_docker.py index ada83c5ff..be9703621 100644 --- a/core/tests/test_docker_in_docker.py +++ b/core/tests/test_docker_in_docker.py @@ -15,7 +15,7 @@ from testcontainers.core.labels import SESSION_ID from testcontainers.core.network import Network from testcontainers.core.container import DockerContainer -from testcontainers.core.docker_client import DockerClient, LOGGER +from testcontainers.core.docker_client import DockerClient, LOGGER, is_ssh_docker_host from testcontainers.core.utils import inside_container from testcontainers.core.utils import is_mac from testcontainers.core.waiting_utils import wait_for_logs @@ -23,6 +23,11 @@ _DIND_PYTHON_VERSION = (3, 13) +SKIP_SSH_DOCKER = pytest.mark.skipif( + is_ssh_docker_host(), + reason="DinD/DooD tests require local Docker socket access, incompatible with SSH DOCKER_HOST", +) + RUN_ONCE_IN_CI = pytest.mark.skipif( bool(os.environ.get("CI")) and tuple([*sys.version_info][:2]) != _DIND_PYTHON_VERSION, reason=( @@ -51,6 +56,7 @@ def _wait_for_dind_return_ip(client: DockerClient, dind: Container): @pytest.mark.skipif(is_mac(), reason="Docker socket forwarding (socat) is unsupported on Docker Desktop for macOS") +@SKIP_SSH_DOCKER @RUN_ONCE_IN_CI def test_wait_for_logs_docker_in_docker(): # real dind isn't possible (AFAIK) in CI @@ -84,6 +90,7 @@ def test_wait_for_logs_docker_in_docker(): is_mac(), reason="Bridge networking and Docker socket forwarding are not supported on Docker Desktop for macOS", ) +@SKIP_SSH_DOCKER @RUN_ONCE_IN_CI def test_dind_inherits_network(): client = DockerClient() @@ -168,6 +175,7 @@ def get_docker_info() -> dict[str, Any]: @pytest.mark.xfail(reason="Does not work in rootless docker i.e. github actions") @pytest.mark.inside_docker_check @pytest.mark.skipif(not os.environ.get(EXPECTED_NETWORK_VAR), reason="No expected network given") +@SKIP_SSH_DOCKER def test_find_host_network_in_dood() -> None: """ Check that the correct host network is found for DooD @@ -185,6 +193,7 @@ def test_find_host_network_in_dood() -> None: reason="Docker socket mounting and container networking do not work reliably on Docker Desktop for macOS", ) @pytest.mark.skipif(not Path(tcc.ryuk_docker_socket).exists(), reason="No docker socket available") +@SKIP_SSH_DOCKER @RUN_ONCE_IN_CI def test_dood(python_testcontainer_image: str) -> None: """ @@ -225,6 +234,7 @@ def test_dood(python_testcontainer_image: str) -> None: is_mac(), reason="Docker socket mounting and container networking do not work reliably on Docker Desktop for macOS", ) +@SKIP_SSH_DOCKER @RUN_ONCE_IN_CI def test_dind(python_testcontainer_image: str, tmp_path: Path) -> None: """ From 407f79825be97865010dc0119cdfe3498a609a08 Mon Sep 17 00:00:00 2001 From: Ethan Lee <87640907+ethanlee928@users.noreply.github.com> Date: Sat, 4 Apr 2026 05:37:02 +0800 Subject: [PATCH 05/23] fix(qdrant): migrate Qdrant from deprecated decorator. (#963) ## Summary Hello, this PR addresses the deprecation warning coming from `QdrantContainer`. The container currently uses the deprecated `@wait_container_is_ready()` decorator, which is scheduled for removal. This change migrates to the recommended `LogMessageWaitStrategy`. Additionally, the Qdrant Docker image version has been updated to match the Qdrant client version, resolving the following warning: `UserWarning: Qdrant client version 1.16.2 is incompatible with server version 1.13.5.` ## Testing All qdrant test cases passed: ```bash poetry run pytest modules/qdrant/tests/ -v ============== 4 passed, 3 warnings in 3.07s ============== ``` The remaining warnings are related to `UserWarning: Api key is used with an insecure connection.`, which is out of scope for this PR. Thank you for reviewing! --------- Co-authored-by: ethanlee Co-authored-by: Roy Moore --- modules/qdrant/testcontainers/qdrant/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/qdrant/testcontainers/qdrant/__init__.py b/modules/qdrant/testcontainers/qdrant/__init__.py index 3b77b50fd..b2a2e8df2 100644 --- a/modules/qdrant/testcontainers/qdrant/__init__.py +++ b/modules/qdrant/testcontainers/qdrant/__init__.py @@ -15,12 +15,11 @@ from pathlib import Path from typing import Optional -from testcontainers.core.config import testcontainers_config as c -from testcontainers.core.generic import DbContainer -from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs +from testcontainers.core.container import DockerContainer +from testcontainers.core.wait_strategies import LogMessageWaitStrategy -class QdrantContainer(DbContainer): +class QdrantContainer(DockerContainer): """ Qdrant vector database container. @@ -39,7 +38,7 @@ class QdrantContainer(DbContainer): def __init__( self, - image: str = "qdrant/qdrant:v1.13.5", + image: str = "qdrant/qdrant:v1.16.2", rest_port: int = 6333, grpc_port: int = 6334, api_key: Optional[str] = None, @@ -59,9 +58,8 @@ def __init__( def _configure(self) -> None: self.with_env("QDRANT__SERVICE__API_KEY", self._api_key) - @wait_container_is_ready() def _connect(self) -> None: - wait_for_logs(self, ".*Actix runtime found; starting in Actix runtime.*", c.timeout) + LogMessageWaitStrategy(".*Actix runtime found; starting in Actix runtime.*").wait_until_ready(self) def get_client(self, **kwargs): """ From 803454147c03418b7b06601d251eb491a2cd79cf Mon Sep 17 00:00:00 2001 From: David Dzhalaev <72649244+dzhalaevd@users.noreply.github.com> Date: Sat, 4 Apr 2026 00:50:34 +0300 Subject: [PATCH 06/23] fix(clickhouse): add `HttpWaitStrategy` instead of deprecated `wait_container_is_ready` (#962) ### What was wrong? Decorator `wait_container_is_ready` is deprecated and raise `DeprecationWarning` Related issue: #874 ### How it was fixed? Replace the deprecated `wait_container_is_ready` decorator with `HttpWaitStrategy` in the `ClickHouseContainer` Co-authored-by: Roy Moore --- .../clickhouse/testcontainers/clickhouse/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/modules/clickhouse/testcontainers/clickhouse/__init__.py b/modules/clickhouse/testcontainers/clickhouse/__init__.py index fbc8fab65..00ebde809 100644 --- a/modules/clickhouse/testcontainers/clickhouse/__init__.py +++ b/modules/clickhouse/testcontainers/clickhouse/__init__.py @@ -12,12 +12,10 @@ # under the License. import os from typing import Optional -from urllib.error import HTTPError, URLError -from urllib.request import urlopen from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import wait_container_is_ready +from testcontainers.core.wait_strategies import HttpWaitStrategy class ClickHouseContainer(DbContainer): @@ -58,12 +56,9 @@ def __init__( self.with_exposed_ports(self.port) self.with_exposed_ports(8123) - @wait_container_is_ready(HTTPError, URLError) def _connect(self) -> None: - # noinspection HttpUrlsUsage - url = f"http://{self.get_container_host_ip()}:{self.get_exposed_port(8123)}" - with urlopen(url) as r: - assert b"Ok" in r.read() + strategy = HttpWaitStrategy(8123).for_response_predicate(lambda response: "Ok" in response) + strategy.wait_until_ready(self) def _configure(self) -> None: self.with_env("CLICKHOUSE_USER", self.username) From fed65fe14507020007c115c535364c90d4bbdde9 Mon Sep 17 00:00:00 2001 From: Praneeth Jain Date: Sat, 4 Apr 2026 04:04:21 +0530 Subject: [PATCH 07/23] fix(compose): return type in get_service_port docstring (#939) Noticed a mismatch in a function docstring while using the library. Fixed return type of `get_service_port` from `str` to `int` Co-authored-by: Roy Moore --- core/testcontainers/compose/compose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testcontainers/compose/compose.py b/core/testcontainers/compose/compose.py index dbaa8f442..4defac60d 100644 --- a/core/testcontainers/compose/compose.py +++ b/core/testcontainers/compose/compose.py @@ -506,7 +506,7 @@ def get_service_port( Returns ------- - str: + int: The mapped port on the host """ normalize: PublishedPortModel = self.get_container(service_name).get_publisher(by_port=port).normalize() From 58459a13a1523c5dec8b21b0e16ae1afdce48156 Mon Sep 17 00:00:00 2001 From: Victor Cavichioli <79488234+VictorCavichioli@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:34:45 -0300 Subject: [PATCH 08/23] feat(compose): add structured container inspect information (#897) Summary This PR adds the ability to retrieve detailed container information from docker inspect in a structured format for ComposeContainer objects, with lazy loading and caching for optimal performance. Related with #857 --------- Co-authored-by: Roy Moore --- conf.py | 1 + core/testcontainers/compose/compose.py | 49 +- core/testcontainers/core/container.py | 23 + core/testcontainers/core/docker_client.py | 6 + core/testcontainers/core/inspect.py | 632 ++++++++++++++++++++++ core/tests/test_compose.py | 58 +- core/tests/test_container.py | 47 ++ core/tests/test_inspect.py | 249 +++++++++ docs/features/creating_container.md | 56 ++ docs/features/docker_compose.md | 56 ++ 10 files changed, 1160 insertions(+), 17 deletions(-) create mode 100644 core/testcontainers/core/inspect.py create mode 100644 core/tests/test_inspect.py diff --git a/conf.py b/conf.py index 25271fd6c..3c37b2bff 100644 --- a/conf.py +++ b/conf.py @@ -168,4 +168,5 @@ nitpick_ignore = [ ("py:class", "typing_extensions.Self"), ("py:class", "docker.models.containers.ExecResult"), + ("py:class", "testcontainers.core.docker_client.ContainerInspectInfo"), ] diff --git a/core/testcontainers/compose/compose.py b/core/testcontainers/compose/compose.py index 4defac60d..ffe6538ad 100644 --- a/core/testcontainers/compose/compose.py +++ b/core/testcontainers/compose/compose.py @@ -1,5 +1,5 @@ import sys -from dataclasses import asdict, dataclass, field, fields, is_dataclass +from dataclasses import asdict, dataclass, field from functools import cached_property from json import loads from logging import getLogger, warning @@ -11,29 +11,16 @@ from types import TracebackType from typing import Any, Callable, Literal, Optional, TypeVar, Union, cast -from testcontainers.core.docker_client import get_docker_host_hostname +from testcontainers.core.docker_client import DockerClient, get_docker_host_hostname from testcontainers.core.exceptions import ContainerIsNotRunning, NoSuchPortExposed +from testcontainers.core.inspect import ContainerInspectInfo, _ignore_properties from testcontainers.core.waiting_utils import WaitStrategy -_IPT = TypeVar("_IPT") _WARNINGS = {"DOCKER_COMPOSE_GET_CONFIG": "get_config is experimental, see testcontainers/testcontainers-python#669"} logger = getLogger(__name__) -def _ignore_properties(cls: type[_IPT], dict_: Any) -> _IPT: - """omits extra fields like @JsonIgnoreProperties(ignoreUnknown = true) - - https://gist.github.com/alexanderankin/2a4549ac03554a31bef6eaaf2eaf7fd5""" - if isinstance(dict_, cls): - return dict_ - if not is_dataclass(cls): - raise TypeError(f"Expected a dataclass type, got {cls}") - class_fields = {f.name for f in fields(cls)} - filtered = {k: v for k, v in dict_.items() if k in class_fields} - return cls(**filtered) - - @dataclass class PublishedPortModel: """ @@ -93,6 +80,7 @@ class ComposeContainer: ExitCode: Optional[int] = None Publishers: list[PublishedPortModel] = field(default_factory=list) _docker_compose: Optional["DockerCompose"] = field(default=None, init=False, repr=False) + _cached_container_info: Optional[ContainerInspectInfo] = field(default=None, init=False, repr=False) def __post_init__(self) -> None: if self.Publishers: @@ -159,6 +147,28 @@ def reload(self) -> None: # each time through get_container(), but we need this method for compatibility pass + def get_container_info(self) -> Optional[ContainerInspectInfo]: + """Get container information via docker inspect (lazy loaded). + + Returns: + Container inspect information or None if container is not started. + """ + if self._cached_container_info is not None: + return self._cached_container_info + + if not self._docker_compose or not self.ID: + return None + + try: + docker_client = self._docker_compose._get_docker_client() + self._cached_container_info = docker_client.get_container_inspect_info(self.ID) + + except Exception as e: + logger.warning(f"Failed to get container info for {self.ID}: {e}") + self._cached_container_info = None + + return self._cached_container_info + @property def status(self) -> str: """Get container status for compatibility with wait strategies.""" @@ -233,6 +243,7 @@ class DockerCompose: quiet_pull: bool = False quiet_build: bool = False _wait_strategies: Optional[dict[str, Any]] = field(default=None, init=False, repr=False) + _docker_client: Optional[DockerClient] = field(default=None, init=False, repr=False) def __post_init__(self) -> None: if isinstance(self.compose_file_name, str): @@ -597,3 +608,9 @@ def wait_for(self, url: str) -> "DockerCompose": with urlopen(url) as response: response.read() return self + + def _get_docker_client(self) -> DockerClient: + """Get Docker client instance.""" + if self._docker_client is None: + self._docker_client = DockerClient() + return self._docker_client diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 09a980b28..680e7ca20 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -19,6 +19,7 @@ from testcontainers.core.config import testcontainers_config as c from testcontainers.core.docker_client import DockerClient from testcontainers.core.exceptions import ContainerConnectException, ContainerStartException +from testcontainers.core.inspect import ContainerInspectInfo from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID from testcontainers.core.network import Network from testcontainers.core.transferable import Transferable, TransferSpec, build_transfer_tar @@ -104,6 +105,7 @@ def __init__( self._kwargs = kwargs self._wait_strategy: Optional[WaitStrategy] = _wait_strategy + self._cached_container_info: Optional[ContainerInspectInfo] = None self._transferable_specs: list[TransferSpec] = [] if transferables: @@ -328,6 +330,27 @@ def exec(self, command: Union[str, list[str]]) -> ExecResult: raise ContainerStartException("Container should be started before executing a command") return self._container.exec_run(command) + def get_container_info(self) -> Optional[ContainerInspectInfo]: + """Get container information via docker inspect (lazy loaded). + + Returns: + Container inspect information or None if container is not started. + """ + if self._cached_container_info is not None: + return self._cached_container_info + + if not self._container: + return None + + try: + self._cached_container_info = self.get_docker_client().get_container_inspect_info(self._container.id) + + except Exception as e: + logger.warning(f"Failed to get container info for {self._container.id}: {e}") + self._cached_container_info = None + + return self._cached_container_info + def _configure(self) -> None: # placeholder if subclasses want to define this and use the default start method pass diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 74053455e..ad08b1823 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -30,6 +30,7 @@ from testcontainers.core.auth import DockerAuthInfo, parse_docker_auth_config from testcontainers.core.config import ConnectionMode from testcontainers.core.config import testcontainers_config as c +from testcontainers.core.inspect import ContainerInspectInfo from testcontainers.core.labels import SESSION_ID, create_labels if TYPE_CHECKING: @@ -275,6 +276,11 @@ def client_networks_create(self, name: str, param: dict[str, Any]) -> "DockerNet labels = create_labels("", param.get("labels")) return self.client.networks.create(name, **{**param, "labels": labels}) + def get_container_inspect_info(self, container_id: str) -> "ContainerInspectInfo": + """Get container inspect information with fresh data.""" + container = self.client.containers.get(container_id) + return ContainerInspectInfo.from_dict(container.attrs) + def get_docker_host() -> Optional[str]: host = c.tc_properties_get_tc_host() or os.getenv("DOCKER_HOST") diff --git a/core/testcontainers/core/inspect.py b/core/testcontainers/core/inspect.py new file mode 100644 index 000000000..a139422dc --- /dev/null +++ b/core/testcontainers/core/inspect.py @@ -0,0 +1,632 @@ +# +# 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. + +"""Docker Engine API data structures for container inspect responses.""" + +from dataclasses import dataclass, fields, is_dataclass +from typing import Any, Optional, TypeVar + +_IPT = TypeVar("_IPT") + + +def _ignore_properties(cls: type[_IPT], dict_: Any) -> _IPT: + """omits extra fields like @JsonIgnoreProperties(ignoreUnknown = true) + + https://gist.github.com/alexanderankin/2a4549ac03554a31bef6eaaf2eaf7fd5""" + if isinstance(dict_, cls): + return dict_ + if not is_dataclass(cls): + raise TypeError(f"Expected a dataclass type, got {cls}") + class_fields = {f.name for f in fields(cls)} + filtered = {k: v for k, v in dict_.items() if k in class_fields} + return cls(**filtered) + + +@dataclass +class ContainerLog: + """Container health check log entry.""" + + Start: Optional[str] = None + End: Optional[str] = None + ExitCode: Optional[int] = None + Output: Optional[str] = None + + +@dataclass +class ContainerHealth: + """Container health check information.""" + + Status: Optional[str] = None + FailingStreak: Optional[int] = None + Log: Optional[list[ContainerLog]] = None + + +@dataclass +class ContainerState: + """Container state information.""" + + Status: Optional[str] = None + Running: Optional[bool] = None + Paused: Optional[bool] = None + Restarting: Optional[bool] = None + OOMKilled: Optional[bool] = None + Dead: Optional[bool] = None + Pid: Optional[int] = None + ExitCode: Optional[int] = None + Error: Optional[str] = None + StartedAt: Optional[str] = None + FinishedAt: Optional[str] = None + Health: Optional[ContainerHealth] = None + + +@dataclass +class ContainerPlatform: + """Platform information for image manifest.""" + + architecture: Optional[str] = None + os: Optional[str] = None + variant: Optional[str] = None + + +@dataclass +class ContainerImageManifestDescriptor: + """Image manifest descriptor.""" + + mediaType: Optional[str] = None + digest: Optional[str] = None + size: Optional[int] = None + urls: Optional[list[str]] = None + annotations: Optional[dict[str, str]] = None + data: Optional[Any] = None + platform: Optional[ContainerPlatform] = None + artifactType: Optional[str] = None + + +@dataclass +class ContainerBlkioWeightDevice: + """Block IO weight device configuration.""" + + Path: Optional[str] = None + Weight: Optional[int] = None + + +@dataclass +class ContainerBlkioDeviceRate: + """Block IO device rate configuration.""" + + Path: Optional[str] = None + Rate: Optional[int] = None + + +@dataclass +class ContainerDeviceMapping: + """Device mapping configuration.""" + + PathOnHost: Optional[str] = None + PathInContainer: Optional[str] = None + CgroupPermissions: Optional[str] = None + + +@dataclass +class ContainerDeviceRequest: + """Device request configuration.""" + + Driver: Optional[str] = None + Count: Optional[int] = None + DeviceIDs: Optional[list[str]] = None + Capabilities: Optional[list[list[str]]] = None + Options: Optional[dict[str, str]] = None + + +@dataclass +class ContainerUlimit: + """Ulimit configuration.""" + + Name: Optional[str] = None + Soft: Optional[int] = None + Hard: Optional[int] = None + + +@dataclass +class ContainerLogConfig: + """Logging configuration.""" + + Type: Optional[str] = None + Config: Optional[dict[str, str]] = None + + +@dataclass +class ContainerPortBinding: + """Port binding configuration.""" + + HostIp: Optional[str] = None + HostPort: Optional[str] = None + + +@dataclass +class ContainerRestartPolicy: + """Restart policy configuration.""" + + Name: Optional[str] = None + MaximumRetryCount: Optional[int] = None + + +@dataclass +class ContainerBindOptions: + """Bind mount options.""" + + Propagation: Optional[str] = None + NonRecursive: Optional[bool] = None + CreateMountpoint: Optional[bool] = None + ReadOnlyNonRecursive: Optional[bool] = None + ReadOnlyForceRecursive: Optional[bool] = None + + +@dataclass +class ContainerVolumeDriverConfig: + """Volume driver configuration.""" + + Name: Optional[str] = None + Options: Optional[dict[str, str]] = None + + +@dataclass +class ContainerVolumeOptions: + """Volume mount options.""" + + NoCopy: Optional[bool] = None + Labels: Optional[dict[str, str]] = None + DriverConfig: Optional[ContainerVolumeDriverConfig] = None + Subpath: Optional[str] = None + + +@dataclass +class ContainerImageOptions: + """Image mount options.""" + + Subpath: Optional[str] = None + + +@dataclass +class ContainerTmpfsOptions: + """Tmpfs mount options.""" + + SizeBytes: Optional[int] = None + Mode: Optional[int] = None + Options: Optional[list[list[str]]] = None + + +@dataclass +class ContainerMountPoint: + """Mount point configuration.""" + + Target: Optional[str] = None + Source: Optional[str] = None + Type: Optional[str] = None + ReadOnly: Optional[bool] = None + Consistency: Optional[str] = None + BindOptions: Optional[ContainerBindOptions] = None + VolumeOptions: Optional[ContainerVolumeOptions] = None + ImageOptions: Optional[ContainerImageOptions] = None + TmpfsOptions: Optional[ContainerTmpfsOptions] = None + + +@dataclass +class ContainerHostConfig: + """Host configuration for container.""" + + CpuShares: Optional[int] = None + Memory: Optional[int] = None + CgroupParent: Optional[str] = None + BlkioWeight: Optional[int] = None + BlkioWeightDevice: Optional[list[ContainerBlkioWeightDevice]] = None + BlkioDeviceReadBps: Optional[list[ContainerBlkioDeviceRate]] = None + BlkioDeviceWriteBps: Optional[list[ContainerBlkioDeviceRate]] = None + BlkioDeviceReadIOps: Optional[list[ContainerBlkioDeviceRate]] = None + BlkioDeviceWriteIOps: Optional[list[ContainerBlkioDeviceRate]] = None + CpuPeriod: Optional[int] = None + CpuQuota: Optional[int] = None + CpuRealtimePeriod: Optional[int] = None + CpuRealtimeRuntime: Optional[int] = None + CpusetCpus: Optional[str] = None + CpusetMems: Optional[str] = None + Devices: Optional[list[ContainerDeviceMapping]] = None + DeviceCgroupRules: Optional[list[str]] = None + DeviceRequests: Optional[list[ContainerDeviceRequest]] = None + KernelMemoryTCP: Optional[int] = None + MemoryReservation: Optional[int] = None + MemorySwap: Optional[int] = None + MemorySwappiness: Optional[int] = None + NanoCpus: Optional[int] = None + OomKillDisable: Optional[bool] = None + Init: Optional[bool] = None + PidsLimit: Optional[int] = None + Ulimits: Optional[list[ContainerUlimit]] = None + CpuCount: Optional[int] = None + CpuPercent: Optional[int] = None + IOMaximumIOps: Optional[int] = None + IOMaximumBandwidth: Optional[int] = None + Binds: Optional[list[str]] = None + ContainerIDFile: Optional[str] = None + LogConfig: Optional[ContainerLogConfig] = None + NetworkMode: Optional[str] = None + PortBindings: Optional[dict[str, Optional[list[ContainerPortBinding]]]] = None + RestartPolicy: Optional[ContainerRestartPolicy] = None + AutoRemove: Optional[bool] = None + VolumeDriver: Optional[str] = None + VolumesFrom: Optional[list[str]] = None + Mounts: Optional[list[ContainerMountPoint]] = None + ConsoleSize: Optional[list[int]] = None + Annotations: Optional[dict[str, str]] = None + CapAdd: Optional[list[str]] = None + CapDrop: Optional[list[str]] = None + CgroupnsMode: Optional[str] = None + Dns: Optional[list[str]] = None + DnsOptions: Optional[list[str]] = None + DnsSearch: Optional[list[str]] = None + ExtraHosts: Optional[list[str]] = None + GroupAdd: Optional[list[str]] = None + IpcMode: Optional[str] = None + Cgroup: Optional[str] = None + Links: Optional[list[str]] = None + OomScoreAdj: Optional[int] = None + PidMode: Optional[str] = None + Privileged: Optional[bool] = None + PublishAllPorts: Optional[bool] = None + ReadonlyRootfs: Optional[bool] = None + SecurityOpt: Optional[list[str]] = None + StorageOpt: Optional[dict[str, str]] = None + Tmpfs: Optional[dict[str, str]] = None + UTSMode: Optional[str] = None + UsernsMode: Optional[str] = None + ShmSize: Optional[int] = None + Sysctls: Optional[dict[str, str]] = None + Runtime: Optional[str] = None + Isolation: Optional[str] = None + MaskedPaths: Optional[list[str]] = None + ReadonlyPaths: Optional[list[str]] = None + + def __post_init__(self) -> None: + list_conversions = [ + ("BlkioWeightDevice", ContainerBlkioWeightDevice), + ("BlkioDeviceReadBps", ContainerBlkioDeviceRate), + ("BlkioDeviceWriteBps", ContainerBlkioDeviceRate), + ("BlkioDeviceReadIOps", ContainerBlkioDeviceRate), + ("BlkioDeviceWriteIOps", ContainerBlkioDeviceRate), + ("Devices", ContainerDeviceMapping), + ("DeviceRequests", ContainerDeviceRequest), + ("Ulimits", ContainerUlimit), + ("Mounts", ContainerMountPoint), + ] + + for field_name, target_class in list_conversions: + field_value = getattr(self, field_name) + if field_value is not None and isinstance(field_value, list): + setattr( + self, + field_name, + [ + _ignore_properties(target_class, item) if isinstance(item, dict) else item + for item in field_value + ], + ) + + if self.LogConfig is not None and isinstance(self.LogConfig, dict): + self.LogConfig = _ignore_properties(ContainerLogConfig, self.LogConfig) + + if self.RestartPolicy is not None and isinstance(self.RestartPolicy, dict): + self.RestartPolicy = _ignore_properties(ContainerRestartPolicy, self.RestartPolicy) + + if self.PortBindings is not None and isinstance(self.PortBindings, dict): + for port, bindings in self.PortBindings.items(): + if bindings is not None and isinstance(bindings, list): + self.PortBindings[port] = [ + _ignore_properties(ContainerPortBinding, b) if isinstance(b, dict) else b for b in bindings + ] + + +@dataclass +class ContainerGraphDriver: + """Graph driver information.""" + + Name: Optional[str] = None + Data: Optional[dict[str, str]] = None + + +@dataclass +class ContainerMount: + """Mount information.""" + + Type: Optional[str] = None + Name: Optional[str] = None + Source: Optional[str] = None + Destination: Optional[str] = None + Driver: Optional[str] = None + Mode: Optional[str] = None + RW: Optional[bool] = None + Propagation: Optional[str] = None + + +@dataclass +class ContainerHealthcheck: + """Container healthcheck configuration.""" + + Test: Optional[list[str]] = None + Interval: Optional[int] = None + Timeout: Optional[int] = None + Retries: Optional[int] = None + StartPeriod: Optional[int] = None + StartInterval: Optional[int] = None + + +@dataclass +class ContainerConfig: + """Container configuration.""" + + Hostname: Optional[str] = None + Domainname: Optional[str] = None + User: Optional[str] = None + AttachStdin: Optional[bool] = None + AttachStdout: Optional[bool] = None + AttachStderr: Optional[bool] = None + ExposedPorts: Optional[dict[str, dict[str, Any]]] = None + Tty: Optional[bool] = None + OpenStdin: Optional[bool] = None + StdinOnce: Optional[bool] = None + Env: Optional[list[str]] = None + Cmd: Optional[list[str]] = None + Healthcheck: Optional[ContainerHealthcheck] = None + ArgsEscaped: Optional[bool] = None + Image: Optional[str] = None + Volumes: Optional[dict[str, dict[str, Any]]] = None + WorkingDir: Optional[str] = None + Entrypoint: Optional[list[str]] = None + NetworkDisabled: Optional[bool] = None + MacAddress: Optional[str] = None + OnBuild: Optional[list[str]] = None + Labels: Optional[dict[str, str]] = None + StopSignal: Optional[str] = None + StopTimeout: Optional[int] = None + Shell: Optional[list[str]] = None + + +@dataclass +class ContainerIPAMConfig: + """IPAM configuration for network.""" + + IPv4Address: Optional[str] = None + IPv6Address: Optional[str] = None + LinkLocalIPs: Optional[list[str]] = None + + +@dataclass +class ContainerNetworkEndpoint: + """Network endpoint information.""" + + IPAMConfig: Optional[ContainerIPAMConfig] = None + Links: Optional[list[str]] = None + MacAddress: Optional[str] = None + Aliases: Optional[list[str]] = None + DriverOpts: Optional[dict[str, str]] = None + GwPriority: Optional[list[int]] = None + NetworkID: Optional[str] = None + EndpointID: Optional[str] = None + Gateway: Optional[str] = None + IPAddress: Optional[str] = None + IPPrefixLen: Optional[int] = None + IPv6Gateway: Optional[str] = None + GlobalIPv6Address: Optional[str] = None + GlobalIPv6PrefixLen: Optional[int] = None + DNSNames: Optional[list[str]] = None + + +@dataclass +class ContainerAddress: + """IP address information.""" + + Addr: Optional[str] = None + PrefixLen: Optional[int] = None + + +@dataclass +class ContainerNetworkSettings: + """Network settings for container.""" + + Bridge: Optional[str] = None + SandboxID: Optional[str] = None + HairpinMode: Optional[bool] = None + LinkLocalIPv6Address: Optional[str] = None + LinkLocalIPv6PrefixLen: Optional[str] = None + Ports: Optional[dict[str, Optional[list[ContainerPortBinding]]]] = None + SandboxKey: Optional[str] = None + SecondaryIPAddresses: Optional[list[ContainerAddress]] = None + SecondaryIPv6Addresses: Optional[list[ContainerAddress]] = None + EndpointID: Optional[str] = None + Gateway: Optional[str] = None + GlobalIPv6Address: Optional[str] = None + GlobalIPv6PrefixLen: Optional[int] = None + IPAddress: Optional[str] = None + IPPrefixLen: Optional[int] = None + IPv6Gateway: Optional[str] = None + MacAddress: Optional[str] = None + Networks: Optional[dict[str, ContainerNetworkEndpoint]] = None + + def __post_init__(self) -> None: + if self.Ports is not None and isinstance(self.Ports, dict): + for port, bindings in self.Ports.items(): + if bindings is not None and isinstance(bindings, list): + self.Ports[port] = [ + _ignore_properties(ContainerPortBinding, b) if isinstance(b, dict) else b for b in bindings + ] + + if self.Networks is not None and isinstance(self.Networks, dict): + for name, network_data in self.Networks.items(): + if isinstance(network_data, dict): + self.Networks[name] = _ignore_properties(ContainerNetworkEndpoint, network_data) + + if self.SecondaryIPAddresses is not None and isinstance(self.SecondaryIPAddresses, list): + self.SecondaryIPAddresses = [ + _ignore_properties(ContainerAddress, addr) if isinstance(addr, dict) else addr + for addr in self.SecondaryIPAddresses + ] + + if self.SecondaryIPv6Addresses is not None and isinstance(self.SecondaryIPv6Addresses, list): + self.SecondaryIPv6Addresses = [ + _ignore_properties(ContainerAddress, addr) if isinstance(addr, dict) else addr + for addr in self.SecondaryIPv6Addresses + ] + + def get_networks(self) -> Optional[dict[str, ContainerNetworkEndpoint]]: + """Get networks for the container.""" + return self.Networks + + +@dataclass +class ContainerInspectInfo: + """Complete container information from docker inspect.""" + + Id: Optional[str] = None + Created: Optional[str] = None + Path: Optional[str] = None + Args: Optional[list[str]] = None + State: Optional[ContainerState] = None + Image: Optional[str] = None + ResolvConfPath: Optional[str] = None + HostnamePath: Optional[str] = None + HostsPath: Optional[str] = None + LogPath: Optional[str] = None + Name: Optional[str] = None + RestartCount: Optional[int] = None + Driver: Optional[str] = None + Platform: Optional[str] = None + ImageManifestDescriptor: Optional[ContainerImageManifestDescriptor] = None + MountLabel: Optional[str] = None + ProcessLabel: Optional[str] = None + AppArmorProfile: Optional[str] = None + ExecIDs: Optional[list[str]] = None + HostConfig: Optional[ContainerHostConfig] = None + GraphDriver: Optional[ContainerGraphDriver] = None + SizeRw: Optional[str] = None + SizeRootFs: Optional[str] = None + Mounts: Optional[list[ContainerMount]] = None + Config: Optional[ContainerConfig] = None + NetworkSettings: Optional[ContainerNetworkSettings] = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ContainerInspectInfo": + """Create from docker inspect JSON.""" + return cls( + Id=data.get("Id"), + Created=data.get("Created"), + Path=data.get("Path"), + Args=data.get("Args"), + State=cls._parse_state(data.get("State", {})) if data.get("State") else None, + Image=data.get("Image"), + ResolvConfPath=data.get("ResolvConfPath"), + HostnamePath=data.get("HostnamePath"), + HostsPath=data.get("HostsPath"), + LogPath=data.get("LogPath"), + Name=data.get("Name"), + RestartCount=data.get("RestartCount"), + Driver=data.get("Driver"), + Platform=data.get("Platform"), + ImageManifestDescriptor=cls._parse_image_manifest(data.get("ImageManifestDescriptor", {})) + if data.get("ImageManifestDescriptor") + else None, + MountLabel=data.get("MountLabel"), + ProcessLabel=data.get("ProcessLabel"), + AppArmorProfile=data.get("AppArmorProfile"), + ExecIDs=data.get("ExecIDs"), + HostConfig=_ignore_properties(ContainerHostConfig, data.get("HostConfig", {})) + if data.get("HostConfig") + else None, + GraphDriver=_ignore_properties(ContainerGraphDriver, data.get("GraphDriver", {})) + if data.get("GraphDriver") + else None, + SizeRw=data.get("SizeRw"), + SizeRootFs=data.get("SizeRootFs"), + Mounts=[_ignore_properties(ContainerMount, mount) for mount in data.get("Mounts", [])], + Config=_ignore_properties(ContainerConfig, data.get("Config", {})) if data.get("Config") else None, + NetworkSettings=_ignore_properties(ContainerNetworkSettings, data.get("NetworkSettings", {})) + if data.get("NetworkSettings") + else None, + ) + + @classmethod + def _parse_state(cls, data: dict[str, Any]) -> Optional[ContainerState]: + """Parse State with nested Health object.""" + if not data: + return None + + health_data = data.get("Health", {}) + health = None + if health_data: + logs = [_ignore_properties(ContainerLog, log) for log in health_data.get("Log", [])] + health = ContainerHealth( + Status=health_data.get("Status"), + FailingStreak=health_data.get("FailingStreak"), + Log=logs if logs else None, + ) + + return ContainerState( + Status=data.get("Status"), + Running=data.get("Running"), + Paused=data.get("Paused"), + Restarting=data.get("Restarting"), + OOMKilled=data.get("OOMKilled"), + Dead=data.get("Dead"), + Pid=data.get("Pid"), + ExitCode=data.get("ExitCode"), + Error=data.get("Error"), + StartedAt=data.get("StartedAt"), + FinishedAt=data.get("FinishedAt"), + Health=health, + ) + + @classmethod + def _parse_image_manifest(cls, data: dict[str, Any]) -> Optional[ContainerImageManifestDescriptor]: + """Parse ImageManifestDescriptor with nested Platform.""" + if not data: + return None + + platform_data = data.get("platform", {}) + platform = _ignore_properties(ContainerPlatform, platform_data) if platform_data else None + + return ContainerImageManifestDescriptor( + mediaType=data.get("mediaType"), + digest=data.get("digest"), + size=data.get("size"), + urls=data.get("urls"), + annotations=data.get("annotations"), + data=data.get("data"), + platform=platform, + artifactType=data.get("artifactType"), + ) + + @classmethod + def _parse_host_config(cls, data: dict[str, Any]) -> Optional[ContainerHostConfig]: + """Parse HostConfig with all nested objects.""" + if not data: + return None + return _ignore_properties(ContainerHostConfig, data) + + @classmethod + def _parse_network_settings(cls, data: dict[str, Any]) -> Optional[ContainerNetworkSettings]: + """Parse NetworkSettings with nested Networks and Ports.""" + if not data: + return None + return _ignore_properties(ContainerNetworkSettings, data) + + def get_network_settings(self) -> Optional[ContainerNetworkSettings]: + """Get network settings for the container.""" + return self.NetworkSettings diff --git a/core/tests/test_compose.py b/core/tests/test_compose.py index f1faae5c4..ac0de6e61 100644 --- a/core/tests/test_compose.py +++ b/core/tests/test_compose.py @@ -9,7 +9,7 @@ import pytest from pytest_mock import MockerFixture -from testcontainers.compose import DockerCompose +from testcontainers.compose import DockerCompose, ComposeContainer from testcontainers.core.exceptions import ContainerIsNotRunning, NoSuchPortExposed FIXTURES = Path(__file__).parent.joinpath("compose_fixtures") @@ -408,3 +408,59 @@ def test_compose_normalize_rewrites_local_url_for_ssh_docker_host( result = model.normalize() assert result.URL == expected_url assert result.PublishedPort == 9999 + + +def test_container_info(): + """Test get_container_info functionality""" + basic = DockerCompose(context=FIXTURES / "basic") + with basic: + container = basic.get_container("alpine") + + info = container.get_container_info() + assert info is not None + assert info.Id is not None + assert info.Name is not None + assert info.Image is not None + + assert info.State is not None + assert info.State.Status == "running" + assert info.State.Running is True + assert info.State.Pid is not None + + assert info.Config is not None + assert info.Config.Image is not None + assert info.Config.Hostname is not None + + network_settings = info.get_network_settings() + assert network_settings is not None + assert network_settings.Networks is not None + + info2 = container.get_container_info() + assert info is info2 + + +def test_container_info_network_details(): + """Test network details in container info""" + single = DockerCompose(context=FIXTURES / "port_single") + with single: + container = single.get_container() + info = container.get_container_info() + assert info is not None + + network_settings = info.get_network_settings() + assert network_settings is not None + + if network_settings.Networks: + # Test first network + network_name, network = next(iter(network_settings.Networks.items())) + assert network.IPAddress is not None + assert network.Gateway is not None + assert network.NetworkID is not None + + +def test_container_info_none_when_no_docker_compose(): + """Test get_container_info returns None when docker_compose reference is missing""" + + container = ComposeContainer() + info = container.get_container_info() + assert info is None diff --git a/core/tests/test_container.py b/core/tests/test_container.py index 30b80f79d..84949fef1 100644 --- a/core/tests/test_container.py +++ b/core/tests/test_container.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from testcontainers.core.container import DockerContainer @@ -8,6 +10,9 @@ class FakeContainer: + def __init__(self) -> None: + self.attrs: dict[str, Any] = {} + @property def id(self) -> str: return FAKE_ID @@ -96,3 +101,45 @@ def test_attribute(init_attr, init_value, class_attr, stored_value): """Test that the attributes set through the __init__ function are properly stored.""" with DockerContainer("ubuntu", **{init_attr: init_value}) as container: assert getattr(container, class_attr) == stored_value + + +def test_container_info(): + """Test get_container_info functionality with a real container.""" + with DockerContainer("alpine:latest").with_command("sleep 30") as container: + info = container.get_container_info() + assert info is not None + assert info.Id is not None + assert info.Name is not None + assert info.Image is not None + + assert info.State is not None + assert info.State.Status == "running" + assert info.State.Running is True + assert info.State.Pid is not None + + assert info.Config is not None + assert info.Config.Image is not None + assert info.Config.Hostname is not None + + network_settings = info.get_network_settings() + assert network_settings is not None + assert network_settings.Networks is not None + + info2 = container.get_container_info() + assert info is info2 + + +def test_container_info_network_details(): + """Test network details in container info.""" + with DockerContainer("nginx:alpine") as container: + info = container.get_container_info() + assert info is not None + + network_settings = info.get_network_settings() + assert network_settings is not None + + if network_settings.Networks: + network_name, network = next(iter(network_settings.Networks.items())) + assert network.IPAddress is not None + assert network.Gateway is not None + assert network.NetworkID is not None diff --git a/core/tests/test_inspect.py b/core/tests/test_inspect.py new file mode 100644 index 000000000..0baf0dc5d --- /dev/null +++ b/core/tests/test_inspect.py @@ -0,0 +1,249 @@ +from typing import Any + +import pytest + +from testcontainers.core.container import DockerContainer +from testcontainers.core.docker_client import DockerClient +from testcontainers.core.inspect import ContainerInspectInfo +from testcontainers.core.config import ConnectionMode + +FAKE_ID = "ABC123" + + +class FakeContainer: + def __init__(self) -> None: + self.attrs: dict[str, Any] = {} + + @property + def id(self) -> str: + return FAKE_ID + + +@pytest.fixture +def container(monkeypatch: pytest.MonkeyPatch) -> DockerContainer: + """ + Fake initialized container + """ + client = DockerClient() + container = DockerContainer("foobar") + monkeypatch.setattr(container, "_docker", client) + monkeypatch.setattr(container, "_container", FakeContainer()) + + return container + + +def test_get_container_info_returns_none_when_no_container( + container: DockerContainer, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test get_container_info returns None when container is not started.""" + monkeypatch.setattr(container, "_container", None) + info = container.get_container_info() + assert info is None + + +def test_get_container_info_lazy_loading(container: DockerContainer, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_container_info lazy loading and caching.""" + fake_data = {"Id": "test123", "Name": "/test-container", "Image": "nginx:alpine"} + fake_info = ContainerInspectInfo.from_dict(fake_data) + + monkeypatch.setattr(container._docker, "get_container_inspect_info", lambda _: fake_info) + + info1 = container.get_container_info() + assert info1 is not None + assert info1.Id == "test123" + assert info1.Name == "/test-container" + assert info1.Image == "nginx:alpine" + + info2 = container.get_container_info() + assert info1 is info2 + + +def test_get_container_info_structure(container: DockerContainer, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_container_info returns properly structured data.""" + fake_data = { + "Id": "abc123def456", + "Name": "/my-test-container", + "Image": "sha256:nginx123", + "Created": "2023-01-01T00:00:00Z", + "State": { + "Status": "running", + "Running": True, + "Pid": 5678, + "ExitCode": 0, + "Health": {"Status": "healthy", "FailingStreak": 0, "Log": [{"Output": "healthy"}]}, + }, + "Config": { + "Image": "nginx:alpine", + "Hostname": "my-hostname", + "Env": ["PATH=/usr/bin", "HOME=/root"], + "Cmd": ["nginx", "-g", "daemon off;"], + "ExposedPorts": {"80/tcp": {}}, + }, + "NetworkSettings": { + "IPAddress": "172.17.0.3", + "Gateway": "172.17.0.1", + "Networks": { + "bridge": { + "IPAddress": "172.17.0.3", + "Gateway": "172.17.0.1", + "NetworkID": "net123", + "MacAddress": "02:42:ac:11:00:03", + "Aliases": ["container-alias"], + } + }, + }, + "HostConfig": {"Memory": 1073741824, "CpuShares": 1024, "NetworkMode": "bridge"}, + } + fake_info = ContainerInspectInfo.from_dict(fake_data) + + monkeypatch.setattr(container._docker, "get_container_inspect_info", lambda _: fake_info) + + info = container.get_container_info() + assert info is not None + + assert info.Id == "abc123def456" + assert info.Name == "/my-test-container" + assert info.Image == "sha256:nginx123" + assert info.Created == "2023-01-01T00:00:00Z" + + assert info.State is not None + assert info.State.Status == "running" + assert info.State.Running is True + assert info.State.Pid == 5678 + assert info.State.ExitCode == 0 + assert info.State.Health is not None + assert info.State.Health.Status == "healthy" + assert info.State.Health.FailingStreak == 0 + assert info.State.Health.Log is not None + assert len(info.State.Health.Log) == 1 + assert info.State.Health.Log[0].Output == "healthy" + + assert info.Config is not None + assert info.Config.Image == "nginx:alpine" + assert info.Config.Hostname == "my-hostname" + assert info.Config.Env == ["PATH=/usr/bin", "HOME=/root"] + assert info.Config.Cmd == ["nginx", "-g", "daemon off;"] + assert info.Config.ExposedPorts == {"80/tcp": {}} + + network_settings = info.get_network_settings() + assert network_settings is not None + assert network_settings.IPAddress == "172.17.0.3" + assert network_settings.Gateway == "172.17.0.1" + + assert network_settings.Networks is not None + assert "bridge" in network_settings.Networks + bridge_network = network_settings.Networks["bridge"] + assert bridge_network.IPAddress == "172.17.0.3" + assert bridge_network.Gateway == "172.17.0.1" + assert bridge_network.NetworkID == "net123" + assert bridge_network.MacAddress == "02:42:ac:11:00:03" + assert bridge_network.Aliases == ["container-alias"] + + assert info.HostConfig is not None + assert info.HostConfig.Memory == 1073741824 + assert info.HostConfig.CpuShares == 1024 + assert info.HostConfig.NetworkMode == "bridge" + + +def test_get_container_info_handles_exceptions(container: DockerContainer, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_container_info handles exceptions gracefully.""" + + def mock_exception(_): + raise Exception("Docker API error") + + monkeypatch.setattr(container._docker, "get_container_inspect_info", mock_exception) + + info = container.get_container_info() + assert info is None + + +def test_get_container_info_with_none_values(container: DockerContainer, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_container_info handles None values in HostConfig and NetworkSettings.""" + fake_data = { + "Id": "test-none-values", + "Name": "/test-none", + "Image": "nginx:alpine", + "NetworkSettings": {"IPAddress": "172.17.0.2", "Networks": None, "Ports": None}, + "HostConfig": {"Memory": 0, "NetworkMode": "bridge", "PortBindings": None}, + } + fake_info = ContainerInspectInfo.from_dict(fake_data) + + monkeypatch.setattr(container._docker, "get_container_inspect_info", lambda _: fake_info) + + info = container.get_container_info() + assert info is not None + assert info.Id == "test-none-values" + + network_settings = info.get_network_settings() + assert network_settings is not None + assert network_settings.IPAddress == "172.17.0.2" + assert network_settings.Networks is None + assert network_settings.Ports is None + + assert info.HostConfig is not None + assert info.HostConfig.Memory == 0 + assert info.HostConfig.NetworkMode == "bridge" + assert info.HostConfig.PortBindings is None + + +def test_get_container_info_with_port_bindings(container: DockerContainer, monkeypatch: pytest.MonkeyPatch) -> None: + """Test get_container_info handles port bindings correctly.""" + fake_data = { + "Id": "test-port-bindings", + "Name": "/test-ports", + "Image": "nginx:alpine", + "NetworkSettings": {"Ports": {"80/tcp": [{"HostPort": "8080"}], "443/tcp": None}}, + "HostConfig": {"NetworkMode": "bridge", "PortBindings": {"80/tcp": [{"HostPort": "8080"}], "443/tcp": None}}, + } + fake_info = ContainerInspectInfo.from_dict(fake_data) + + monkeypatch.setattr(container._docker, "get_container_inspect_info", lambda _: fake_info) + + info = container.get_container_info() + assert info is not None + + network_settings = info.get_network_settings() + assert network_settings is not None + assert network_settings.Ports is not None + assert "80/tcp" in network_settings.Ports + port_bindings = network_settings.Ports["80/tcp"] + assert port_bindings is not None + assert len(port_bindings) == 1 + assert port_bindings[0].HostPort == "8080" + assert network_settings.Ports["443/tcp"] is None + + assert info.HostConfig is not None + assert info.HostConfig.PortBindings is not None + assert "80/tcp" in info.HostConfig.PortBindings + host_port_bindings = info.HostConfig.PortBindings["80/tcp"] + assert host_port_bindings is not None + assert len(host_port_bindings) == 1 + assert host_port_bindings[0].HostPort == "8080" + assert info.HostConfig.PortBindings["443/tcp"] is None + + +def test_get_container_info_edge_cases_regression(container: DockerContainer, monkeypatch: pytest.MonkeyPatch) -> None: + """Regression test for None value handling.""" + fake_data = { + "Id": "regression-test", + "Name": "/regression-container", + "Image": "nginx:alpine", + "NetworkSettings": {"IPAddress": "172.17.0.2", "Networks": None, "Ports": None}, + "HostConfig": {"Memory": 0, "NetworkMode": "bridge", "PortBindings": None}, + } + fake_info = ContainerInspectInfo.from_dict(fake_data) + + monkeypatch.setattr(container._docker, "get_container_inspect_info", lambda _: fake_info) + + info = container.get_container_info() + assert info is not None + assert info.Id == "regression-test" + + network_settings = info.get_network_settings() + assert network_settings is not None + assert network_settings.Networks is None + assert network_settings.Ports is None + + host_config = info.HostConfig + assert host_config is not None + assert host_config.PortBindings is None diff --git a/docs/features/creating_container.md b/docs/features/creating_container.md index fa3b1190b..40c632943 100644 --- a/docs/features/creating_container.md +++ b/docs/features/creating_container.md @@ -128,6 +128,62 @@ def test_with_nginx(nginx_container): For details on waiting for containers to be ready, see [Wait strategies](wait_strategies.md). +## Container Information + +You can get detailed information about containers using the `get_container_info()` method. This works with both `DockerContainer` and `ComposeContainer`: + +```python +from testcontainers.generic import GenericContainer + +def test_container_info(): + with GenericContainer("nginx:alpine") as container: + # Get detailed container information + info = container.get_container_info() + + if info: + # Basic container information + print(f"Container ID: {info.Id}") + print(f"Name: {info.Name}") + print(f"Image: {info.Image}") + + # Container state + if info.State: + print(f"Status: {info.State.Status}") + print(f"Running: {info.State.Running}") + print(f"PID: {info.State.Pid}") + print(f"Exit Code: {info.State.ExitCode}") + + # Container configuration + if info.Config: + print(f"Hostname: {info.Config.Hostname}") + print(f"Environment: {info.Config.Env}") + print(f"Command: {info.Config.Cmd}") + + # Network information + network_settings = info.get_network_settings() + if network_settings and network_settings.Networks: + for network_name, network in network_settings.Networks.items(): + print(f"Network: {network_name}") + print(f" IP Address: {network.IPAddress}") + print(f" Gateway: {network.Gateway}") + print(f" MAC Address: {network.MacAddress}") +``` + +The container information is lazy-loaded and cached, so subsequent calls to `get_container_info()` will return the same data without making additional Docker API calls. + +### Available Information + +The `ContainerInspectInfo` object provides structured access to all Docker Engine API fields: + +- **Basic Info**: Container ID, name, image, creation time, platform +- **State**: Running status, PID, exit code, start/finish times, health status +- **Config**: Environment variables, command, working directory, labels, exposed ports +- **Network**: IP addresses, port bindings, network configurations, aliases +- **Host Config**: Memory limits, CPU settings, device mappings, restart policies +- **Mounts**: Volume and bind mount information with detailed options +- **Health**: Health check status and logs (if configured) +- **Platform**: Architecture and OS information + ## Best Practices 1. Always use context managers or ensure proper cleanup diff --git a/docs/features/docker_compose.md b/docs/features/docker_compose.md index 006a12b92..6b874a348 100644 --- a/docs/features/docker_compose.md +++ b/docs/features/docker_compose.md @@ -60,6 +60,22 @@ with DockerCompose("path/to/compose/directory") as compose: # Get container logs stdout, stderr = compose.get_logs("web") + + # Get detailed container information + container = compose.get_container("web") + info = container.get_container_info() + if info: + print(f"Container ID: {info.Id}") + if info.State: + print(f"Status: {info.State.Status}") + if info.Config: + print(f"Image: {info.Config.Image}") + + # Access network settings + network_settings = info.get_network_settings() + if network_settings and network_settings.Networks: + for name, network in network_settings.Networks.items(): + print(f"Network {name}: IP {network.IPAddress}") ``` ## Waiting for Services @@ -105,6 +121,46 @@ def test_web_application(): assert exit_code == 0 ``` +## Container Information + +You can get detailed information about containers using the `get_container_info()` method: + +```python +with DockerCompose("path/to/compose/directory") as compose: + container = compose.get_container("web") + info = container.get_container_info() + + if info: + # Basic container information + print(f"Container ID: {info.Id}") + print(f"Name: {info.Name}") + print(f"Image: {info.Image}") + + # Container state + if info.State: + print(f"Status: {info.State.Status}") + print(f"Running: {info.State.Running}") + print(f"PID: {info.State.Pid}") + print(f"Exit Code: {info.State.ExitCode}") + + # Container configuration + if info.Config: + print(f"Hostname: {info.Config.Hostname}") + print(f"Environment: {info.Config.Env}") + print(f"Command: {info.Config.Cmd}") + + # Network information + network_settings = info.get_network_settings() + if network_settings and network_settings.Networks: + for network_name, network in network_settings.Networks.items(): + print(f"Network: {network_name}") + print(f" IP Address: {network.IPAddress}") + print(f" Gateway: {network.Gateway}") + print(f" MAC Address: {network.MacAddress}") +``` + +The container information is lazy-loaded and cached, so subsequent calls to `get_container_info()` will return the same data without making additional Docker API calls. + ## Best Practices 1. Use context managers (`with` statement) to ensure proper cleanup From c8a5bbdbab137e6dc5af9a7224e65972665ec84d Mon Sep 17 00:00:00 2001 From: Oliver Lambson Date: Sat, 4 Apr 2026 03:45:35 -0400 Subject: [PATCH 09/23] fix(postgres): add py.typed marker to postgres module (#849) py.typed marker added Co-authored-by: Roy Moore --- modules/postgres/testcontainers/postgres/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 modules/postgres/testcontainers/postgres/py.typed diff --git a/modules/postgres/testcontainers/postgres/py.typed b/modules/postgres/testcontainers/postgres/py.typed new file mode 100644 index 000000000..e69de29bb From e25713a300eda6a14973d2465590d2318dcc375d Mon Sep 17 00:00:00 2001 From: Marc Schmitzer Date: Tue, 7 Apr 2026 16:58:45 +0200 Subject: [PATCH 10/23] fix(redis): Use wait strategy instead of deprecated decorator (#914) Another part of fixing #874 (cf. #899). --------- Co-authored-by: Dave Ankin --- modules/mqtt/testcontainers/mqtt/__init__.py | 2 +- .../redis/testcontainers/redis/__init__.py | 23 +++++++++---------- modules/redis/tests/test_redis.py | 11 +++++++++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/modules/mqtt/testcontainers/mqtt/__init__.py b/modules/mqtt/testcontainers/mqtt/__init__.py index 854ec21f8..8321d0d06 100644 --- a/modules/mqtt/testcontainers/mqtt/__init__.py +++ b/modules/mqtt/testcontainers/mqtt/__init__.py @@ -121,7 +121,7 @@ def start(self, configfile: Optional[str] = None) -> Self: # default config file configfile = Path(__file__).parent / MosquittoContainer.CONFIG_FILE self.with_volume_mapping(configfile, "/mosquitto/config/mosquitto.conf") - # since version 2.1.1 - 2026-02-04, which fixed a PUID/PGID issue, the container needs to write to the data directory, + # since version 2.1.1 - 2026-02-04, which fixed a PUID/PGID issue, the container needs to write to the data directory, # so we mount it as tmpfs for better performance in tests self.with_tmpfs_mount("/data") diff --git a/modules/redis/testcontainers/redis/__init__.py b/modules/redis/testcontainers/redis/__init__.py index 7a4d46613..24895b328 100644 --- a/modules/redis/testcontainers/redis/__init__.py +++ b/modules/redis/testcontainers/redis/__init__.py @@ -17,7 +17,7 @@ from redis.asyncio import Redis as asyncRedis from testcontainers.core.container import DockerContainer from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import wait_container_is_ready +from testcontainers.core.waiting_utils import WaitStrategy, WaitStrategyTarget class RedisContainer(DockerContainer): @@ -36,19 +36,13 @@ class RedisContainer(DockerContainer): def __init__(self, image: str = "redis:latest", port: int = 6379, password: Optional[str] = None, **kwargs) -> None: raise_for_deprecated_parameter(kwargs, "port_to_expose", "port") - super().__init__(image, **kwargs) + super().__init__(image, _wait_strategy=PingWaitStrategy(), **kwargs) self.port = port self.password = password self.with_exposed_ports(self.port) if self.password: self.with_command(f"redis-server --requirepass {self.password}") - @wait_container_is_ready(redis.exceptions.ConnectionError) - def _connect(self) -> None: - client = self.get_client() - if not client.ping(): - raise redis.exceptions.ConnectionError("Could not connect to Redis") - def get_client(self, **kwargs) -> redis.Redis: """ Get a redis client. @@ -66,10 +60,15 @@ def get_client(self, **kwargs) -> redis.Redis: **kwargs, ) - def start(self) -> "RedisContainer": - super().start() - self._connect() - return self + +class PingWaitStrategy(WaitStrategy): + def __init__(self) -> None: + super().__init__() + self.with_transient_exceptions(redis.exceptions.ConnectionError) + + def wait_until_ready(self, container: WaitStrategyTarget) -> None: + if not self._poll(lambda: container.get_client().ping()): + raise redis.exceptions.ConnectionError("Could not connect to Redis") class AsyncRedisContainer(RedisContainer): diff --git a/modules/redis/tests/test_redis.py b/modules/redis/tests/test_redis.py index bd8e244c5..01be35f14 100644 --- a/modules/redis/tests/test_redis.py +++ b/modules/redis/tests/test_redis.py @@ -2,6 +2,7 @@ from testcontainers.redis import RedisContainer, AsyncRedisContainer import pytest +import redis def test_docker_run_redis(): @@ -24,6 +25,16 @@ def test_docker_run_redis_with_password(): assert client.get("hello") == "world" +def test_docker_run_start_fails(monkeypatch: pytest.MonkeyPatch): + # Patch config to speed up the test. + monkeypatch.setattr("testcontainers.core.config.testcontainers_config.max_tries", 0.3) + monkeypatch.setattr("testcontainers.core.config.testcontainers_config.sleep_time", 0.02) + # Use a bogus image to make the startup check fail. + config = RedisContainer(image="hello-world") + with pytest.raises(redis.exceptions.ConnectionError, match="Could not connect"): + config.start() + + pytest.mark.usefixtures("anyio_backend") From 6ecf34717e3ffdab44581374cebad3b074ab8939 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:43:08 -0700 Subject: [PATCH 11/23] chore(main): release testcontainers 4.15.0-rc.1 (#986) :robot: I have created a release *beep* *boop* --- ## [4.15.0-rc.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.2...testcontainers-v4.15.0-rc.1) (2026-04-07) ### Features * **compose:** add structured container inspect information ([#897](https://github.com/testcontainers/testcontainers-python/issues/897)) ([58459a1](https://github.com/testcontainers/testcontainers-python/commit/58459a13a1523c5dec8b21b0e16ae1afdce48156)) * **core:** support SSH-based DOCKER_HOST ([#993](https://github.com/testcontainers/testcontainers-python/issues/993)) ([d48115d](https://github.com/testcontainers/testcontainers-python/commit/d48115def127644964d4d2b09a38e3f4492cc43c)) * **generic:** Reintroducing the generic SQL module ([#892](https://github.com/testcontainers/testcontainers-python/issues/892)) ([2ca2321](https://github.com/testcontainers/testcontainers-python/commit/2ca2321ada12e09d491280c8ec855bf8511de7c2)) * **keycloak:** support for relative path and management relative path ([#982](https://github.com/testcontainers/testcontainers-python/issues/982)) ([898faf6](https://github.com/testcontainers/testcontainers-python/commit/898faf6a5955698958be6e8cfd32b87323d62a44)) * **mqtt:** MosquittoContainer: Add version 2.1.2 ([#978](https://github.com/testcontainers/testcontainers-python/issues/978)) ([af382f7](https://github.com/testcontainers/testcontainers-python/commit/af382f74e82bdcb14eac3f4e04a83432ae9beeba)) ### Bug Fixes * **azurite:** make visible to type checkers ([#927](https://github.com/testcontainers/testcontainers-python/issues/927)) ([baa5668](https://github.com/testcontainers/testcontainers-python/commit/baa566814b22fa922094a625ff92037cbe8bd93f)) * **clickhouse:** add `HttpWaitStrategy` instead of deprecated `wait_container_is_ready` ([#962](https://github.com/testcontainers/testcontainers-python/issues/962)) ([8034541](https://github.com/testcontainers/testcontainers-python/commit/803454147c03418b7b06601d251eb491a2cd79cf)) * **compose:** return type in get_service_port docstring ([#939](https://github.com/testcontainers/testcontainers-python/issues/939)) ([fed65fe](https://github.com/testcontainers/testcontainers-python/commit/fed65fe14507020007c115c535364c90d4bbdde9)) * **core:** Refactor copy file ([#996](https://github.com/testcontainers/testcontainers-python/issues/996)) ([0e0bb24](https://github.com/testcontainers/testcontainers-python/commit/0e0bb24a2bddfd8a03bebdfc3b9ff8cf8c78092b)) * **core:** wait for ryuk more reliably, improve tests: long_running, filter logs ([#984](https://github.com/testcontainers/testcontainers-python/issues/984)) ([b12ae13](https://github.com/testcontainers/testcontainers-python/commit/b12ae13e589a4ffe326c162a38df56eb30521d69)) * **generic:** Migrate ServerContainer from deprecated decorator to HttpWaitStrategy ([#971](https://github.com/testcontainers/testcontainers-python/issues/971)) ([460b0d8](https://github.com/testcontainers/testcontainers-python/commit/460b0d8a09635068815ea8c5c5a4e4cc1e3dfea7)) * **kafka:** Use wait strategy instead of deprecated wait_for_logs ([#903](https://github.com/testcontainers/testcontainers-python/issues/903)) ([87332c1](https://github.com/testcontainers/testcontainers-python/commit/87332c1332a30b673aac919b48e296e21f2c1baf)) * **postgres:** add py.typed marker to postgres module ([#849](https://github.com/testcontainers/testcontainers-python/issues/849)) ([c8a5bbd](https://github.com/testcontainers/testcontainers-python/commit/c8a5bbdbab137e6dc5af9a7224e65972665ec84d)) * **qdrant:** migrate Qdrant from deprecated decorator. ([#963](https://github.com/testcontainers/testcontainers-python/issues/963)) ([407f798](https://github.com/testcontainers/testcontainers-python/commit/407f79825be97865010dc0119cdfe3498a609a08)) * **redis:** Use wait strategy instead of deprecated decorator ([#914](https://github.com/testcontainers/testcontainers-python/issues/914)) ([e25713a](https://github.com/testcontainers/testcontainers-python/commit/e25713a300eda6a14973d2465590d2318dcc375d)) * **sftp:** Avoid using wait_for_logs in module. ([#995](https://github.com/testcontainers/testcontainers-python/issues/995)) ([83157eb](https://github.com/testcontainers/testcontainers-python/commit/83157eb4acd931949cfec3d2a84db0a61685e739)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: David Ankin --- .github/.release-please-manifest.json | 2 +- CHANGELOG.md | 26 ++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index 1d17cc812..b5f8eec9a 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.14.2" + ".": "4.15.0-rc.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ce24ff24..27b0aed86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## [4.15.0-rc.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.2...testcontainers-v4.15.0-rc.1) (2026-04-07) + + +### Features + +* **compose:** add structured container inspect information ([#897](https://github.com/testcontainers/testcontainers-python/issues/897)) ([58459a1](https://github.com/testcontainers/testcontainers-python/commit/58459a13a1523c5dec8b21b0e16ae1afdce48156)) +* **core:** support SSH-based DOCKER_HOST ([#993](https://github.com/testcontainers/testcontainers-python/issues/993)) ([d48115d](https://github.com/testcontainers/testcontainers-python/commit/d48115def127644964d4d2b09a38e3f4492cc43c)) +* **generic:** Reintroducing the generic SQL module ([#892](https://github.com/testcontainers/testcontainers-python/issues/892)) ([2ca2321](https://github.com/testcontainers/testcontainers-python/commit/2ca2321ada12e09d491280c8ec855bf8511de7c2)) +* **keycloak:** support for relative path and management relative path ([#982](https://github.com/testcontainers/testcontainers-python/issues/982)) ([898faf6](https://github.com/testcontainers/testcontainers-python/commit/898faf6a5955698958be6e8cfd32b87323d62a44)) +* **mqtt:** MosquittoContainer: Add version 2.1.2 ([#978](https://github.com/testcontainers/testcontainers-python/issues/978)) ([af382f7](https://github.com/testcontainers/testcontainers-python/commit/af382f74e82bdcb14eac3f4e04a83432ae9beeba)) + + +### Bug Fixes + +* **azurite:** make visible to type checkers ([#927](https://github.com/testcontainers/testcontainers-python/issues/927)) ([baa5668](https://github.com/testcontainers/testcontainers-python/commit/baa566814b22fa922094a625ff92037cbe8bd93f)) +* **clickhouse:** add `HttpWaitStrategy` instead of deprecated `wait_container_is_ready` ([#962](https://github.com/testcontainers/testcontainers-python/issues/962)) ([8034541](https://github.com/testcontainers/testcontainers-python/commit/803454147c03418b7b06601d251eb491a2cd79cf)) +* **compose:** return type in get_service_port docstring ([#939](https://github.com/testcontainers/testcontainers-python/issues/939)) ([fed65fe](https://github.com/testcontainers/testcontainers-python/commit/fed65fe14507020007c115c535364c90d4bbdde9)) +* **core:** Refactor copy file ([#996](https://github.com/testcontainers/testcontainers-python/issues/996)) ([0e0bb24](https://github.com/testcontainers/testcontainers-python/commit/0e0bb24a2bddfd8a03bebdfc3b9ff8cf8c78092b)) +* **core:** wait for ryuk more reliably, improve tests: long_running, filter logs ([#984](https://github.com/testcontainers/testcontainers-python/issues/984)) ([b12ae13](https://github.com/testcontainers/testcontainers-python/commit/b12ae13e589a4ffe326c162a38df56eb30521d69)) +* **generic:** Migrate ServerContainer from deprecated decorator to HttpWaitStrategy ([#971](https://github.com/testcontainers/testcontainers-python/issues/971)) ([460b0d8](https://github.com/testcontainers/testcontainers-python/commit/460b0d8a09635068815ea8c5c5a4e4cc1e3dfea7)) +* **kafka:** Use wait strategy instead of deprecated wait_for_logs ([#903](https://github.com/testcontainers/testcontainers-python/issues/903)) ([87332c1](https://github.com/testcontainers/testcontainers-python/commit/87332c1332a30b673aac919b48e296e21f2c1baf)) +* **postgres:** add py.typed marker to postgres module ([#849](https://github.com/testcontainers/testcontainers-python/issues/849)) ([c8a5bbd](https://github.com/testcontainers/testcontainers-python/commit/c8a5bbdbab137e6dc5af9a7224e65972665ec84d)) +* **qdrant:** migrate Qdrant from deprecated decorator. ([#963](https://github.com/testcontainers/testcontainers-python/issues/963)) ([407f798](https://github.com/testcontainers/testcontainers-python/commit/407f79825be97865010dc0119cdfe3498a609a08)) +* **redis:** Use wait strategy instead of deprecated decorator ([#914](https://github.com/testcontainers/testcontainers-python/issues/914)) ([e25713a](https://github.com/testcontainers/testcontainers-python/commit/e25713a300eda6a14973d2465590d2318dcc375d)) +* **sftp:** Avoid using wait_for_logs in module. ([#995](https://github.com/testcontainers/testcontainers-python/issues/995)) ([83157eb](https://github.com/testcontainers/testcontainers-python/commit/83157eb4acd931949cfec3d2a84db0a61685e739)) + ## [4.14.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.1...testcontainers-v4.14.2) (2026-03-18) diff --git a/pyproject.toml b/pyproject.toml index 1440bf0d5..b0b431008 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "testcontainers" -version = "4.14.2" # auto-incremented by release-please +version = "4.15.0-rc.1" # auto-incremented by release-please description = "Python library for throwaway instances of anything that can run in a Docker container" readme = "README.md" requires-python = ">=3.10" From fc09dc17bccd45d57d92f12c0de26b99ab1ccecf Mon Sep 17 00:00:00 2001 From: Daria Korenieva Date: Thu, 9 Apr 2026 14:09:00 -0700 Subject: [PATCH 12/23] feat(valkey): add Valkey module (#947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’d like to add Valkey, the open-source fork of Redis, as a dedicated Testcontainers module. - Docker container: valkey/valkey:latest - Valkey website: https://valkey.io/ - Documentation: https://valkey.io/docs/ --------- Signed-off-by: Daria Korenieva Co-authored-by: Roy Moore --- docs/modules/valkey.md | 23 ++++ mkdocs.yml | 1 + modules/valkey/README.rst | 2 + modules/valkey/example_basic.py | 82 ++++++++++++++ .../valkey/testcontainers/valkey/__init__.py | 103 ++++++++++++++++++ modules/valkey/tests/test_valkey.py | 84 ++++++++++++++ pyproject.toml | 3 + uv.lock | 10 +- 8 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 docs/modules/valkey.md create mode 100644 modules/valkey/README.rst create mode 100644 modules/valkey/example_basic.py create mode 100644 modules/valkey/testcontainers/valkey/__init__.py create mode 100644 modules/valkey/tests/test_valkey.py diff --git a/docs/modules/valkey.md b/docs/modules/valkey.md new file mode 100644 index 000000000..d71182fac --- /dev/null +++ b/docs/modules/valkey.md @@ -0,0 +1,23 @@ +# Valkey + +Since testcontainers-python :material-tag: v4.14.3 + +## Introduction + +The Testcontainers module for Valkey. + +## Adding this module to your project dependencies + +Please run the following command to add the Valkey module to your python dependencies: + +```bash +pip install testcontainers[valkey] +``` + +## Usage example + + + +[Creating a Valkey container](../../modules/valkey/example_basic.py) + + diff --git a/mkdocs.yml b/mkdocs.yml index aca8281b7..0a31629a2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - modules/redis.md - modules/scylla.md - modules/trino.md + - modules/valkey.md - modules/weaviate.md - modules/aws.md - modules/azurite.md diff --git a/modules/valkey/README.rst b/modules/valkey/README.rst new file mode 100644 index 000000000..abe0c74e1 --- /dev/null +++ b/modules/valkey/README.rst @@ -0,0 +1,2 @@ +.. autoclass:: testcontainers.valkey.ValkeyContainer +.. title:: testcontainers.valkey.ValkeyContainer diff --git a/modules/valkey/example_basic.py b/modules/valkey/example_basic.py new file mode 100644 index 000000000..1288b5d94 --- /dev/null +++ b/modules/valkey/example_basic.py @@ -0,0 +1,82 @@ +""" +Valkey container usage examples with valkey-glide sync client. + +Requires: pip install valkey-glide-sync +""" + +from glide_sync import GlideClient, GlideClientConfiguration, NodeAddress, ServerCredentials + +from testcontainers.valkey import ValkeyContainer + + +def basic_example(): + with ValkeyContainer() as valkey_container: + host = valkey_container.get_host() + port = valkey_container.get_exposed_port() + connection_url = valkey_container.get_connection_url() + + print(f"Valkey connection URL: {connection_url}") + print(f"Host: {host}, Port: {port}") + + config = GlideClientConfiguration([NodeAddress(host, port)]) + client = GlideClient.create(config) + + pong = client.ping() + print(f"PING response: {pong}") + + client.set("key", "value") + print("SET response: OK") + + value = client.get("key") + print(f"GET response: {value}") + + client.close() + + +def password_example(): + with ValkeyContainer().with_password("mypassword") as valkey_container: + host = valkey_container.get_host() + port = valkey_container.get_exposed_port() + connection_url = valkey_container.get_connection_url() + + print(f"\nValkey with password connection URL: {connection_url}") + + config = GlideClientConfiguration( + [NodeAddress(host, port)], + credentials=ServerCredentials(password="mypassword"), + ) + client = GlideClient.create(config) + + pong = client.ping() + print(f"PING response: {pong}") + + client.close() + + +def version_example(): + with ValkeyContainer().with_image_tag("8.0") as valkey_container: + print(f"\nUsing image: {valkey_container.image}") + connection_url = valkey_container.get_connection_url() + print(f"Connection URL: {connection_url}") + + +def bundle_example(): + with ValkeyContainer().with_bundle() as valkey_container: + print(f"\nUsing bundle image: {valkey_container.image}") + host = valkey_container.get_host() + port = valkey_container.get_exposed_port() + + config = GlideClientConfiguration([NodeAddress(host, port)]) + client = GlideClient.create(config) + + pong = client.ping() + print(f"PING response: {pong}") + + client.close() + + +if __name__ == "__main__": + basic_example() + password_example() + version_example() + bundle_example() diff --git a/modules/valkey/testcontainers/valkey/__init__.py b/modules/valkey/testcontainers/valkey/__init__.py new file mode 100644 index 000000000..7237a64fe --- /dev/null +++ b/modules/valkey/testcontainers/valkey/__init__.py @@ -0,0 +1,103 @@ +# +# 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. + +from testcontainers.core.container import DockerContainer +from testcontainers.core.wait_strategies import ExecWaitStrategy + +_BASE_IMAGE = "valkey/valkey" +_BUNDLE_IMAGE = "valkey/valkey-bundle" + + +class ValkeyContainer(DockerContainer): + """ + Valkey container. + + """ + + def __init__(self, image: str = f"{_BASE_IMAGE}:latest", port: int = 6379, **kwargs) -> None: + super().__init__(image, **kwargs) + self.port = port + self.password: str | None = None + self.with_exposed_ports(self.port) + self.waiting_for(ExecWaitStrategy(["valkey-cli", "ping"])) + + def with_password(self, password: str) -> "ValkeyContainer": + """ + Configure authentication for Valkey. + + Args: + password: Password for Valkey authentication. + + Returns: + self: Container instance for method chaining. + """ + self.password = password + self.with_command(["valkey-server", "--requirepass", password]) + self.waiting_for(ExecWaitStrategy(["valkey-cli", "-a", password, "ping"])) + return self + + def with_image_tag(self, tag: str) -> "ValkeyContainer": + """ + Specify Valkey version. + + Args: + tag: Image tag (e.g., '8.0', 'latest'). + + Returns: + self: Container instance for method chaining. + """ + base_image = self.image.rsplit(":", 1)[0] + self.image = f"{base_image}:{tag}" + return self + + def with_bundle(self) -> "ValkeyContainer": + """ + Enable all modules by switching to valkey-bundle image. + + Returns: + self: Container instance for method chaining. + """ + tag = self.image.rsplit(":", 1)[-1] + self.image = f"{_BUNDLE_IMAGE}:{tag}" + return self + + def get_connection_url(self) -> str: + """ + Get connection URL for Valkey. + + Returns: + url: Connection URL in format valkey://[:password@]host:port + """ + host = self.get_host() + port = self.get_exposed_port() + if self.password: + return f"valkey://:{self.password}@{host}:{port}" + return f"valkey://{host}:{port}" + + def get_host(self) -> str: + """ + Get container host. + + Returns: + host: Container host IP. + """ + return self.get_container_host_ip() + + def get_exposed_port(self) -> int: + """ + Get mapped port. + + Returns: + port: Exposed port number. + """ + return int(super().get_exposed_port(self.port)) diff --git a/modules/valkey/tests/test_valkey.py b/modules/valkey/tests/test_valkey.py new file mode 100644 index 000000000..bcdf590ed --- /dev/null +++ b/modules/valkey/tests/test_valkey.py @@ -0,0 +1,84 @@ +import socket + +from testcontainers.valkey import ValkeyContainer + + +def test_docker_run_valkey(): + with ValkeyContainer() as valkey: + host = valkey.get_host() + port = valkey.get_exposed_port() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, port)) + s.sendall(b"*1\r\n$4\r\nPING\r\n") + response = s.recv(1024) + assert b"+PONG" in response + + +def test_docker_run_valkey_with_password(): + with ValkeyContainer().with_password("mypass") as valkey: + host = valkey.get_host() + port = valkey.get_exposed_port() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, port)) + # Authenticate + s.sendall(b"*2\r\n$4\r\nAUTH\r\n$6\r\nmypass\r\n") + auth_response = s.recv(1024) + assert b"+OK" in auth_response + + # Test SET command + s.sendall(b"*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n") + set_response = s.recv(1024) + assert b"+OK" in set_response + + # Test GET command + s.sendall(b"*2\r\n$3\r\nGET\r\n$5\r\nhello\r\n") + get_response = s.recv(1024) + assert b"world" in get_response + + +def test_get_connection_url(): + with ValkeyContainer() as valkey: + url = valkey.get_connection_url() + assert url.startswith("valkey://") + assert str(valkey.get_exposed_port()) in url + + +def test_get_connection_url_with_password(): + with ValkeyContainer().with_password("secret") as valkey: + url = valkey.get_connection_url() + assert url.startswith("valkey://:secret@") + assert str(valkey.get_exposed_port()) in url + + +def test_with_image_tag(): + container = ValkeyContainer().with_image_tag("8.0") + assert container.image == "valkey/valkey:8.0" + + +def test_with_bundle(): + container = ValkeyContainer().with_bundle() + assert container.image == "valkey/valkey-bundle:latest" + + +def test_with_bundle_and_tag(): + container = ValkeyContainer().with_bundle().with_image_tag("9.0") + assert container.image == "valkey/valkey-bundle:9.0" + + +def test_with_tag_and_bundle(): + container = ValkeyContainer().with_image_tag("8.0").with_bundle() + assert container.image == "valkey/valkey-bundle:8.0" + + +def test_bundle_starts(): + with ValkeyContainer().with_bundle() as valkey: + host = valkey.get_host() + port = valkey.get_exposed_port() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((host, port)) + s.sendall(b"*1\r\n$4\r\nPING\r\n") + response = s.recv(1024) + assert b"+PONG" in response diff --git a/pyproject.toml b/pyproject.toml index b0b431008..1d34750ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,7 @@ registry = ["bcrypt>=5"] selenium = ["selenium>=4"] scylla = ["cassandra-driver>=3; python_version < '3.14'"] sftp = ["cryptography"] +valkey = [] vault = [] weaviate = ["weaviate-client>=4"] chroma = ["chromadb-client>=1"] @@ -218,6 +219,7 @@ packages = [ "modules/selenium/testcontainers", "modules/scylla/testcontainers", "modules/trino/testcontainers", + "modules/valkey/testcontainers", "modules/vault/testcontainers", "modules/weaviate/testcontainers", ] @@ -267,6 +269,7 @@ dev-mode-dirs = [ "modules/selenium", "modules/scylla", "modules/trino", + "modules/valkey", "modules/vault", "modules/weaviate", ] diff --git a/uv.lock b/uv.lock index 22c671f37..1c9a3163d 100644 --- a/uv.lock +++ b/uv.lock @@ -1373,6 +1373,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, @@ -1380,6 +1381,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, @@ -1387,6 +1389,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, @@ -1394,6 +1397,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -1401,6 +1405,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -1408,6 +1413,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, @@ -4896,7 +4902,7 @@ wheels = [ [[package]] name = "testcontainers" -version = "4.14.2" +version = "4.15.0rc1" source = { editable = "." } dependencies = [ { name = "docker" }, @@ -5144,7 +5150,7 @@ requires-dist = [ { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=4" }, { name = "wrapt" }, ] -provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "db2", "elasticsearch", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "vault", "weaviate", "chroma", "trino"] +provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "db2", "elasticsearch", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "valkey", "vault", "weaviate", "chroma", "trino"] [package.metadata.requires-dev] dev = [ From 9fe6b074852e5d6f1df2942bda52ee0557e5cb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= <16805946+edgarrmondragon@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:09:30 -0600 Subject: [PATCH 13/23] fix(azurite): use `HttpWaitStrategy` instead of deprecated `wait_container_is_ready` (#1003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Related - #874 Signed-off-by: Edgar Ramírez Mondragón --- modules/azurite/testcontainers/azurite/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/azurite/testcontainers/azurite/__init__.py b/modules/azurite/testcontainers/azurite/__init__.py index f4e76d670..3cd755f34 100644 --- a/modules/azurite/testcontainers/azurite/__init__.py +++ b/modules/azurite/testcontainers/azurite/__init__.py @@ -12,12 +12,11 @@ # under the License. import enum import os -import socket from typing import Optional from testcontainers.core.container import DockerContainer from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import wait_container_is_ready +from testcontainers.core.wait_strategies import PortWaitStrategy class ConnectionStringType(enum.Enum): @@ -223,7 +222,6 @@ def start(self) -> "AzuriteContainer": self._connect() return self - @wait_container_is_ready(OSError) def _connect(self) -> None: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.connect((self.get_container_host_ip(), int(self.get_exposed_port(next(iter(self.ports)))))) + strategy = PortWaitStrategy(int(next(iter(self.ports)))) + strategy.wait_until_ready(self) From 2c1145c9c82e747d7b415475b201a9d705837ee4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:58:39 +0300 Subject: [PATCH 14/23] chore(deps): bump authlib from 1.6.6 to 1.6.9 (#999) Bumps [authlib](https://github.com/authlib/authlib) from 1.6.6 to 1.6.9. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roy Moore --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 1c9a3163d..a17b3266c 100644 --- a/uv.lock +++ b/uv.lock @@ -270,14 +270,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.6" +version = "1.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, ] [[package]] From aa12ee27ffc260c0f7855fd3d3ecc8aea2cb0736 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:32:48 -0400 Subject: [PATCH 15/23] chore(deps): bump cryptography from 46.0.3 to 46.0.7 (#1006) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.3 to 46.0.7.
Changelog

Sourced from cryptography's changelog.

46.0.7 - 2026-04-07


* **SECURITY ISSUE**: Fixed an issue where non-contiguous buffers could
be
  passed to APIs that accept Python buffers, which could lead to buffer
  overflow. **CVE-2026-39892**
* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
3.5.6.

.. _v46-0-6:

46.0.6 - 2026-03-25

  • SECURITY ISSUE: Fixed a bug where name constraints were not applied to peer names during verification when the leaf certificate contains a wildcard DNS SAN. Ordinary X.509 topologies are not affected by this bug, including those used by the Web PKI. Credit to Oleh Konko (1seal) for reporting the issue. CVE-2026-34073

.. _v46-0-5:

46.0.5 - 2026-02-10


* An attacker could create a malicious public key that reveals portions
of your
private key when using certain uncommon elliptic curves (binary curves).
This version now includes additional security checks to prevent this
attack.
This issue only affects binary elliptic curves, which are rarely used in
real-world applications. Credit to **XlabAI Team of Tencent Xuanwu Lab
and
Atuin Automated Vulnerability Discovery Engine** for reporting the
issue.
  **CVE-2026-26007**
* Support for ``SECT*`` binary elliptic curves is deprecated and will be
  removed in the next release.

.. v46-0-4:

46.0.4 - 2026-01-27

  • Dropped support for win_arm64 wheels_.
  • Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL 3.5.5.

.. _v46-0-3:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=uv&previous-version=46.0.3&new-version=46.0.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/testcontainers/testcontainers-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 113 +++++++++++++++++++++++++------------------------------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/uv.lock b/uv.lock index a17b3266c..ffd100895 100644 --- a/uv.lock +++ b/uv.lock @@ -935,67 +935,62 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] @@ -1373,7 +1368,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, @@ -1381,7 +1375,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, - { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, @@ -1389,7 +1382,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, - { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, @@ -1397,7 +1389,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -1405,7 +1396,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -1413,7 +1403,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, From 23aca0b8a4ac9cd6619e2859d1f0912ca63320a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:32:58 -0400 Subject: [PATCH 16/23] chore(deps): bump aiohttp from 3.13.3 to 3.13.4 (#1005) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aiohttp&package-manager=uv&previous-version=3.13.3&new-version=3.13.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/testcontainers/testcontainers-python/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 210 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/uv.lock b/uv.lock index ffd100895..4d65c6f7d 100644 --- a/uv.lock +++ b/uv.lock @@ -30,7 +30,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -42,110 +42,110 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, - { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, - { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, - { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, - { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, - { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, - { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, - { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/05/6817e0390eb47b0867cf8efdb535298191662192281bc3ca62a0cb7973eb/aiohttp-3.13.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6290fe12fe8cefa6ea3c1c5b969d32c010dfe191d4392ff9b599a3f473cbe722", size = 753094, upload-time = "2026-03-28T17:14:59.928Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/e5b7f25f6dd1ab57da92aa9d226b2c8b56f223dd20475d3ddfddaba86ab8/aiohttp-3.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7520d92c0e8fbbe63f36f20a5762db349ff574ad38ad7bc7732558a650439845", size = 505213, upload-time = "2026-03-28T17:15:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e5/8f42033c7ce98b54dfd3791f03e60231cfe4a2db4471b5fc188df2b8a6ad/aiohttp-3.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2710ae1e1b81d0f187883b6e9d66cecf8794b50e91aa1e73fc78bfb5503b5d9", size = 498580, upload-time = "2026-03-28T17:15:03.879Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/bbc989f5362066b81930da1a66084a859a971d03faab799dc59a3ce3a220/aiohttp-3.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:717d17347567ded1e273aa09918650dfd6fd06f461549204570c7973537d4123", size = 1692718, upload-time = "2026-03-28T17:15:05.541Z" }, + { url = "https://files.pythonhosted.org/packages/1c/72/3775116969931f151be116689d2ae6ddafff2ec2887d8f9b4e7043f32e74/aiohttp-3.13.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:383880f7b8de5ac208fa829c7038d08e66377283b2de9e791b71e06e803153c2", size = 1660714, upload-time = "2026-03-28T17:15:08.23Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e8/d2f1a2da2743e32fe348ebf8a4c59caad14a92f5f18af616fd33381275e1/aiohttp-3.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1867087e2c1963db1216aedf001efe3b129835ed2b05d97d058176a6d08b5726", size = 1744152, upload-time = "2026-03-28T17:15:10.828Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a6/575886f417ac3c08e462f2ca237cc49f436bd992ca3f7ff95b7dd9c44205/aiohttp-3.13.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6234bf416a38d687c3ab7f79934d7fb2a42117a5b9813aca07de0a5398489023", size = 1836278, upload-time = "2026-03-28T17:15:12.537Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4c/0051d4550fb9e8b5ca4e0fe1ccd58652340915180c5164999e6741bf2083/aiohttp-3.13.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdd3393130bf6588962441ffd5bde1d3ea2d63a64afa7119b3f3ba349cebbe7", size = 1687953, upload-time = "2026-03-28T17:15:14.248Z" }, + { url = "https://files.pythonhosted.org/packages/c9/54/841e87b8c51c2adc01a3ceb9919dc45c7899fe4c21deb70aada734ea5a38/aiohttp-3.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d0dbc6c76befa76865373d6aa303e480bb8c3486e7763530f7f6e527b471118", size = 1572484, upload-time = "2026-03-28T17:15:15.911Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/21cbf5f7fa1e267af6301f886cab9b314f085e4d0097668d189d165cd7da/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10fb7b53262cf4144a083c9db0d2b4d22823d6708270a9970c4627b248c6064c", size = 1662851, upload-time = "2026-03-28T17:15:17.822Z" }, + { url = "https://files.pythonhosted.org/packages/40/15/bcad6b68d7bef27ae7443288215767263c7753ede164267cf6cf63c94a87/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:eb10ce8c03850e77f4d9518961c227be569e12f71525a7e90d17bca04299921d", size = 1671984, upload-time = "2026-03-28T17:15:19.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/ab316931afc7a73c7f493bb1b30fbd61e28ec2d3ea50353336e76293e8ec/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7c65738ac5ae32b8feef699a4ed0dc91a0c8618b347781b7461458bbcaaac7eb", size = 1713880, upload-time = "2026-03-28T17:15:21.589Z" }, + { url = "https://files.pythonhosted.org/packages/1c/45/314e8e64c7f328174964b6db511dd5e9e60c9121ab5457bc2c908b7d03a4/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6b335919ffbaf98df8ff3c74f7a6decb8775882632952fd1810a017e38f15aee", size = 1560315, upload-time = "2026-03-28T17:15:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/18/e7/93d5fa06fe00219a81466577dacae9e3732f3b4f767b12b2e2cc8c35c970/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ec75fc18cb9f4aca51c2cbace20cf6716e36850f44189644d2d69a875d5e0532", size = 1735115, upload-time = "2026-03-28T17:15:25.77Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/f64b95392ddd4e204fd9ab7cd33dd18d14ac9e4b86866f1f6a69b7cda83d/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:463fa18a95c5a635d2b8c09babe240f9d7dbf2a2010a6c0b35d8c4dff2a0e819", size = 1673916, upload-time = "2026-03-28T17:15:27.526Z" }, + { url = "https://files.pythonhosted.org/packages/52/c1/bb33be79fd285c69f32e5b074b299cae8847f748950149c3965c1b3b3adf/aiohttp-3.13.4-cp310-cp310-win32.whl", hash = "sha256:13168f5645d9045522c6cef818f54295376257ed8d02513a37c2ef3046fc7a97", size = 440277, upload-time = "2026-03-28T17:15:29.173Z" }, + { url = "https://files.pythonhosted.org/packages/23/f9/7cf1688da4dd0885f914ee40bc8e1dce776df98fe6518766de975a570538/aiohttp-3.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:a7058af1f53209fdf07745579ced525d38d481650a989b7aa4a3b484b901cdab", size = 463015, upload-time = "2026-03-28T17:15:30.802Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, + { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, + { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, + { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, + { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, + { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, + { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, + { url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" }, + { url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" }, + { url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" }, + { url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" }, + { url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" }, + { url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" }, + { url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" }, + { url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" }, + { url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" }, + { url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" }, + { url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" }, + { url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" }, + { url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" }, + { url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" }, ] [[package]] From 59ec1ce6dc7d54fa7f4b3c69f5bf674dfd19bfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Grill?= Date: Thu, 30 Apr 2026 00:40:12 +0200 Subject: [PATCH 17/23] feat: support with_copy_to (#976) This PR supports copying files into the container before startup time: ```python DockerContainer("alpine") .with_command(["cat", "/tmp/copied.txt"]) .with_copy_into_container(src, "/tmp/copied.txt") ``` To support this, I had to change how the container is started: `run` is not longer used, but instead `create` and `start` are used now, in order to be able to make the `copy` before the container is actually running. Inspired by https://github.com/testcontainers/testcontainers-rs/pull/730 where I did exactly the same feature for the rust implementation of testcontainers :) ---- No tests are failing ```bash > uv run pytest -v core/tests ... ======================================= 316 passed, 1 skipped in 160.87s (0:02:40) ======================================== ``` Co-authored-by: David Ankin --- core/testcontainers/core/container.py | 18 ++++++++--- core/testcontainers/core/docker_client.py | 37 +++++++++++++++++++++++ core/tests/test_transferable.py | 13 ++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 680e7ca20..f7665c324 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -204,10 +204,9 @@ def start(self) -> Self: else {} ) - self._container = docker_client.run( + self._container = docker_client.create( self.image, command=self._command, - detach=True, environment=self.env, ports=cast("dict[int, Optional[int]]", self.ports), name=self._name, @@ -216,14 +215,16 @@ def start(self) -> Self: **{**network_kwargs, **self._kwargs}, ) + for t in self._transferable_specs: + self._transfer_into_container(*t) + + docker_client.start(self._container) + if self._wait_strategy is not None: self._wait_strategy.wait_until_ready(self) logger.info("Container started: %s", self._container.short_id) - for t in self._transferable_specs: - self._transfer_into_container(*t) - return self def stop(self, force: bool = True, delete_volume: bool = True) -> None: @@ -330,6 +331,13 @@ def exec(self, command: Union[str, list[str]]) -> ExecResult: raise ContainerStartException("Container should be started before executing a command") return self._container.exec_run(command) + def wait(self) -> int: + """Wait for the container to stop and return its exit code.""" + if not self._container: + raise ContainerStartException("Container should be started before waiting") + result = self._container.wait() + return int(result["StatusCode"]) + def get_container_info(self) -> Optional[ContainerInspectInfo]: """Get container information via docker inspect (lazy loaded). diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index ad08b1823..799b3b342 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -118,6 +118,43 @@ def run( ) return container + @_wrapped_container_collection + def create( + self, + image: str, + command: Optional[Union[str, list[str]]] = None, + environment: Optional[dict[str, str]] = None, + ports: Optional[dict[int, Optional[int]]] = None, + labels: Optional[dict[str, str]] = None, + **kwargs: Any, + ) -> Container: + """Create a container without starting it, pulling the image first if not present locally.""" + if "network" not in kwargs and not get_docker_host(): + host_network = self.find_host_network() + if host_network: + kwargs["network"] = host_network + + try: + # This is more or less a replication of what the self.client.containers.start does internally + self.client.images.get(image) + except docker.errors.ImageNotFound: + self.client.images.pull(image) + + container = self.client.containers.create( + image, + command=command, + environment=environment, + ports=ports, + labels=create_labels(image, labels), + **kwargs, + ) + return container + + @_wrapped_container_collection + def start(self, container: Container) -> None: + """Start a previously created container.""" + container.start() + @_wrapped_image_collection def build( self, path: str, tag: Optional[str], rm: bool = True, **kwargs: Any diff --git a/core/tests/test_transferable.py b/core/tests/test_transferable.py index 992f163af..592ad87df 100644 --- a/core/tests/test_transferable.py +++ b/core/tests/test_transferable.py @@ -104,6 +104,19 @@ def test_copy_into_container_at_startup(transferable: Transferable): assert result.output == b"hello world" +def test_copy_into_startup_file(transferable: Transferable): + destination_in_container = "/tmp/my_file" + + container = DockerContainer("bash", command=f"cat {destination_in_container}") + container.with_copy_into_container(transferable, destination_in_container) + + with container: + exit_code = container.wait() + stdout, _ = container.get_logs() + assert exit_code == 0 + assert stdout.decode() == "hello world" + + def test_copy_into_container_via_initializer(transferable: Transferable): destination_in_container = "/tmp/my_file" transferables: list[TransferSpec] = [(transferable, destination_in_container, 0o644)] From 73aeb43c18d56993d7c2626fb598a01842a91c35 Mon Sep 17 00:00:00 2001 From: Jeff Goddard Date: Wed, 29 Apr 2026 23:55:36 +0100 Subject: [PATCH 18/23] feat(mongodb): Add Atlas Local for MongoDb (#873) Linked issue: https://github.com/testcontainers/testcontainers-python/issues/865 This adds a Mongo DB Atlas local container. This works similarly to the Java container: https://java.testcontainers.org/modules/databases/mongodb/#mongodbatlaslocalcontainer Like the java one, I added this into the same module as the normal Mongo container, but we can make it into its own module if this would be better. Changes form standard mongo container: 1. Use different environment variables for configuration 2. Wait for the container healthcheck rather than the logs, as it takes a little longer for the search service to start. --------- Co-authored-by: Jeff Goddard Co-authored-by: Roy Moore Co-authored-by: David Ankin --- modules/mongodb/README.rst | 1 + .../testcontainers/mongodb/__init__.py | 92 +++++++++++++++++++ modules/mongodb/tests/test_mongodb.py | 50 +++++++++- 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/modules/mongodb/README.rst b/modules/mongodb/README.rst index 37e836406..4c2af683e 100644 --- a/modules/mongodb/README.rst +++ b/modules/mongodb/README.rst @@ -1,2 +1,3 @@ .. autoclass:: testcontainers.mongodb.MongoDbContainer +.. autoclass:: testcontainers.mongodb.MongoDBAtlasLocalContainer .. title:: testcontainers.mongodb.MongoDbContainer diff --git a/modules/mongodb/testcontainers/mongodb/__init__.py b/modules/mongodb/testcontainers/mongodb/__init__.py index 7ab4e11d4..43818d816 100644 --- a/modules/mongodb/testcontainers/mongodb/__init__.py +++ b/modules/mongodb/testcontainers/mongodb/__init__.py @@ -18,6 +18,7 @@ from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter +from testcontainers.core.wait_strategies import HealthcheckWaitStrategy from testcontainers.core.waiting_utils import wait_for_logs @@ -87,3 +88,94 @@ def predicate(text: str) -> bool: def get_connection_client(self) -> MongoClient: return MongoClient(self.get_connection_url()) + + +class MongoDBAtlasLocalContainer(DbContainer): + """ + MongoDB Atlas Local document-based database container. + + This is the local version of the Mongo Atlas service. + It includes Mongo DB and Mongo Atlas Search services + Example: + + .. doctest:: + + >>> from testcontainers.mongodb import MongoDBAtlasLocalContainer + >>> import time + >>> with MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8.0.13") as mongo: + ... db = mongo.get_connection_client().test + ... # Insert a database entry + ... result = db.restaurants.insert_one( + ... { + ... "name": "Vella", + ... "cuisine": "Italian", + ... "restaurant_id": "123456" + ... } + ... ) + ... # add an index + ... _ = db.restaurants.create_search_index( + ... { + ... "definition": { + ... "mappings": { + ... "dynamic": True + ... } + ... }, + ... "name": "default" + ... } + ... ) + ... # wait for the index to be created + ... time.sleep(1) + ... + ... # Find the restaurant document + ... result = db.restaurants.aggregate([{ + ... "$search": { + ... "index": "default", + ... "text": { + ... "query": "Vella", + ... "path": "name" + ... } + ... } + ... }]).next() + ... result["restaurant_id"] + '123456' + """ + + def __init__( + self, + image: str = "mongodb/mongodb-atlas-local:latest", + port: int = 27017, + username: Optional[str] = None, + password: Optional[str] = None, + dbname: Optional[str] = None, + **kwargs, + ) -> None: + raise_for_deprecated_parameter(kwargs, "port_to_expose", "port") + super().__init__(image=image, **kwargs) + self.username = username if username else os.environ.get("MONGODB_INITDB_ROOT_USERNAME", "test") + self.password = password if password else os.environ.get("MONGODB_INITDB_ROOT_PASSWORD", "test") + self.dbname = dbname if dbname else os.environ.get("MONGODB_INITDB_DATABASE", "test") + self.port = port + self.with_exposed_ports(self.port) + + def _configure(self) -> None: + self.with_env("MONGODB_INITDB_ROOT_USERNAME", self.username) + self.with_env("MONGODB_INITDB_ROOT_PASSWORD", self.password) + self.with_env("MONGODB_INITDB_DATABASE", self.dbname) + + def get_connection_url(self) -> str: + return ( + self._create_connection_url( + dialect="mongodb", + username=self.username, + password=self.password, + port=self.port, + ) + + "?directConnection=true" + ) + + def _connect(self) -> None: + strategy = HealthcheckWaitStrategy() + strategy.wait_until_ready(self) + + def get_connection_client(self) -> MongoClient: + return MongoClient(self.get_connection_url()) diff --git a/modules/mongodb/tests/test_mongodb.py b/modules/mongodb/tests/test_mongodb.py index 9bf3600f2..352c4a709 100644 --- a/modules/mongodb/tests/test_mongodb.py +++ b/modules/mongodb/tests/test_mongodb.py @@ -1,8 +1,9 @@ +import time import pytest from pymongo import MongoClient from pymongo.errors import OperationFailure -from testcontainers.mongodb import MongoDbContainer +from testcontainers.mongodb import MongoDbContainer, MongoDBAtlasLocalContainer @pytest.mark.parametrize("version", ["7.0.7", "6.0.14", "5.0.26"]) @@ -28,6 +29,53 @@ def test_docker_run_mongodb(version: str): assert cursor.next()["restaurant_id"] == doc["restaurant_id"] +@pytest.mark.parametrize("version", ["8.0.13", "7.0.23"]) +def test_docker_run_mongodb_atlas_local(version: str): + with MongoDBAtlasLocalContainer(f"mongodb/mongodb-atlas-local:{version}") as mongo_atlas: + db = mongo_atlas.get_connection_client().test + index_doc = { + "definition": { + "mappings": { + "dynamic": False, + "fields": { + "borough": {"analyzer": "lucene.keyword", "type": "string"}, + }, + }, + }, + "name": "test", + } + + db.create_collection("restaurants") + + db.restaurants.create_search_index(index_doc) + + doc = { + "address": { + "street": "2 Avenue", + "zipcode": "10075", + "building": "1480", + "coord": [-73.9557413, 40.7720266], + }, + "borough": "Manhattan", + "cuisine": "Italian", + "name": "Vella", + "restaurant_id": "41704620", + } + result = db.restaurants.insert_one(doc) + assert result.inserted_id + + # Wait for index to catch up + indexes = db.restaurants.list_search_indexes() + while indexes.next()["status"] != "READY": + time.sleep(0.1) + indexes = db.restaurants.list_search_indexes() + + cursor = db.restaurants.aggregate( + [{"$search": {"index": "test", "text": {"query": "Manhattan", "path": "borough"}}}] + ) + assert cursor.next()["restaurant_id"] == doc["restaurant_id"] + + # This is a feature in the generic DbContainer class # but it can't be tested on its own # so is tested in various database modules: From be9a0a612d934c77bdde20defd4d9f7d5228fb0c Mon Sep 17 00:00:00 2001 From: Zach Paden Date: Wed, 29 Apr 2026 18:56:25 -0500 Subject: [PATCH 19/23] feat(core): support TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX (#961) fixes #808 partially resolve #562, there may be some additional features requested there that this commit does not resolve. Additionally added test cases to check that it's picked up from env into config, and that the config changes the resolved image in DockerContainer Co-authored-by: David Ankin --- core/testcontainers/core/config.py | 3 ++- core/testcontainers/core/container.py | 5 ++--- core/tests/test_config.py | 9 ++++++++- core/tests/test_container.py | 23 +++++++++++++++++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index c9cd8c21e..2643cf7f2 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -110,11 +110,12 @@ def _render_bool(self, env_name: str, prop_name: str) -> bool: _docker_auth_config: Optional[str] = field(default_factory=lambda: environ.get("DOCKER_AUTH_CONFIG")) tc_host_override: Optional[str] = environ.get("TC_HOST", environ.get("TESTCONTAINERS_HOST_OVERRIDE")) connection_mode_override: Optional[ConnectionMode] = field(default_factory=get_user_overwritten_connection_mode) - """ https://github.com/testcontainers/testcontainers-go/blob/dd76d1e39c654433a3d80429690d07abcec04424/docker.go#L644 if os env TC_HOST is set, use it """ + hub_image_name_prefix: str = environ.get("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "") + """ Prefix to use for hub image names, e.g. for private registries. """ @property def docker_auth_config(self) -> Optional[str]: diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index f7665c324..d2c6007e6 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -3,7 +3,7 @@ import pathlib import sys import tarfile -from os import PathLike +from os import PathLike, getenv from socket import socket from types import TracebackType from typing import TYPE_CHECKING, Any, Optional, TypedDict, Union, cast @@ -89,8 +89,7 @@ def __init__( self.with_volume_mapping(*vol) self.tmpfs: dict[str, str] = {} - - self.image = image + self.image = c.hub_image_name_prefix + image self._docker = DockerClient(**(docker_client_kw or {})) self._container: Optional[Container] = None self._command: Optional[Union[str, list[str]]] = command diff --git a/core/tests/test_config.py b/core/tests/test_config.py index 435860313..90261f892 100644 --- a/core/tests/test_config.py +++ b/core/tests/test_config.py @@ -27,7 +27,14 @@ def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None: config = TCC() assert config.tc_properties == {"tc.host": "some_value"} - +def test_hub_image_name_prefix(monkeypatch: MonkeyPatch) -> None: + """ + Ensure that the hub_image_name_prefix configuration variable can be read from the environment + """ + monkeypatch.setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "myregistry.local/") + config = TCC() + assert config.hub_image_name_prefix == "myregistry.local/" + def test_set_tc_properties(monkeypatch: MonkeyPatch) -> None: """ Ensure the configuration file variables can be read if no environment variable is set diff --git a/core/tests/test_container.py b/core/tests/test_container.py index 84949fef1..aa2f66c9b 100644 --- a/core/tests/test_container.py +++ b/core/tests/test_container.py @@ -5,6 +5,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.docker_client import DockerClient from testcontainers.core.config import ConnectionMode +from testcontainers.core.config import testcontainers_config FAKE_ID = "ABC123" @@ -103,6 +104,28 @@ def test_attribute(init_attr, init_value, class_attr, stored_value): assert getattr(container, class_attr) == stored_value +def test_image_prefix_applied(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that the hub_image_name_prefix is properly applied to the image name.""" + + # Set a prefix + test_prefix = "myregistry.example.com/" + monkeypatch.setattr(testcontainers_config, "hub_image_name_prefix", test_prefix) + + # Create a container and verify the prefix is applied + container = DockerContainer("nginx:latest") + assert container.image == "myregistry.example.com/nginx:latest" + + +def test_image_no_prefix_applied_when_empty(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that when hub_image_name_prefix is empty, no prefix is applied.""" + # Set an empty prefix + monkeypatch.setattr(testcontainers_config, "hub_image_name_prefix", "") + + # Create a container and verify no prefix is applied + container = DockerContainer("nginx:latest") + assert container.image == "nginx:latest" + + def test_container_info(): """Test get_container_info functionality with a real container.""" with DockerContainer("alpine:latest").with_command("sleep 30") as container: From 8eff90851eecaf5720021d63e852a927c47f978c Mon Sep 17 00:00:00 2001 From: David Ankin Date: Wed, 29 Apr 2026 19:57:17 -0400 Subject: [PATCH 20/23] fix: fix pr #961 (#1011) fix #961 --- core/testcontainers/core/config.py | 2 +- core/testcontainers/core/container.py | 2 +- core/tests/test_compose.py | 2 +- core/tests/test_config.py | 4 +++- modules/azurite/testcontainers/azurite/py.typed | 1 - 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 2643cf7f2..bc9be49fa 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -114,7 +114,7 @@ def _render_bool(self, env_name: str, prop_name: str) -> bool: https://github.com/testcontainers/testcontainers-go/blob/dd76d1e39c654433a3d80429690d07abcec04424/docker.go#L644 if os env TC_HOST is set, use it """ - hub_image_name_prefix: str = environ.get("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "") + hub_image_name_prefix: str = field(default_factory=lambda: environ.get("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "")) """ Prefix to use for hub image names, e.g. for private registries. """ @property diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index d2c6007e6..3fcdc9807 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -3,7 +3,7 @@ import pathlib import sys import tarfile -from os import PathLike, getenv +from os import PathLike from socket import socket from types import TracebackType from typing import TYPE_CHECKING, Any, Optional, TypedDict, Union, cast diff --git a/core/tests/test_compose.py b/core/tests/test_compose.py index ac0de6e61..7ea26082a 100644 --- a/core/tests/test_compose.py +++ b/core/tests/test_compose.py @@ -408,7 +408,7 @@ def test_compose_normalize_rewrites_local_url_for_ssh_docker_host( result = model.normalize() assert result.URL == expected_url assert result.PublishedPort == 9999 - + def test_container_info(): """Test get_container_info functionality""" diff --git a/core/tests/test_config.py b/core/tests/test_config.py index 90261f892..ed5d6aa25 100644 --- a/core/tests/test_config.py +++ b/core/tests/test_config.py @@ -27,6 +27,7 @@ def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None: config = TCC() assert config.tc_properties == {"tc.host": "some_value"} + def test_hub_image_name_prefix(monkeypatch: MonkeyPatch) -> None: """ Ensure that the hub_image_name_prefix configuration variable can be read from the environment @@ -34,7 +35,8 @@ def test_hub_image_name_prefix(monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX", "myregistry.local/") config = TCC() assert config.hub_image_name_prefix == "myregistry.local/" - + + def test_set_tc_properties(monkeypatch: MonkeyPatch) -> None: """ Ensure the configuration file variables can be read if no environment variable is set diff --git a/modules/azurite/testcontainers/azurite/py.typed b/modules/azurite/testcontainers/azurite/py.typed index 8b1378917..e69de29bb 100644 --- a/modules/azurite/testcontainers/azurite/py.typed +++ b/modules/azurite/testcontainers/azurite/py.typed @@ -1 +0,0 @@ - From 79d920e173de501809501a309d7c8625e1096f92 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:47:09 -0400 Subject: [PATCH 21/23] chore(main): release testcontainers 4.15.0-rc2 (#1004) :robot: I have created a release *beep* *boop* --- ## [4.15.0-rc2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.15.0-rc.1...testcontainers-v4.15.0-rc2) (2026-04-30) ### Features * **core:** support TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX ([#961](https://github.com/testcontainers/testcontainers-python/issues/961)) ([be9a0a6](https://github.com/testcontainers/testcontainers-python/commit/be9a0a612d934c77bdde20defd4d9f7d5228fb0c)) * **mongodb:** Add Atlas Local for MongoDb ([#873](https://github.com/testcontainers/testcontainers-python/issues/873)) ([73aeb43](https://github.com/testcontainers/testcontainers-python/commit/73aeb43c18d56993d7c2626fb598a01842a91c35)) * support with_copy_to ([#976](https://github.com/testcontainers/testcontainers-python/issues/976)) ([59ec1ce](https://github.com/testcontainers/testcontainers-python/commit/59ec1ce6dc7d54fa7f4b3c69f5bf674dfd19bfc0)) * **valkey:** add Valkey module ([#947](https://github.com/testcontainers/testcontainers-python/issues/947)) ([fc09dc1](https://github.com/testcontainers/testcontainers-python/commit/fc09dc17bccd45d57d92f12c0de26b99ab1ccecf)) ### Bug Fixes * **azurite:** use `HttpWaitStrategy` instead of deprecated `wait_container_is_ready` ([#1003](https://github.com/testcontainers/testcontainers-python/issues/1003)) ([9fe6b07](https://github.com/testcontainers/testcontainers-python/commit/9fe6b074852e5d6f1df2942bda52ee0557e5cb32)), closes [#874](https://github.com/testcontainers/testcontainers-python/issues/874) * fix pr [#961](https://github.com/testcontainers/testcontainers-python/issues/961) ([#1011](https://github.com/testcontainers/testcontainers-python/issues/1011)) ([8eff908](https://github.com/testcontainers/testcontainers-python/commit/8eff90851eecaf5720021d63e852a927c47f978c)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: David Ankin --- .github/.release-please-manifest.json | 2 +- CHANGELOG.md | 16 ++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index b5f8eec9a..1c273a818 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.15.0-rc.1" + ".": "4.15.0-rc2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b0aed86..d873df1d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [4.15.0-rc2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.15.0-rc.1...testcontainers-v4.15.0-rc2) (2026-04-30) + + +### Features + +* **core:** support TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX ([#961](https://github.com/testcontainers/testcontainers-python/issues/961)) ([be9a0a6](https://github.com/testcontainers/testcontainers-python/commit/be9a0a612d934c77bdde20defd4d9f7d5228fb0c)) +* **mongodb:** Add Atlas Local for MongoDb ([#873](https://github.com/testcontainers/testcontainers-python/issues/873)) ([73aeb43](https://github.com/testcontainers/testcontainers-python/commit/73aeb43c18d56993d7c2626fb598a01842a91c35)) +* support with_copy_to ([#976](https://github.com/testcontainers/testcontainers-python/issues/976)) ([59ec1ce](https://github.com/testcontainers/testcontainers-python/commit/59ec1ce6dc7d54fa7f4b3c69f5bf674dfd19bfc0)) +* **valkey:** add Valkey module ([#947](https://github.com/testcontainers/testcontainers-python/issues/947)) ([fc09dc1](https://github.com/testcontainers/testcontainers-python/commit/fc09dc17bccd45d57d92f12c0de26b99ab1ccecf)) + + +### Bug Fixes + +* **azurite:** use `HttpWaitStrategy` instead of deprecated `wait_container_is_ready` ([#1003](https://github.com/testcontainers/testcontainers-python/issues/1003)) ([9fe6b07](https://github.com/testcontainers/testcontainers-python/commit/9fe6b074852e5d6f1df2942bda52ee0557e5cb32)), closes [#874](https://github.com/testcontainers/testcontainers-python/issues/874) +* fix pr [#961](https://github.com/testcontainers/testcontainers-python/issues/961) ([#1011](https://github.com/testcontainers/testcontainers-python/issues/1011)) ([8eff908](https://github.com/testcontainers/testcontainers-python/commit/8eff90851eecaf5720021d63e852a927c47f978c)) + ## [4.15.0-rc.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.2...testcontainers-v4.15.0-rc.1) (2026-04-07) diff --git a/pyproject.toml b/pyproject.toml index 1d34750ce..8f90bfda8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "testcontainers" -version = "4.15.0-rc.1" # auto-incremented by release-please +version = "4.15.0-rc2" # auto-incremented by release-please description = "Python library for throwaway instances of anything that can run in a Docker container" readme = "README.md" requires-python = ">=3.10" From 4fb69bb2804a2051c89bafd8e63fd5afc0b47512 Mon Sep 17 00:00:00 2001 From: Borys Date: Mon, 16 Feb 2026 21:53:03 +0100 Subject: [PATCH 22/23] feat(core): add support for temporalio module --- docs/modules/temporal.md | 29 ++++++++++ mkdocs.yml | 1 + modules/temporal/README.rst | 2 + modules/temporal/example_basic.py | 40 ++++++++++++++ .../testcontainers/temporal/__init__.py | 54 +++++++++++++++++++ modules/temporal/tests/test_temporal.py | 42 +++++++++++++++ pyproject.toml | 3 ++ uv.lock | 2 +- 8 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 docs/modules/temporal.md create mode 100644 modules/temporal/README.rst create mode 100644 modules/temporal/example_basic.py create mode 100644 modules/temporal/testcontainers/temporal/__init__.py create mode 100644 modules/temporal/tests/test_temporal.py diff --git a/docs/modules/temporal.md b/docs/modules/temporal.md new file mode 100644 index 000000000..5a986fd35 --- /dev/null +++ b/docs/modules/temporal.md @@ -0,0 +1,29 @@ +# Temporal + +## Introduction + +The Testcontainers module for [Temporal](https://temporal.io/) — a durable execution platform for running reliable, long-running workflows. + +This module spins up the Temporal dev server (`temporalio/auto-setup`) which includes the Temporal server, a preconfigured `default` namespace, and the Web UI. + +## Adding this module to your project dependencies + +Please run the following command to add the Temporal module to your python dependencies: + +```bash +pip install testcontainers[temporal] +``` + +To interact with the server you will also need a Temporal SDK, for example: + +```bash +pip install temporalio +``` + +## Usage example + + + +[Creating a Temporal container](../../modules/temporal/example_basic.py) + + diff --git a/mkdocs.yml b/mkdocs.yml index 0a31629a2..305aed7c8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,6 +94,7 @@ nav: - modules/registry.md - modules/selenium.md - modules/sftp.md + - modules/temporal.md - modules/test_module_import.md - modules/vault.md - System Requirements: diff --git a/modules/temporal/README.rst b/modules/temporal/README.rst new file mode 100644 index 000000000..f9ac1eb3f --- /dev/null +++ b/modules/temporal/README.rst @@ -0,0 +1,2 @@ +.. autoclass:: testcontainers.temporal.TemporalContainer +.. title:: testcontainers.temporal.TemporalContainer diff --git a/modules/temporal/example_basic.py b/modules/temporal/example_basic.py new file mode 100644 index 000000000..86258a29b --- /dev/null +++ b/modules/temporal/example_basic.py @@ -0,0 +1,40 @@ +import asyncio +from datetime import timedelta + +from temporalio.api.workflowservice.v1 import ListNamespacesRequest +from temporalio.client import Client + +from testcontainers.temporal import TemporalContainer + + +async def main(): + with TemporalContainer() as temporal: + print(f"Temporal gRPC address: {temporal.get_grpc_address()}") + print(f"Temporal Web UI: {temporal.get_web_ui_url()}") + + # Connect a Temporal client + client = await Client.connect(temporal.get_grpc_address()) + + # List available namespaces + resp = await client.service_client.workflow_service.list_namespaces(ListNamespacesRequest()) + for ns in resp.namespaces: + print(f"Namespace: {ns.namespace_info.name}") + + # Start a workflow (untyped — no workflow definition class needed) + handle = await client.start_workflow( + "GreetingWorkflow", + id="greeting-wf-1", + task_queue="greeting-queue", + execution_timeout=timedelta(seconds=10), + memo={"env": "example"}, + ) + print(f"Started workflow: {handle.id}") + + # Describe the workflow + desc = await handle.describe() + print(f"Workflow type: {desc.workflow_type}") + print(f"Task queue: {desc.task_queue}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/modules/temporal/testcontainers/temporal/__init__.py b/modules/temporal/testcontainers/temporal/__init__.py new file mode 100644 index 000000000..3e8be903e --- /dev/null +++ b/modules/temporal/testcontainers/temporal/__init__.py @@ -0,0 +1,54 @@ +import urllib.error +import urllib.parse +import urllib.request + +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready + + +class TemporalContainer(DockerContainer): + """Temporal dev server container for integration testing. + + Example: + + The example spins up a Temporal dev server and connects to it using the + ``temporalio`` Python SDK. + + .. doctest:: + + >>> from testcontainers.temporal import TemporalContainer + >>> with TemporalContainer() as temporal: + ... address = temporal.get_grpc_address() + """ + + GRPC_PORT = 7233 + HTTP_PORT = 8233 + + def __init__(self, image: str = "temporalio/temporal:1.5.1", **kwargs) -> None: + super().__init__(image, **kwargs) + self.with_exposed_ports(self.GRPC_PORT, self.HTTP_PORT) + self.with_command("server start-dev --ip 0.0.0.0") + + @wait_container_is_ready(urllib.error.URLError, ConnectionError) + def _healthcheck(self) -> None: + host = self.get_container_host_ip() + port = self.get_exposed_port(self.HTTP_PORT) + url = urllib.parse.urlunsplit(("http", f"{host}:{port}", "/api/v1/namespaces", "", "")) + urllib.request.urlopen(url, timeout=1) + + def start(self) -> "TemporalContainer": + super().start() + self._healthcheck() + return self + + def get_grpc_address(self) -> str: + """Returns ``host:port`` for the Temporal gRPC frontend. + + The address intentionally omits a scheme because the Temporal SDKs + expect a plain ``host:port`` string. + """ + return f"{self.get_container_host_ip()}:{self.get_exposed_port(self.GRPC_PORT)}" + + def get_web_ui_url(self) -> str: + """Returns the base URL for the Temporal Web UI / HTTP API.""" + return f"http://{self.get_container_host_ip()}:{self.get_exposed_port(self.HTTP_PORT)}" diff --git a/modules/temporal/tests/test_temporal.py b/modules/temporal/tests/test_temporal.py new file mode 100644 index 000000000..067439b4f --- /dev/null +++ b/modules/temporal/tests/test_temporal.py @@ -0,0 +1,42 @@ +from datetime import timedelta +from uuid import uuid4 + +import pytest +from temporalio.api.workflowservice.v1 import ListNamespacesRequest +from temporalio.client import Client + +from testcontainers.temporal import TemporalContainer + + +@pytest.fixture(scope="module") +def temporal_container(): + with TemporalContainer() as container: + yield container + + +@pytest.mark.asyncio +async def test_default_namespace_exists(temporal_container): + client = await Client.connect(temporal_container.get_grpc_address()) + resp = await client.service_client.workflow_service.list_namespaces(ListNamespacesRequest()) + names = [ns.namespace_info.name for ns in resp.namespaces] + assert "default" in names + + +@pytest.mark.asyncio +async def test_start_and_describe_workflow(temporal_container): + client = await Client.connect(temporal_container.get_grpc_address()) + workflow_id = str(uuid4()) + + handle = await client.start_workflow( + "MyWorkflow", + id=workflow_id, + task_queue="my-task-queue", + execution_timeout=timedelta(seconds=10), + memo={"env": "test"}, + ) + desc = await handle.describe() + assert desc.id == workflow_id + assert desc.workflow_type == "MyWorkflow" + assert desc.task_queue == "my-task-queue" + memo = await desc.memo() + assert memo is not None diff --git a/pyproject.toml b/pyproject.toml index 8f90bfda8..e20079fbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,7 @@ registry = ["bcrypt>=5"] selenium = ["selenium>=4"] scylla = ["cassandra-driver>=3; python_version < '3.14'"] sftp = ["cryptography"] +temporal = [] valkey = [] vault = [] weaviate = ["weaviate-client>=4"] @@ -217,6 +218,7 @@ packages = [ "modules/registry/testcontainers", "modules/sftp/testcontainers", "modules/selenium/testcontainers", + "modules/temporal/testcontainers", "modules/scylla/testcontainers", "modules/trino/testcontainers", "modules/valkey/testcontainers", @@ -267,6 +269,7 @@ dev-mode-dirs = [ "modules/registry", "modules/sftp", "modules/selenium", + "modules/temporal", "modules/scylla", "modules/trino", "modules/valkey", diff --git a/uv.lock b/uv.lock index 4d65c6f7d..9dc734adc 100644 --- a/uv.lock +++ b/uv.lock @@ -5139,7 +5139,7 @@ requires-dist = [ { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=4" }, { name = "wrapt" }, ] -provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "db2", "elasticsearch", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "valkey", "vault", "weaviate", "chroma", "trino"] +provides-extras = ["arangodb", "aws", "azurite", "cassandra", "clickhouse", "cosmosdb", "cockroachdb", "db2", "elasticsearch", "generic", "test-module-import", "google", "influxdb", "k3s", "kafka", "keycloak", "localstack", "mailpit", "memcached", "minio", "milvus", "mongodb", "mqtt", "mssql", "mysql", "nats", "neo4j", "nginx", "openfga", "opensearch", "ollama", "oracle", "oracle-free", "postgres", "qdrant", "rabbitmq", "redis", "registry", "selenium", "scylla", "sftp", "valkey", "vault", "temporal", "weaviate", "chroma", "trino"] [package.metadata.requires-dev] dev = [ From e2b556b241764794eaedac01dcf17e88a0c6c59b Mon Sep 17 00:00:00 2001 From: Borys Date: Mon, 16 Feb 2026 21:55:19 +0100 Subject: [PATCH 23/23] corrected description --- docs/modules/temporal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/temporal.md b/docs/modules/temporal.md index 5a986fd35..ac960b86e 100644 --- a/docs/modules/temporal.md +++ b/docs/modules/temporal.md @@ -4,7 +4,7 @@ The Testcontainers module for [Temporal](https://temporal.io/) — a durable execution platform for running reliable, long-running workflows. -This module spins up the Temporal dev server (`temporalio/auto-setup`) which includes the Temporal server, a preconfigured `default` namespace, and the Web UI. +This module spins up the Temporal dev server (`temporalio/temporal`) which includes the Temporal server, a preconfigured `default` namespace, and the Web UI. ## Adding this module to your project dependencies