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
[](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)
[](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