diff --git a/uv.lock b/uv.lock index c5e63b87..9072c25f 100644 --- a/uv.lock +++ b/uv.lock @@ -838,6 +838,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 = "dunamai" version = "1.26.1" @@ -2816,6 +2830,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/3c/df6641d7e2e84a6dd4de3b3a4426db7f6a7270c05bbdeadd523645c9c45f/taplo-0.9.3-py3-none-win_amd64.whl", hash = "sha256:7d80b630b93fb43cee99d1e1ee07b616236dc5615efaf7cd51074b4cffc33bab", size = 3985843, upload-time = "2024-08-19T10:22:13.446Z" }, ] +[[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.optional-dependencies] +mongodb = [ + { name = "pymongo" }, +] + [[package]] name = "tqdm" version = "4.67.3" @@ -3094,6 +3129,7 @@ dev = [ { name = "rumdl" }, { name = "semver" }, { name = "taplo" }, + { name = "testcontainers", extra = ["mongodb"] }, { name = "ty" }, { name = "types-aioboto3", extra = ["s3"] }, { name = "types-authlib" }, @@ -3143,6 +3179,7 @@ requires-dist = [ { name = "starlette-htmx", specifier = ">=0.1.1" }, { name = "streaq", extras = ["web"], specifier = ">=6.4.0,<7.0.0" }, { name = "taplo", marker = "extra == 'dev'", specifier = ">=0.9.3" }, + { name = "testcontainers", extras = ["mongodb"], marker = "extra == 'dev'", specifier = ">=4.9.0" }, { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.32" }, { name = "typer", specifier = ">=0.24.1" }, { name = "types-aioboto3", extras = ["s3"], marker = "extra == 'dev'", specifier = ">=15.5.0" }, diff --git a/vibetuner-docs/docs/architecture.md b/vibetuner-docs/docs/architecture.md index e5519178..5c917505 100644 --- a/vibetuner-docs/docs/architecture.md +++ b/vibetuner-docs/docs/architecture.md @@ -403,7 +403,7 @@ Pytest fixtures for testing vibetuner applications without external services: |---------|---------| | `vibetuner_client` | Async HTTP test client (httpx + ASGITransport) | | `vibetuner_app` | Overridable FastAPI app fixture | -| `vibetuner_db` | Temporary MongoDB database with auto-teardown | +| `vibetuner_db` | Session-scoped MongoDB database, truncated per test | | `mock_auth` | Patch authentication (login/logout without sessions) | | `mock_tasks` | Record `enqueue` calls without Redis | | `override_config` | Override `RuntimeConfig` values with auto-cleanup | diff --git a/vibetuner-docs/docs/development-guide.md b/vibetuner-docs/docs/development-guide.md index 55d20c91..69777971 100644 --- a/vibetuner-docs/docs/development-guide.md +++ b/vibetuner-docs/docs/development-guide.md @@ -1709,10 +1709,11 @@ async def test_homepage(vibetuner_client): Override `vibetuner_app` fixture to supply a custom FastAPI app instance. -#### `vibetuner_db` — Temporary MongoDB Database +#### `vibetuner_db` — Shared MongoDB Test Database -Creates a unique test database, initialises Beanie with all registered -models, and drops the database on teardown: +Creates a single MongoDB database for the whole test session, runs Beanie +index registration once, and **truncates every non-system collection +before and after each test** so each test starts with empty collections: ```python async def test_create_post(vibetuner_db): @@ -1723,6 +1724,19 @@ async def test_create_post(vibetuner_db): Skips the test automatically if `MONGODB_URL` is not set. +**Caveats:** + +- All tests in a session share the same database. Don't assert on + database-level state (existence, name, full collection drops) or on + indexes being absent. +- Indexes (including unique constraints) are built once at session + scope and persist across tests. `DuplicateKeyError` is still raised + by unique violations. +- Concurrent runs need `pytest-xdist`; the session DB name includes + the worker id (`PYTEST_XDIST_WORKER`) so workers don't collide. +- If a test crashes mid-run, the next test re-truncates on setup so + state is self-healing. + #### `mock_auth` — Authentication Mocking Patches the auth backend so requests appear authenticated without real diff --git a/vibetuner-docs/docs/llms-full.txt b/vibetuner-docs/docs/llms-full.txt index 3eac01f0..b8d58e73 100644 --- a/vibetuner-docs/docs/llms-full.txt +++ b/vibetuner-docs/docs/llms-full.txt @@ -1637,7 +1637,7 @@ auto-discovered. |---------|-------------| | `vibetuner_client` | Async HTTP test client with full middleware | | `vibetuner_app` | The FastAPI app instance (override for custom apps) | -| `vibetuner_db` | Temporary MongoDB database with Beanie initialized | +| `vibetuner_db` | Session-scoped MongoDB DB; collections truncated per test | | `mock_auth` | Mock authentication without real sessions | | `mock_tasks` | Mock background tasks without Redis | | `override_config` | Override RuntimeConfig values with auto-cleanup | diff --git a/vibetuner-docs/docs/tech-stack.md b/vibetuner-docs/docs/tech-stack.md index 9243fa3d..2f80bec1 100644 --- a/vibetuner-docs/docs/tech-stack.md +++ b/vibetuner-docs/docs/tech-stack.md @@ -197,7 +197,7 @@ Vibetuner supports multiple database backends. All are optional - choose what fi **Why:** Test vibetuner apps without external services. - `vibetuner_client` — async HTTP client with full middleware stack -- `vibetuner_db` — temporary MongoDB with auto-teardown +- `vibetuner_db` — session-scoped MongoDB, truncated per test - `mock_auth` — patch authentication without sessions or cookies - `mock_tasks` — record background task enqueue calls without Redis - `override_config` — temporarily override runtime config values diff --git a/vibetuner-py/pyproject.toml b/vibetuner-py/pyproject.toml index 94e2a6b8..16787331 100644 --- a/vibetuner-py/pyproject.toml +++ b/vibetuner-py/pyproject.toml @@ -79,6 +79,7 @@ dev = [ "pytest>=9.0.3", "pytest-asyncio>=1.3.0", "ruff>=0.15.11", + "testcontainers[mongodb]>=4.9.0", "rumdl>=0.1.77", "semver>=3.0.4", "taplo>=0.9.3", diff --git a/vibetuner-py/src/vibetuner/testing.py b/vibetuner-py/src/vibetuner/testing.py index 2fd798d4..e89a7eaf 100644 --- a/vibetuner-py/src/vibetuner/testing.py +++ b/vibetuner-py/src/vibetuner/testing.py @@ -1,5 +1,6 @@ # ABOUTME: Pytest fixtures and testing utilities for vibetuner applications. # ABOUTME: Provides test client, mock auth, mock DB, mock tasks, and config overrides. +import os import uuid from typing import Any, AsyncGenerator from unittest.mock import AsyncMock, patch @@ -8,6 +9,8 @@ import pytest_asyncio from fastapi import FastAPI from httpx import ASGITransport, AsyncClient +from pymongo import AsyncMongoClient +from pymongo.asynchronous.database import AsyncDatabase from starlette.authentication import AuthCredentials from vibetuner.frontend.oauth import WebUser @@ -52,43 +55,117 @@ async def vibetuner_client( # --------------------------------------------------------------------------- -@pytest_asyncio.fixture -async def vibetuner_db() -> AsyncGenerator[str, None]: - """Temporary MongoDB test database with Beanie initialised. +async def _truncate_collections(database: AsyncDatabase) -> None: + """Delete every document from every non-system collection.""" + for name in await database.list_collection_names(): + if name.startswith("system."): + continue + await database[name].delete_many({}) + + +@pytest_asyncio.fixture(scope="session", loop_scope="session") +async def _vibetuner_db_session() -> AsyncGenerator[str, None]: + """Session-scoped MongoDB test database with indexes built once. + + Creates a uniquely-named database (namespaced per ``pytest-xdist`` + worker so parallel runs don't collide), runs ``init_beanie`` with + full index registration once for the entire session, yields the DB + name, and drops the database on session teardown. - Creates a uniquely-named database, initialises Beanie with all - registered models, yields the DB name, and drops the database on - teardown. Skips the test if ``MONGODB_URL`` is not set. + The ``AsyncMongoClient`` used here lives only on the session event + loop; per-test fixtures create their own function-loop clients and + re-wire Beanie via ``init_beanie(skip_indexes=True)``. Sharing the + client across loops is impossible (pymongo binds it on first use), + so the only thing shared across tests is the database itself + (collections + indexes living server-side). + + Skips the session if ``MONGODB_URL`` is not set. """ - import vibetuner.mongo as mongo_mod - from vibetuner.config import settings - from vibetuner.mongo import _ensure_client, get_all_models, teardown_mongodb + from beanie import init_beanie - test_db_name = f"test_{uuid.uuid4().hex[:12]}" + from vibetuner.config import settings + from vibetuner.mongo import get_all_models - _ensure_client() - if mongo_mod.mongo_client is None: + if settings.mongodb_url is None: pytest.skip("MongoDB not configured (MONGODB_URL not set)") - # Override the DB name used by all framework code + worker_id = os.environ.get("PYTEST_XDIST_WORKER", "main") + test_db_name = f"test_{worker_id}_{uuid.uuid4().hex[:8]}" + + # Make every consumer of ``settings.mongo_dbname`` (framework code, + # user code) target the session test database for the whole run. original = type(settings).mongo_dbname type(settings).mongo_dbname = property(lambda self: test_db_name) # type: ignore[assignment] settings.__dict__.pop("mongo_dbname", None) + session_client: AsyncMongoClient = AsyncMongoClient( + host=str(settings.mongodb_url), + compressors=["zstd"], + ) try: - from beanie import init_beanie - await init_beanie( - database=mongo_mod.mongo_client[test_db_name], + database=session_client[test_db_name], document_models=get_all_models(), ) yield test_db_name finally: - if mongo_mod.mongo_client is not None: - await mongo_mod.mongo_client.drop_database(test_db_name) - await teardown_mongodb() - type(settings).mongo_dbname = original # type: ignore[assignment] - settings.__dict__.pop("mongo_dbname", None) + try: + await session_client.drop_database(test_db_name) + finally: + await session_client.close() + type(settings).mongo_dbname = original # type: ignore[assignment] + settings.__dict__.pop("mongo_dbname", None) + + +@pytest_asyncio.fixture +async def vibetuner_db(_vibetuner_db_session: str) -> AsyncGenerator[str, None]: + """MongoDB test database wired to the current event loop with clean + collections. + + Backed by a session-scoped database whose indexes are built once + (see ``_vibetuner_db_session``). Each test creates its own + ``AsyncMongoClient`` on the function event loop, wires Beanie to + that client with ``skip_indexes=True`` (the indexes already exist + on the shared DB), and truncates every non-system collection both + before and after the test. Truncating twice makes the fixture + self-healing if a previous test crashed before its own teardown + ran. + + Skips the test if ``MONGODB_URL`` is not set. + + Caveats: + - All tests in a session share the same database. Tests must not + assert on database-level state (existence, name, full collection + drops) or on indexes being absent. + - Concurrent runs need ``pytest-xdist``; the session DB name + includes the worker id so each worker is isolated. + """ + from beanie import init_beanie + + import vibetuner.mongo as mongo_mod + from vibetuner.mongo import _ensure_client, get_all_models, teardown_mongodb + + test_db_name = _vibetuner_db_session + + _ensure_client() + if mongo_mod.mongo_client is None: + pytest.skip("MongoDB not configured (MONGODB_URL not set)") + + database = mongo_mod.mongo_client[test_db_name] + await init_beanie( + database=database, + document_models=get_all_models(), + skip_indexes=True, + ) + await _truncate_collections(database) + + try: + yield test_db_name + finally: + try: + await _truncate_collections(database) + finally: + await teardown_mongodb() # --------------------------------------------------------------------------- diff --git a/vibetuner-py/tests/integration/__init__.py b/vibetuner-py/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vibetuner-py/tests/integration/test_vibetuner_db_real.py b/vibetuner-py/tests/integration/test_vibetuner_db_real.py new file mode 100644 index 00000000..fcdb0e8f --- /dev/null +++ b/vibetuner-py/tests/integration/test_vibetuner_db_real.py @@ -0,0 +1,84 @@ +# ABOUTME: Integration test for vibetuner_db against a Dockerised MongoDB. +# ABOUTME: Verifies cross-test isolation, session DB reuse, and index persistence. +# ruff: noqa: S101 +import os + +import pytest +import vibetuner.mongo as mongo_mod +from beanie import Document +from pydantic import Field +from pymongo import IndexModel +from testcontainers.mongodb import MongoDbContainer +from vibetuner.config import settings +from vibetuner.mongo import get_all_models + + +class FixtureProbe(Document): + """Test-only document used to exercise the vibetuner_db fixture.""" + + label: str = Field(...) + + class Settings: + name = "fixture_probe" + indexes = [IndexModel("label", unique=True, name="fixture_probe_label_unique")] + + +_original_get_all_models = get_all_models + + +def _patched_get_all_models() -> list[type]: + base = _original_get_all_models() + if FixtureProbe not in base: + base.append(FixtureProbe) + return base + + +@pytest.fixture(scope="session", autouse=True) +def mongo_container(): + """Spin up a MongoDB container for the test session.""" + from pydantic import MongoDsn + + with MongoDbContainer("mongo:7") as container: + url = container.get_connection_url() + original_url = settings.mongodb_url + settings.mongodb_url = MongoDsn(url) + original_env = os.environ.get("MONGODB_URL") + os.environ["MONGODB_URL"] = url + # Patch model registry to include our probe. + mongo_mod.get_all_models = _patched_get_all_models + try: + yield container + finally: + mongo_mod.get_all_models = _original_get_all_models + settings.mongodb_url = original_url + if original_env is None: + os.environ.pop("MONGODB_URL", None) + else: + os.environ["MONGODB_URL"] = original_env + + +@pytest.mark.integration +class TestVibetunerDbIsolation: + async def test_first_test_inserts_a_row(self, vibetuner_db): + await FixtureProbe(label="from-test-1").insert() + assert ( + await FixtureProbe.find_one(FixtureProbe.label == "from-test-1") is not None + ) + + async def test_second_test_sees_clean_state(self, vibetuner_db): + # If the per-test cleanup works, the row from test 1 is gone. + assert await FixtureProbe.find_one(FixtureProbe.label == "from-test-1") is None + await FixtureProbe(label="from-test-2").insert() + assert ( + await FixtureProbe.find_one(FixtureProbe.label == "from-test-2") is not None + ) + + async def test_uniqueness_index_still_enforced(self, vibetuner_db): + # The unique index on ``label`` is built once at session scope and + # persists across per-test cleanups, even though per-test + # init_beanie runs with skip_indexes=True. + from pymongo.errors import DuplicateKeyError + + await FixtureProbe(label="dup").insert() + with pytest.raises(DuplicateKeyError): + await FixtureProbe(label="dup").insert() diff --git a/vibetuner-py/tests/unit/test_vibetuner_db_fixture.py b/vibetuner-py/tests/unit/test_vibetuner_db_fixture.py index bdc29d64..7b159396 100644 --- a/vibetuner-py/tests/unit/test_vibetuner_db_fixture.py +++ b/vibetuner-py/tests/unit/test_vibetuner_db_fixture.py @@ -1,5 +1,5 @@ -# ABOUTME: Tests that the vibetuner_db fixture resets the MongoDB client on teardown. -# ABOUTME: Prevents AsyncMongoClient from leaking across event loops in test runs. +# ABOUTME: Tests that the per-test vibetuner_db fixture wires Beanie with +# ABOUTME: skip_indexes, truncates collections, and resets the client on teardown. # ruff: noqa: S101 from unittest.mock import AsyncMock, MagicMock, patch @@ -7,59 +7,69 @@ import vibetuner.mongo as mongo_mod -async def _run_fixture_lifecycle(): - """Run the vibetuner_db generator logic directly (bypassing pytest decorator). +async def _run_function_fixture(session_db: str) -> str: + """Drive the per-test ``vibetuner_db`` body directly. - Duplicates the fixture body so the test doesn't depend on pytest fixture - internals, while still exercising the exact same teardown path. + Bypasses the pytest fixture decorator so the test exercises the exact + setup/teardown path without needing a session-scoped fixture in scope. """ - # Import the same things the fixture does from vibetuner.testing import vibetuner_db - # Access the raw coroutine under the pytest_asyncio wrapper - gen = vibetuner_db.__wrapped__() + gen = vibetuner_db.__wrapped__(session_db) db_name = await gen.__anext__() - - # Simulate teardown with pytest.raises(StopAsyncIteration): await gen.__anext__() - return db_name @pytest.mark.unit class TestVibetunerDbFixtureCleanup: - """The vibetuner_db fixture must reset mongo_client on teardown. + """The per-test ``vibetuner_db`` fixture must: - When pytest-asyncio uses function-scoped event loops (the default), - each test gets a new loop. AsyncMongoClient binds to the loop it was - created on, so a stale client causes RuntimeError in the next test. - The fixture must close and reset the client during teardown. + - Reuse the session DB name (no per-test DB creation). + - Wire Beanie with ``skip_indexes=True`` (indexes live on the shared DB). + - Truncate non-system collections before AND after the test. + - Reset ``mongo_client`` on teardown so the next test's event loop gets + a fresh ``AsyncMongoClient``. """ - async def test_mongo_client_is_none_after_teardown(self): - """After vibetuner_db yields and tears down, mongo_client must be None.""" + async def test_function_fixture_truncates_and_resets_client(self): + mock_collection = MagicMock() + mock_collection.delete_many = AsyncMock() + + mock_database = MagicMock() + mock_database.list_collection_names = AsyncMock( + return_value=["users", "posts", "system.indexes"] + ) + mock_database.__getitem__ = MagicMock(return_value=mock_collection) + mock_client = MagicMock() - mock_client.__getitem__ = MagicMock(return_value=MagicMock()) - mock_client.drop_database = AsyncMock() + mock_client.__getitem__ = MagicMock(return_value=mock_database) mock_client.close = AsyncMock() + init_beanie_mock = AsyncMock() + with ( patch.object(mongo_mod, "mongo_client", None), patch( "vibetuner.mongo._ensure_client", side_effect=lambda: setattr(mongo_mod, "mongo_client", mock_client), ), - patch("vibetuner.config.settings") as mock_settings, - patch("beanie.init_beanie", new_callable=AsyncMock), + patch("beanie.init_beanie", init_beanie_mock), patch("vibetuner.mongo.get_all_models", return_value=[]), ): - mock_settings.mongodb_url = "mongodb://localhost:27017" - type(mock_settings).mongo_dbname = property(lambda self: "original_db") + db_name = await _run_function_fixture("test_session_abcd1234") + + assert db_name == "test_session_abcd1234" + + init_beanie_mock.assert_awaited_once() + assert init_beanie_mock.await_args.kwargs["skip_indexes"] is True - db_name = await _run_fixture_lifecycle() - assert db_name.startswith("test_") + # Truncated twice (setup + teardown), skipping system.* collections. + assert mock_database.list_collection_names.await_count == 2 + assert ( + mock_collection.delete_many.await_count == 4 + ) # 2 collections x 2 passes - # The client must have been closed and reset mock_client.close.assert_awaited_once() assert mongo_mod.mongo_client is None diff --git a/vibetuner-py/uv.lock b/vibetuner-py/uv.lock index 13d5e981..03cd72e6 100644 --- a/vibetuner-py/uv.lock +++ b/vibetuner-py/uv.lock @@ -838,6 +838,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 = "dunamai" version = "1.26.1" @@ -2816,6 +2830,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/3c/df6641d7e2e84a6dd4de3b3a4426db7f6a7270c05bbdeadd523645c9c45f/taplo-0.9.3-py3-none-win_amd64.whl", hash = "sha256:7d80b630b93fb43cee99d1e1ee07b616236dc5615efaf7cd51074b4cffc33bab", size = 3985843, upload-time = "2024-08-19T10:22:13.446Z" }, ] +[[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.optional-dependencies] +mongodb = [ + { name = "pymongo" }, +] + [[package]] name = "tqdm" version = "4.67.3" @@ -3094,6 +3129,7 @@ dev = [ { name = "rumdl" }, { name = "semver" }, { name = "taplo" }, + { name = "testcontainers", extra = ["mongodb"] }, { name = "ty" }, { name = "types-aioboto3", extra = ["s3"] }, { name = "types-authlib" }, @@ -3143,6 +3179,7 @@ requires-dist = [ { name = "starlette-htmx", specifier = ">=0.1.1" }, { name = "streaq", extras = ["web"], specifier = ">=6.4.0,<7.0.0" }, { name = "taplo", marker = "extra == 'dev'", specifier = ">=0.9.3" }, + { name = "testcontainers", extras = ["mongodb"], marker = "extra == 'dev'", specifier = ">=4.9.0" }, { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.32" }, { name = "typer", specifier = ">=0.24.1" }, { name = "types-aioboto3", extras = ["s3"], marker = "extra == 'dev'", specifier = ">=15.5.0" },