diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2f1664..e6ecac8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
+## v26.06.31 (2026-06-07)
+
+### Added (testing — Testcontainers integration)
+
+Wave 4 (final) — the Spring Boot `@Testcontainers` / `@ServiceConnection` equivalent,
+in `pyfly.testing.testcontainers`.
+
+- **Container factories** (`postgres_container`, `mysql_container`, `redis_container`,
+ `mongodb_container`, `kafka_container`) wrapping `testcontainers` (new optional
+ extra `pyfly[testcontainers]`; lazily imported with a clear install hint).
+- **`@ServiceConnection`-style auto-wiring**: `pyfly_config_for(container)` maps a
+ started container to the right pyfly config keys (Postgres/MySQL → async
+ `pyfly.data.relational.url`; Redis → `pyfly.cache.redis.url` + `pyfly.session.redis.url`;
+ Mongo → `pyfly.data.document.uri`; Kafka → `pyfly.eda.kafka.bootstrap-servers`), and
+ `pyfly_config(*containers)` builds a ready-to-use `Config`.
+- **Graceful skip**: `is_docker_available()` + the `@requires_docker` decorator skip
+ integration tests cleanly when Docker (or the extra) is absent.
+
+All exported from `pyfly.testing`.
+
+---
+
## v26.06.30 (2026-06-07)
### Added (core — SpEL-lite expression evaluation)
diff --git a/README.md b/README.md
index 3ee5eb0..e0d9825 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
diff --git a/pyproject.toml b/pyproject.toml
index a4653eb..f62cb1f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ name = "pyfly"
# CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4);
# git tag, GitHub release and human-readable display use leading-zero form
# (v26.05.04) to match the Java/.NET/Go siblings.
-version = "26.6.30"
+version = "26.6.31"
description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more."
readme = "README.md"
license = "Apache-2.0"
@@ -50,6 +50,9 @@ data-relational = [
testing = [
"jsonpath-ng>=1.8.0",
]
+testcontainers = [
+ "testcontainers>=4.0.0",
+]
data-document = [
"motor>=3.7.1",
"beanie>=2.1.0",
diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py
index 1911086..1d50976 100644
--- a/src/pyfly/__init__.py
+++ b/src/pyfly/__init__.py
@@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""
-__version__ = "26.06.30"
+__version__ = "26.06.31"
diff --git a/src/pyfly/testing/__init__.py b/src/pyfly/testing/__init__.py
index 4d05b50..75b8fee 100644
--- a/src/pyfly/testing/__init__.py
+++ b/src/pyfly/testing/__init__.py
@@ -19,6 +19,17 @@
from pyfly.testing.fixtures import PyFlyTestCase
from pyfly.testing.mock import mock_bean
from pyfly.testing.slices import DataTest, ServiceTest, WebTest, get_test_slice
+from pyfly.testing.testcontainers import (
+ is_docker_available,
+ kafka_container,
+ mongodb_container,
+ mysql_container,
+ postgres_container,
+ pyfly_config,
+ pyfly_config_for,
+ redis_container,
+ requires_docker,
+)
__all__ = [
"DataTest",
@@ -31,5 +42,14 @@
"assert_no_events_published",
"create_test_container",
"get_test_slice",
+ "is_docker_available",
+ "kafka_container",
"mock_bean",
+ "mongodb_container",
+ "mysql_container",
+ "postgres_container",
+ "pyfly_config",
+ "pyfly_config_for",
+ "redis_container",
+ "requires_docker",
]
diff --git a/src/pyfly/testing/testcontainers.py b/src/pyfly/testing/testcontainers.py
new file mode 100644
index 0000000..725ed01
--- /dev/null
+++ b/src/pyfly/testing/testcontainers.py
@@ -0,0 +1,167 @@
+# Copyright 2026 Firefly Software Foundation.
+#
+# 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.
+"""Testcontainers — Docker-backed integration-test fixtures.
+
+The Spring Boot ``@Testcontainers`` / ``@ServiceConnection`` equivalent: spin up a
+real Postgres/MySQL/Redis/MongoDB/Kafka in Docker, then wire its connection details
+straight into pyfly config keys via :func:`pyfly_config_for` / :func:`pyfly_config`.
+
+Requires the extra and a running Docker daemon::
+
+ pip install 'pyfly[testcontainers]'
+
+Guard integration tests so they skip cleanly where Docker is unavailable::
+
+ from pyfly.testing.testcontainers import postgres_container, pyfly_config, requires_docker
+
+ @requires_docker
+ def test_with_real_postgres():
+ with postgres_container() as pg:
+ config = pyfly_config(pg) # -> pyfly.data.relational.url = the container
+ ...
+"""
+
+from __future__ import annotations
+
+import importlib
+from typing import TYPE_CHECKING, Any, TypeVar, cast
+
+if TYPE_CHECKING:
+ from pyfly.core.config import Config
+
+F = TypeVar("F")
+
+_EXTRA_HINT = (
+ "Testcontainers support requires the extra and a running Docker daemon: pip install 'pyfly[testcontainers]'."
+)
+_NO_DOCKER = "Docker is not available (daemon down or the pyfly[testcontainers] extra is not installed)."
+
+
+def is_docker_available() -> bool:
+ """Whether a Docker daemon is reachable — integration tests should skip if not."""
+ try:
+ import docker # type: ignore[import-untyped]
+ except ModuleNotFoundError:
+ return False
+ try:
+ docker.from_env().ping()
+ return True
+ except Exception: # noqa: BLE001 - any connectivity failure means "not available"
+ return False
+
+
+def requires_docker(func: F) -> F:
+ """``pytest`` decorator that skips the test when Docker is unavailable."""
+ import pytest
+
+ return cast(F, pytest.mark.skipif(not is_docker_available(), reason=_NO_DOCKER)(func))
+
+
+def _load(module: str, name: str) -> Any:
+ try:
+ loaded = importlib.import_module(module)
+ except ModuleNotFoundError as exc: # pragma: no cover - exercised only without the extra
+ raise RuntimeError(_EXTRA_HINT) from exc
+ return getattr(loaded, name)
+
+
+def postgres_container(image: str = "postgres:16-alpine", **kwargs: Any) -> Any:
+ """A ``testcontainers`` PostgresContainer (start via ``with``)."""
+ return _load("testcontainers.postgres", "PostgresContainer")(image, **kwargs)
+
+
+def mysql_container(image: str = "mysql:8", **kwargs: Any) -> Any:
+ """A ``testcontainers`` MySqlContainer."""
+ return _load("testcontainers.mysql", "MySqlContainer")(image, **kwargs)
+
+
+def redis_container(image: str = "redis:7-alpine", **kwargs: Any) -> Any:
+ """A ``testcontainers`` RedisContainer."""
+ return _load("testcontainers.redis", "RedisContainer")(image, **kwargs)
+
+
+def mongodb_container(image: str = "mongo:7", **kwargs: Any) -> Any:
+ """A ``testcontainers`` MongoDbContainer."""
+ return _load("testcontainers.mongodb", "MongoDbContainer")(image, **kwargs)
+
+
+def kafka_container(image: str = "confluentinc/cp-kafka:7.6.0", **kwargs: Any) -> Any:
+ """A ``testcontainers`` KafkaContainer."""
+ return _load("testcontainers.kafka", "KafkaContainer")(image, **kwargs)
+
+
+def _async_db_url(url: str, replacements: dict[str, str]) -> str:
+ for sync_prefix, async_prefix in replacements.items():
+ if url.startswith(sync_prefix):
+ return async_prefix + url[len(sync_prefix) :]
+ return url
+
+
+def pyfly_config_for(container: Any) -> dict[str, Any]:
+ """Map a **started** testcontainer to pyfly config overrides (the ``@ServiceConnection``
+ equivalent). Returns flat dotted keys; raises ``ValueError`` for unmapped types.
+ """
+ name = type(container).__name__
+ if "Postgres" in name:
+ url = _async_db_url(
+ container.get_connection_url(),
+ {
+ "postgresql+psycopg2://": "postgresql+asyncpg://",
+ "postgresql+psycopg://": "postgresql+asyncpg://",
+ "postgresql://": "postgresql+asyncpg://",
+ },
+ )
+ return {"pyfly.data.relational.url": url}
+ if "MySql" in name or "MySQL" in name:
+ url = _async_db_url(
+ container.get_connection_url(),
+ {"mysql+pymysql://": "mysql+aiomysql://", "mysql://": "mysql+aiomysql://"},
+ )
+ return {"pyfly.data.relational.url": url}
+ if "Redis" in name:
+ host = container.get_container_host_ip()
+ port = container.get_exposed_port(6379)
+ url = f"redis://{host}:{port}/0"
+ return {"pyfly.cache.redis.url": url, "pyfly.session.redis.url": url}
+ if "Mongo" in name:
+ return {"pyfly.data.document.uri": container.get_connection_url()}
+ if "Kafka" in name:
+ return {"pyfly.eda.kafka.bootstrap-servers": container.get_bootstrap_server()}
+ raise ValueError(f"No pyfly config mapping for container type {name!r}")
+
+
+def _nest(flat: dict[str, Any]) -> dict[str, Any]:
+ """Turn flat dotted keys into a nested dict (``{'a.b': 1}`` -> ``{'a': {'b': 1}}``)."""
+ root: dict[str, Any] = {}
+ for dotted, value in flat.items():
+ node = root
+ parts = dotted.split(".")
+ for part in parts[:-1]:
+ node = node.setdefault(part, {})
+ node[parts[-1]] = value
+ return root
+
+
+def pyfly_config(*containers: Any, base: dict[str, Any] | None = None) -> Config:
+ """Build a pyfly ``Config`` wiring every started container's connection details.
+
+ Merges :func:`pyfly_config_for` for each container (plus optional *base* flat
+ overrides) into a nested config — one-call setup for an integration ApplicationContext.
+ """
+ from pyfly.core.config import Config
+
+ merged: dict[str, Any] = dict(base or {})
+ for container in containers:
+ merged.update(pyfly_config_for(container))
+ return Config(_nest(merged))
diff --git a/tests/testing/test_testcontainers.py b/tests/testing/test_testcontainers.py
new file mode 100644
index 0000000..808d85c
--- /dev/null
+++ b/tests/testing/test_testcontainers.py
@@ -0,0 +1,119 @@
+# Copyright 2026 Firefly Software Foundation.
+#
+# 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.
+"""Testcontainers integration (v26.06.31): @ServiceConnection-style config mapping,
+graceful skip without Docker, and clear error without the extra.
+
+The connection-mapping logic is tested with duck-typed fakes (no Docker needed); real
+container startup is covered by the @requires_docker skip path.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+from pyfly.testing.testcontainers import (
+ _load,
+ _nest,
+ is_docker_available,
+ pyfly_config,
+ pyfly_config_for,
+ requires_docker,
+)
+
+
+class _FakePostgres:
+ def get_connection_url(self) -> str:
+ return "postgresql+psycopg2://u:p@h:5432/test"
+
+
+class _FakeMySql:
+ def get_connection_url(self) -> str:
+ return "mysql+pymysql://u:p@h:3306/test"
+
+
+class _FakeRedis:
+ def get_container_host_ip(self) -> str:
+ return "127.0.0.1"
+
+ def get_exposed_port(self, port: int) -> int:
+ return 55001
+
+
+class _FakeMongoDb:
+ def get_connection_url(self) -> str:
+ return "mongodb://h:27017"
+
+
+class _FakeKafka:
+ def get_bootstrap_server(self) -> str:
+ return "127.0.0.1:55002"
+
+
+def test_postgres_mapping_to_async_url() -> None:
+ assert pyfly_config_for(_FakePostgres()) == {"pyfly.data.relational.url": "postgresql+asyncpg://u:p@h:5432/test"}
+
+
+def test_mysql_mapping_to_async_url() -> None:
+ assert pyfly_config_for(_FakeMySql()) == {"pyfly.data.relational.url": "mysql+aiomysql://u:p@h:3306/test"}
+
+
+def test_redis_mapping_to_cache_and_session() -> None:
+ cfg = pyfly_config_for(_FakeRedis())
+ assert cfg["pyfly.cache.redis.url"] == "redis://127.0.0.1:55001/0"
+ assert cfg["pyfly.session.redis.url"] == "redis://127.0.0.1:55001/0"
+
+
+def test_mongo_and_kafka_mappings() -> None:
+ assert pyfly_config_for(_FakeMongoDb()) == {"pyfly.data.document.uri": "mongodb://h:27017"}
+ assert pyfly_config_for(_FakeKafka()) == {"pyfly.eda.kafka.bootstrap-servers": "127.0.0.1:55002"}
+
+
+def test_unmapped_container_raises() -> None:
+ class Other:
+ pass
+
+ with pytest.raises(ValueError):
+ pyfly_config_for(Other())
+
+
+def test_pyfly_config_builds_nested_config() -> None:
+ cfg = pyfly_config(_FakePostgres(), _FakeRedis())
+ assert cfg.get("pyfly.data.relational.url") == "postgresql+asyncpg://u:p@h:5432/test"
+ assert cfg.get("pyfly.cache.redis.url") == "redis://127.0.0.1:55001/0"
+
+
+def test_nest_flat_keys() -> None:
+ assert _nest({"a.b.c": 1, "a.b.d": 2, "x": 3}) == {"a": {"b": {"c": 1, "d": 2}}, "x": 3}
+
+
+def test_is_docker_available_returns_bool() -> None:
+ assert isinstance(is_docker_available(), bool)
+
+
+def test_load_missing_module_raises_clear_install_hint() -> None:
+ # _load surfaces the extra/install hint when a backing module is unavailable —
+ # independent of whether the testcontainers extra happens to be installed.
+ with pytest.raises(RuntimeError, match=r"pyfly\[testcontainers\]"):
+ _load("testcontainers._pyfly_nonexistent_module", "Nope")
+
+
+def test_requires_docker_applies_skipif_mark() -> None:
+ # Robust across environments (Docker present or not): the decorator must attach a
+ # skipif mark so the test skips wherever Docker is unavailable.
+ @requires_docker
+ def dummy() -> None:
+ pass
+
+ marks = getattr(dummy, "pytestmark", [])
+ assert any(getattr(mark, "name", "") == "skipif" for mark in marks)
diff --git a/uv.lock b/uv.lock
index 2aa00a0..df999b5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -720,6 +720,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
+[[package]]
+name = "docker"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "requests" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" },
+]
+
[[package]]
name = "fastapi"
version = "0.136.1"
@@ -1967,7 +1981,7 @@ wheels = [
[[package]]
name = "pyfly"
-version = "26.6.30"
+version = "26.6.31"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },
@@ -2074,6 +2088,9 @@ security = [
shell = [
{ name = "click" },
]
+testcontainers = [
+ { name = "testcontainers" },
+]
testing = [
{ name = "jsonpath-ng" },
]
@@ -2149,11 +2166,12 @@ requires-dist = [
{ name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'data-relational'", specifier = ">=2.0.49" },
{ name = "starlette", marker = "extra == 'web'", specifier = ">=1.0.0" },
{ name = "structlog", marker = "extra == 'observability'", specifier = ">=25.5.0" },
+ { name = "testcontainers", marker = "extra == 'testcontainers'", specifier = ">=4.0.0" },
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = ">=0.46.0" },
{ name = "uvloop", marker = "sys_platform != 'win32' and extra == 'web-fast'", specifier = ">=0.22.1" },
{ name = "uvloop", marker = "sys_platform != 'win32' and extra == 'web-fastapi'", specifier = ">=0.22.1" },
]
-provides-extras = ["web", "data-relational", "testing", "data-document", "postgresql", "eda", "fastapi", "granian", "hypercorn", "kafka", "rabbitmq", "redis", "cache", "client", "observability", "scheduling", "pii", "security", "cli", "shell", "web-fast", "web-fastapi", "full"]
+provides-extras = ["web", "data-relational", "testing", "testcontainers", "data-document", "postgresql", "eda", "fastapi", "granian", "hypercorn", "kafka", "rabbitmq", "redis", "cache", "client", "observability", "scheduling", "pii", "security", "cli", "shell", "web-fast", "web-fastapi", "full"]
[package.metadata.requires-dev]
dev = [
@@ -2323,6 +2341,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
+[[package]]
+name = "pywin32"
+version = "312"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
+ { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
+ { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" },
+ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
+]
+
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -2773,6 +2810,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" },
]
+[[package]]
+name = "testcontainers"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docker" },
+ { name = "python-dotenv" },
+ { name = "typing-extensions" },
+ { name = "urllib3" },
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/ac/a597c3a0e02b26cbed6dd07df68be1e57684766fd1c381dee9b170a99690/testcontainers-4.14.2.tar.gz", hash = "sha256:1340ccf16fe3acd9389a6c9e1d9ab21d9fe99a8afdf8165f89c3e69c1967d239", size = 166841, upload-time = "2026-03-18T05:19:16.696Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/2d/26b8b30067d94339afee62c3edc9b803a6eb9332f521ba77d8aaab5de873/testcontainers-4.14.2-py3-none-any.whl", hash = "sha256:0d0522c3cd8f8d9627cda41f7a6b51b639fa57bdc492923c045117933c668d68", size = 125712, upload-time = "2026-03-18T05:19:15.29Z" },
+]
+
[[package]]
name = "thinc"
version = "8.3.13"