diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd801c76..499ae7f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
+## v26.06.70 (2026-06-07)
+
+### Performance (notifications/ECM — pooled outbound HTTP clients)
+
+The SendGrid/Resend/Twilio/Firebase notification providers and the DocuSign/Adobe Sign/Logalty
+e-signature adapters built a **new `httpx.AsyncClient` per call** (no connection reuse). They now
+keep one long-lived, lazily-created client (connection pool reused across calls) and close it on
+shutdown via new `start()`/`stop()` lifecycle methods. A shared `PooledHttpClient` async-context
+wrapper keeps the existing `async with await self._client()` call sites unchanged while reusing
+the pooled client (it does not close on exit). Found by the ports/adapters audit.
+
## v26.06.69 (2026-06-07)
### Added (OAuth2 — persistent token stores; fixes a multi-instance production blocker)
diff --git a/README.md b/README.md
index 95173ec8..c8129544 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
diff --git a/pyproject.toml b/pyproject.toml
index c0109ae8..b18a7ed8 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.69"
+version = "26.6.70"
description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more."
readme = "README.md"
license = "Apache-2.0"
diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py
index 0981a311..1cb50280 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.69"
+__version__ = "26.06.70"
diff --git a/src/pyfly/client/pooled.py b/src/pyfly/client/pooled.py
new file mode 100644
index 00000000..3229c628
--- /dev/null
+++ b/src/pyfly/client/pooled.py
@@ -0,0 +1,37 @@
+# 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.
+"""Pooled HTTP client helper for outbound provider adapters.
+
+Lets a provider keep ONE long-lived ``httpx.AsyncClient`` (connection pool reused across calls)
+while leaving the existing ``async with await self._client() as client:`` call sites unchanged —
+:class:`PooledHttpClient` is an async context manager that yields the shared client but does
+**not** close it on exit. The client is closed once, on the provider's ``stop()`` lifecycle.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+
+class PooledHttpClient:
+ """Async-CM wrapper yielding a shared client without closing it on ``__aexit__``."""
+
+ def __init__(self, client: Any) -> None:
+ self._client = client
+
+ async def __aenter__(self) -> Any:
+ return self._client
+
+ async def __aexit__(self, *exc: Any) -> bool:
+ return False # keep the pooled client open for reuse
diff --git a/src/pyfly/ecm/adapters/adobe_sign.py b/src/pyfly/ecm/adapters/adobe_sign.py
index 355a4e85..39518f8a 100644
--- a/src/pyfly/ecm/adapters/adobe_sign.py
+++ b/src/pyfly/ecm/adapters/adobe_sign.py
@@ -7,6 +7,7 @@
from datetime import UTC, datetime
from typing import Any
+from pyfly.client.pooled import PooledHttpClient
from pyfly.ecm.models import (
ESignatureEnvelope,
ESignatureStatus,
@@ -27,14 +28,17 @@ class AdobeSignESignatureAdapter:
def __init__(self, *, api_base: str, access_token: str) -> None:
self._api_base = api_base.rstrip("/")
self._access_token = access_token
+ self._http: Any = None
async def _client(self) -> Any:
- try:
- import httpx # type: ignore[import-not-found, unused-ignore]
- except ImportError as exc: # noqa: BLE001
- msg = "AdobeSignESignatureAdapter requires httpx — `pip install pyfly[client]`"
- raise ImportError(msg) from exc
- return httpx.AsyncClient(timeout=60.0)
+ if self._http is None:
+ try:
+ import httpx # type: ignore[import-not-found, unused-ignore]
+ except ImportError as exc: # noqa: BLE001
+ msg = "AdobeSignESignatureAdapter requires httpx — `pip install pyfly[client]`"
+ raise ImportError(msg) from exc
+ self._http = httpx.AsyncClient(timeout=60.0)
+ return PooledHttpClient(self._http)
@property
def _headers(self) -> dict[str, str]:
@@ -95,6 +99,15 @@ async def cancel(self, envelope_id: str) -> bool:
)
return resp.status_code in (200, 204)
+ async def start(self) -> None:
+ """No-op — the pooled HTTP client is created lazily on first use."""
+
+ async def stop(self) -> None:
+ """Close the pooled HTTP client on shutdown."""
+ if self._http is not None:
+ await self._http.aclose()
+ self._http = None
+
def _map_status(value: str) -> ESignatureStatus:
return {
diff --git a/src/pyfly/ecm/adapters/docusign.py b/src/pyfly/ecm/adapters/docusign.py
index 490eb9ef..619cda0e 100644
--- a/src/pyfly/ecm/adapters/docusign.py
+++ b/src/pyfly/ecm/adapters/docusign.py
@@ -8,6 +8,7 @@
from datetime import UTC, datetime
from typing import Any
+from pyfly.client.pooled import PooledHttpClient
from pyfly.ecm.models import (
ESignatureEnvelope,
ESignatureStatus,
@@ -38,14 +39,17 @@ def __init__(
self._base_url = base_url.rstrip("/")
self._account_id = account_id
self._access_token = access_token
+ self._http: Any = None
async def _client(self) -> Any:
- try:
- import httpx # type: ignore[import-not-found, unused-ignore]
- except ImportError as exc: # noqa: BLE001
- msg = "DocuSignESignatureAdapter requires httpx — `pip install pyfly[client]`"
- raise ImportError(msg) from exc
- return httpx.AsyncClient(timeout=60.0)
+ if self._http is None:
+ try:
+ import httpx # type: ignore[import-not-found, unused-ignore]
+ except ImportError as exc: # noqa: BLE001
+ msg = "DocuSignESignatureAdapter requires httpx — `pip install pyfly[client]`"
+ raise ImportError(msg) from exc
+ self._http = httpx.AsyncClient(timeout=60.0)
+ return PooledHttpClient(self._http)
@property
def _headers(self) -> dict[str, str]:
@@ -123,6 +127,15 @@ async def cancel(self, envelope_id: str) -> bool:
)
return bool(resp.status_code == 200)
+ async def start(self) -> None:
+ """No-op — the pooled HTTP client is created lazily on first use."""
+
+ async def stop(self) -> None:
+ """Close the pooled HTTP client on shutdown."""
+ if self._http is not None:
+ await self._http.aclose()
+ self._http = None
+
def _map_status(value: str) -> ESignatureStatus:
return {
diff --git a/src/pyfly/ecm/adapters/logalty.py b/src/pyfly/ecm/adapters/logalty.py
index c075d6a4..5638a52d 100644
--- a/src/pyfly/ecm/adapters/logalty.py
+++ b/src/pyfly/ecm/adapters/logalty.py
@@ -7,6 +7,7 @@
from datetime import UTC, datetime
from typing import Any
+from pyfly.client.pooled import PooledHttpClient
from pyfly.ecm.models import (
ESignatureEnvelope,
ESignatureStatus,
@@ -27,14 +28,17 @@ class LogaltyESignatureAdapter:
def __init__(self, *, api_base: str, api_key: str) -> None:
self._api_base = api_base.rstrip("/")
self._api_key = api_key
+ self._http: Any = None
async def _client(self) -> Any:
- try:
- import httpx # type: ignore[import-not-found, unused-ignore]
- except ImportError as exc: # noqa: BLE001
- msg = "LogaltyESignatureAdapter requires httpx — `pip install pyfly[client]`"
- raise ImportError(msg) from exc
- return httpx.AsyncClient(timeout=60.0)
+ if self._http is None:
+ try:
+ import httpx # type: ignore[import-not-found, unused-ignore]
+ except ImportError as exc: # noqa: BLE001
+ msg = "LogaltyESignatureAdapter requires httpx — `pip install pyfly[client]`"
+ raise ImportError(msg) from exc
+ self._http = httpx.AsyncClient(timeout=60.0)
+ return PooledHttpClient(self._http)
@property
def _headers(self) -> dict[str, str]:
@@ -82,6 +86,15 @@ async def cancel(self, envelope_id: str) -> bool:
resp = await client.delete(f"{self._api_base}/envelopes/{envelope_id}", headers=self._headers)
return resp.status_code in (200, 204)
+ async def start(self) -> None:
+ """No-op — the pooled HTTP client is created lazily on first use."""
+
+ async def stop(self) -> None:
+ """Close the pooled HTTP client on shutdown."""
+ if self._http is not None:
+ await self._http.aclose()
+ self._http = None
+
def _map_status(value: str) -> ESignatureStatus:
return {
diff --git a/src/pyfly/notifications/providers/firebase.py b/src/pyfly/notifications/providers/firebase.py
index 87cdeb36..2995f847 100644
--- a/src/pyfly/notifications/providers/firebase.py
+++ b/src/pyfly/notifications/providers/firebase.py
@@ -6,6 +6,7 @@
from typing import Any
+from pyfly.client.pooled import PooledHttpClient
from pyfly.notifications.models import EmailStatus, NotificationResult, PushMessage
@@ -22,14 +23,17 @@ class FirebasePushProvider:
def __init__(self, *, project_id: str, access_token: str) -> None:
self._project_id = project_id
self._access_token = access_token
+ self._http: Any = None
async def _client(self) -> Any:
- try:
- import httpx # type: ignore[import-not-found, unused-ignore]
- except ImportError as exc: # noqa: BLE001
- msg = "FirebasePushProvider requires httpx — `pip install pyfly[client]`"
- raise ImportError(msg) from exc
- return httpx.AsyncClient(timeout=30.0)
+ if self._http is None:
+ try:
+ import httpx # type: ignore[import-not-found, unused-ignore]
+ except ImportError as exc: # noqa: BLE001
+ msg = "FirebasePushProvider requires httpx — `pip install pyfly[client]`"
+ raise ImportError(msg) from exc
+ self._http = httpx.AsyncClient(timeout=30.0)
+ return PooledHttpClient(self._http)
async def send(self, message: PushMessage) -> NotificationResult:
async with await self._client() as client:
@@ -62,3 +66,12 @@ async def send(self, message: PushMessage) -> NotificationResult:
error="; ".join(errors) or None,
provider_id=";".join(sent_ids) or None,
)
+
+ async def start(self) -> None:
+ """No-op — the pooled HTTP client is created lazily on first use."""
+
+ async def stop(self) -> None:
+ """Close the pooled HTTP client on shutdown."""
+ if self._http is not None:
+ await self._http.aclose()
+ self._http = None
diff --git a/src/pyfly/notifications/providers/resend.py b/src/pyfly/notifications/providers/resend.py
index 942825e3..3b28243b 100644
--- a/src/pyfly/notifications/providers/resend.py
+++ b/src/pyfly/notifications/providers/resend.py
@@ -6,6 +6,7 @@
from typing import Any
+from pyfly.client.pooled import PooledHttpClient
from pyfly.notifications.models import EmailMessage, EmailStatus, NotificationResult
@@ -20,14 +21,17 @@ def __init__(
self._api_key = api_key
self._api_base = api_base.rstrip("/")
self._default_from = default_from
+ self._http: Any = None
async def _client(self) -> Any:
- try:
- import httpx # type: ignore[import-not-found, unused-ignore]
- except ImportError as exc: # noqa: BLE001
- msg = "ResendEmailProvider requires httpx — `pip install pyfly[client]`"
- raise ImportError(msg) from exc
- return httpx.AsyncClient(timeout=30.0)
+ if self._http is None:
+ try:
+ import httpx # type: ignore[import-not-found, unused-ignore]
+ except ImportError as exc: # noqa: BLE001
+ msg = "ResendEmailProvider requires httpx — `pip install pyfly[client]`"
+ raise ImportError(msg) from exc
+ self._http = httpx.AsyncClient(timeout=30.0)
+ return PooledHttpClient(self._http)
async def send(self, message: EmailMessage) -> NotificationResult:
async with await self._client() as client:
@@ -73,3 +77,12 @@ async def send(self, message: EmailMessage) -> NotificationResult:
status=EmailStatus.FAILED,
error=f"http {resp.status_code}: {resp.text}",
)
+
+ async def start(self) -> None:
+ """No-op — the pooled HTTP client is created lazily on first use."""
+
+ async def stop(self) -> None:
+ """Close the pooled HTTP client on shutdown."""
+ if self._http is not None:
+ await self._http.aclose()
+ self._http = None
diff --git a/src/pyfly/notifications/providers/sendgrid.py b/src/pyfly/notifications/providers/sendgrid.py
index 5a9d3c3f..f7da57ec 100644
--- a/src/pyfly/notifications/providers/sendgrid.py
+++ b/src/pyfly/notifications/providers/sendgrid.py
@@ -7,6 +7,7 @@
import base64
from typing import Any
+from pyfly.client.pooled import PooledHttpClient
from pyfly.notifications.models import EmailMessage, EmailStatus, NotificationResult
@@ -18,14 +19,17 @@ class SendGridEmailProvider:
def __init__(self, api_key: str, *, api_base: str = "https://api.sendgrid.com/v3") -> None:
self._api_key = api_key
self._api_base = api_base.rstrip("/")
+ self._http: Any = None
async def _client(self) -> Any:
- try:
- import httpx # type: ignore[import-not-found, unused-ignore]
- except ImportError as exc: # noqa: BLE001
- msg = "SendGridEmailProvider requires httpx — `pip install pyfly[client]`"
- raise ImportError(msg) from exc
- return httpx.AsyncClient(timeout=30.0)
+ if self._http is None:
+ try:
+ import httpx # type: ignore[import-not-found, unused-ignore]
+ except ImportError as exc: # noqa: BLE001
+ msg = "SendGridEmailProvider requires httpx — `pip install pyfly[client]`"
+ raise ImportError(msg) from exc
+ self._http = httpx.AsyncClient(timeout=30.0)
+ return PooledHttpClient(self._http)
async def send(self, message: EmailMessage) -> NotificationResult:
async with await self._client() as client:
@@ -81,3 +85,12 @@ async def send(self, message: EmailMessage) -> NotificationResult:
status=EmailStatus.FAILED,
error=f"http {resp.status_code}: {resp.text}",
)
+
+ async def start(self) -> None:
+ """No-op — the pooled HTTP client is created lazily on first use."""
+
+ async def stop(self) -> None:
+ """Close the pooled HTTP client on shutdown."""
+ if self._http is not None:
+ await self._http.aclose()
+ self._http = None
diff --git a/src/pyfly/notifications/providers/twilio.py b/src/pyfly/notifications/providers/twilio.py
index f4c918db..eece57c3 100644
--- a/src/pyfly/notifications/providers/twilio.py
+++ b/src/pyfly/notifications/providers/twilio.py
@@ -6,6 +6,7 @@
from typing import Any
+from pyfly.client.pooled import PooledHttpClient
from pyfly.notifications.models import EmailStatus, NotificationResult, SmsMessage
@@ -18,14 +19,17 @@ def __init__(self, account_sid: str, auth_token: str, *, from_number: str | None
self._sid = account_sid
self._token = auth_token
self._from = from_number
+ self._http: Any = None
async def _client(self) -> Any:
- try:
- import httpx # type: ignore[import-not-found, unused-ignore]
- except ImportError as exc: # noqa: BLE001
- msg = "TwilioSmsProvider requires httpx — `pip install pyfly[client]`"
- raise ImportError(msg) from exc
- return httpx.AsyncClient(timeout=30.0)
+ if self._http is None:
+ try:
+ import httpx # type: ignore[import-not-found, unused-ignore]
+ except ImportError as exc: # noqa: BLE001
+ msg = "TwilioSmsProvider requires httpx — `pip install pyfly[client]`"
+ raise ImportError(msg) from exc
+ self._http = httpx.AsyncClient(timeout=30.0)
+ return PooledHttpClient(self._http)
async def send(self, message: SmsMessage) -> NotificationResult:
async with await self._client() as client:
@@ -49,3 +53,12 @@ async def send(self, message: SmsMessage) -> NotificationResult:
status=EmailStatus.FAILED,
error=f"http {resp.status_code}: {resp.text}",
)
+
+ async def start(self) -> None:
+ """No-op — the pooled HTTP client is created lazily on first use."""
+
+ async def stop(self) -> None:
+ """Close the pooled HTTP client on shutdown."""
+ if self._http is not None:
+ await self._http.aclose()
+ self._http = None
diff --git a/tests/client/test_pooled_http_client.py b/tests/client/test_pooled_http_client.py
new file mode 100644
index 00000000..ba245e0e
--- /dev/null
+++ b/tests/client/test_pooled_http_client.py
@@ -0,0 +1,63 @@
+# 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.
+"""Pooled outbound HTTP client (v26.06.70) — wrapper + provider pooling/lifecycle."""
+
+from __future__ import annotations
+
+import pytest
+
+from pyfly.client.pooled import PooledHttpClient
+
+
+class _FakeClient:
+ def __init__(self) -> None:
+ self.closed = False
+
+ async def aclose(self) -> None:
+ self.closed = True
+
+
+@pytest.mark.asyncio
+async def test_pooled_wrapper_does_not_close_on_exit() -> None:
+ fake = _FakeClient()
+ async with PooledHttpClient(fake) as client:
+ assert client is fake
+ assert fake.closed is False # the shared client stays open for reuse
+
+
+@pytest.mark.asyncio
+async def test_email_provider_pools_and_closes_client() -> None:
+ from pyfly.notifications.providers.sendgrid import SendGridEmailProvider
+
+ provider = SendGridEmailProvider(api_key="x")
+ async with await provider._client() as c1:
+ pass
+ async with await provider._client() as c2:
+ pass
+ assert c1 is c2 # one pooled client reused across calls
+ assert not c1.is_closed
+ await provider.stop()
+ assert c1.is_closed and provider._http is None
+
+
+@pytest.mark.asyncio
+async def test_ecm_adapter_pools_and_closes_client() -> None:
+ from pyfly.ecm.adapters.docusign import DocuSignESignatureAdapter
+
+ adapter = DocuSignESignatureAdapter(base_url="https://demo.docusign.net", account_id="a", access_token="t")
+ c1 = (await adapter._client())._client
+ c2 = (await adapter._client())._client
+ assert c1 is c2
+ await adapter.stop()
+ assert c1.is_closed and adapter._http is None
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
new file mode 100644
index 00000000..08a143ef
--- /dev/null
+++ b/tests/integration/conftest.py
@@ -0,0 +1,59 @@
+# 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.
+"""Shared fixtures for testcontainers-backed integration tests.
+
+``@requires_docker`` only checks that a Docker *daemon* answers — but CI can have a daemon that
+cannot *pull* images (registry timeout). These fixtures therefore start the container inside a
+guard and ``pytest.skip`` on any startup failure, so the suite degrades to "skipped" (never
+"errored") wherever Docker isn't fully functional.
+"""
+
+from __future__ import annotations
+
+import contextlib
+from collections.abc import Iterator
+
+import pytest
+
+from pyfly.testing import postgres_container, pyfly_config_for, redis_container
+
+
+@pytest.fixture
+def redis_url() -> Iterator[str]:
+ try:
+ container = redis_container()
+ container.start()
+ except Exception as exc: # noqa: BLE001 — daemon present but cannot run/pull -> skip, don't error
+ pytest.skip(f"Redis testcontainer unavailable: {exc}")
+ try:
+ host = container.get_container_host_ip()
+ port = container.get_exposed_port(6379)
+ yield f"redis://{host}:{port}/0"
+ finally:
+ with contextlib.suppress(Exception):
+ container.stop()
+
+
+@pytest.fixture
+def pg_url() -> Iterator[str]:
+ try:
+ container = postgres_container()
+ container.start()
+ except Exception as exc: # noqa: BLE001
+ pytest.skip(f"Postgres testcontainer unavailable: {exc}")
+ try:
+ yield pyfly_config_for(container)["pyfly.data.relational.url"]
+ finally:
+ with contextlib.suppress(Exception):
+ container.stop()
diff --git a/tests/integration/test_postgres_lock_integration.py b/tests/integration/test_postgres_lock_integration.py
index 68a6878b..fe1466f2 100644
--- a/tests/integration/test_postgres_lock_integration.py
+++ b/tests/integration/test_postgres_lock_integration.py
@@ -15,17 +15,9 @@
from __future__ import annotations
-from collections.abc import Iterator
-
import pytest
-from pyfly.testing import postgres_container, pyfly_config_for, requires_docker
-
-
-@pytest.fixture
-def pg_url() -> Iterator[str]:
- with postgres_container() as container:
- yield pyfly_config_for(container)["pyfly.data.relational.url"]
+from pyfly.testing import requires_docker # the `pg_url` fixture is provided by conftest.py
@requires_docker
diff --git a/tests/integration/test_postgres_session_registry_integration.py b/tests/integration/test_postgres_session_registry_integration.py
index 15b5d703..bf001655 100644
--- a/tests/integration/test_postgres_session_registry_integration.py
+++ b/tests/integration/test_postgres_session_registry_integration.py
@@ -15,17 +15,9 @@
from __future__ import annotations
-from collections.abc import Iterator
-
import pytest
-from pyfly.testing import postgres_container, pyfly_config_for, requires_docker
-
-
-@pytest.fixture
-def pg_url() -> Iterator[str]:
- with postgres_container() as container:
- yield pyfly_config_for(container)["pyfly.data.relational.url"]
+from pyfly.testing import requires_docker # the `pg_url` fixture is provided by conftest.py
@requires_docker
diff --git a/tests/integration/test_redis_adapters_integration.py b/tests/integration/test_redis_adapters_integration.py
index 0dc2882f..8b5f41ea 100644
--- a/tests/integration/test_redis_adapters_integration.py
+++ b/tests/integration/test_redis_adapters_integration.py
@@ -21,19 +21,10 @@
from __future__ import annotations
import asyncio
-from collections.abc import Iterator
import pytest
-from pyfly.testing import redis_container, requires_docker
-
-
-@pytest.fixture
-def redis_url() -> Iterator[str]:
- with redis_container() as container:
- host = container.get_container_host_ip()
- port = container.get_exposed_port(6379)
- yield f"redis://{host}:{port}/0"
+from pyfly.testing import requires_docker # the `redis_url` fixture is provided by conftest.py
@requires_docker
diff --git a/tests/integration/test_token_store_integration.py b/tests/integration/test_token_store_integration.py
index 192ea7cd..686cad7b 100644
--- a/tests/integration/test_token_store_integration.py
+++ b/tests/integration/test_token_store_integration.py
@@ -15,23 +15,9 @@
from __future__ import annotations
-from collections.abc import Iterator
-
import pytest
-from pyfly.testing import postgres_container, pyfly_config_for, redis_container, requires_docker
-
-
-@pytest.fixture
-def redis_url() -> Iterator[str]:
- with redis_container() as container:
- yield f"redis://{container.get_container_host_ip()}:{container.get_exposed_port(6379)}/0"
-
-
-@pytest.fixture
-def pg_url() -> Iterator[str]:
- with postgres_container() as container:
- yield pyfly_config_for(container)["pyfly.data.relational.url"]
+from pyfly.testing import requires_docker # `redis_url` / `pg_url` fixtures provided by conftest.py
@requires_docker
diff --git a/uv.lock b/uv.lock
index 1fb488ef..d4d33554 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1981,7 +1981,7 @@ wheels = [
[[package]]
name = "pyfly"
-version = "26.6.69"
+version = "26.6.70"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },