pytest-testcontainers-django 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ """Bridge between ``pytest-testcontainers`` and ``pytest-django``.
2
+
3
+ The pytest plugin auto-loads via the ``pytest11`` entry point declared
4
+ in ``pyproject.toml`` — no ``-p`` flag or ``conftest.py`` import is
5
+ required. Most users only need to interact with the public surface
6
+ re-exported here.
7
+
8
+ See ``SPEC.md`` for the full design rationale (especially §6 on the
9
+ hook-ordering timing dance).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pytest_testcontainers_django._types import (
15
+ DjangoContainerConfig,
16
+ PostgresService,
17
+ RedisService,
18
+ )
19
+ from pytest_testcontainers_django.config import register
20
+ from pytest_testcontainers_django.errors import (
21
+ BaselineMissingError,
22
+ ConfigError,
23
+ PytestTestcontainersDjangoError,
24
+ ReuseStaleContainerError,
25
+ )
26
+
27
+ __all__ = [
28
+ "BaselineMissingError",
29
+ "ConfigError",
30
+ "DjangoContainerConfig",
31
+ "PostgresService",
32
+ "PytestTestcontainersDjangoError",
33
+ "RedisService",
34
+ "ReuseStaleContainerError",
35
+ "register",
36
+ ]
37
+
38
+ from importlib.metadata import PackageNotFoundError
39
+ from importlib.metadata import version as _pkg_version
40
+
41
+ try:
42
+ __version__ = _pkg_version("pytest-testcontainers-django")
43
+ except PackageNotFoundError: # pragma: no cover — only when running from a non-installed checkout
44
+ __version__ = "0.0.0+unknown"
45
+ del PackageNotFoundError, _pkg_version
@@ -0,0 +1,63 @@
1
+ """Public dataclasses used for programmatic configuration via :func:`register`.
2
+
3
+ See SPEC §4.2 / §5. Default env-var names follow the ``DJANGO_DB_*`` /
4
+ ``DJANGO_REDIS_*`` prefix idiom (SPEC §5.1.3).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ DEFAULT_DB_ENV_NAMES: dict[str, str] = {
13
+ "host": "DJANGO_DB_HOST",
14
+ "port": "DJANGO_DB_PORT",
15
+ "name": "DJANGO_DB_NAME",
16
+ "user": "DJANGO_DB_USER",
17
+ "password": "DJANGO_DB_PASSWORD",
18
+ "test_template": "DJANGO_DB_TEST_TEMPLATE",
19
+ }
20
+
21
+ DEFAULT_REDIS_ENV_NAMES: dict[str, str] = {
22
+ "host": "DJANGO_REDIS_HOST",
23
+ "port": "DJANGO_REDIS_PORT",
24
+ }
25
+
26
+
27
+ @dataclass
28
+ class PostgresService:
29
+ image: str = "postgres:16"
30
+ user: str = "postgres"
31
+ password: str = "postgres"
32
+ database: str = "postgres"
33
+ internal_port: int = 5432
34
+ template: str | None = None
35
+ init_scripts: list[Path] = field(default_factory=list)
36
+ env: dict[str, str] = field(default_factory=dict)
37
+ env_names: dict[str, str] = field(default_factory=lambda: dict(DEFAULT_DB_ENV_NAMES))
38
+
39
+
40
+ @dataclass
41
+ class RedisService:
42
+ image: str = "redis:7-alpine"
43
+ internal_port: int = 6379
44
+ env_names: dict[str, str] = field(default_factory=lambda: dict(DEFAULT_REDIS_ENV_NAMES))
45
+
46
+
47
+ @dataclass
48
+ class DjangoContainerConfig:
49
+ postgres: PostgresService = field(default_factory=PostgresService)
50
+ redis: RedisService | None = None
51
+ skip_dotenv_env: str = "DJANGO_SKIP_DOTENV"
52
+ disable_env: str = "PYTEST_TESTCONTAINERS_DISABLE"
53
+ reuse_env: str = "PYTEST_TESTCONTAINERS_REUSE"
54
+ use_django_pg_baseline: bool = False
55
+
56
+
57
+ __all__ = [
58
+ "DEFAULT_DB_ENV_NAMES",
59
+ "DEFAULT_REDIS_ENV_NAMES",
60
+ "DjangoContainerConfig",
61
+ "PostgresService",
62
+ "RedisService",
63
+ ]
@@ -0,0 +1,232 @@
1
+ """Pyproject reader, ``register()`` API, validation.
2
+
3
+ Resolution order (SPEC §5.1):
4
+
5
+ 1. ``register()`` value if ``conftest.py`` called it.
6
+ 2. ``[tool.pytest-testcontainers-django]`` table in nearest pyproject.toml.
7
+ 3. Built-in defaults from :mod:`pytest_testcontainers_django._types`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import sys
14
+ from dataclasses import replace
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ if sys.version_info >= (3, 11):
19
+ import tomllib
20
+ else: # pragma: no cover — exercised on 3.10
21
+ import tomli as tomllib
22
+
23
+ from pytest_testcontainers_django._types import (
24
+ DEFAULT_DB_ENV_NAMES,
25
+ DEFAULT_REDIS_ENV_NAMES,
26
+ DjangoContainerConfig,
27
+ PostgresService,
28
+ RedisService,
29
+ )
30
+ from pytest_testcontainers_django.errors import ConfigError
31
+
32
+ _REGISTERED: DjangoContainerConfig | None = None
33
+
34
+
35
+ def register(config: DjangoContainerConfig) -> None:
36
+ """Register a programmatic configuration from ``conftest.py``.
37
+
38
+ Overrides any ``[tool.pytest-testcontainers-django]`` pyproject table.
39
+ Last call wins (mostly relevant in test scenarios).
40
+ """
41
+ global _REGISTERED
42
+ if not isinstance(config, DjangoContainerConfig):
43
+ raise TypeError(
44
+ f"register() requires a DjangoContainerConfig instance, got {type(config).__name__}"
45
+ )
46
+ _REGISTERED = config
47
+
48
+
49
+ def _reset_registered() -> None:
50
+ """Test-only helper. Not part of the public API."""
51
+ global _REGISTERED
52
+ _REGISTERED = None
53
+
54
+
55
+ def _find_pyproject(start: Path) -> Path | None:
56
+ """Walk up from *start* looking for the nearest ``pyproject.toml``."""
57
+ current = start.resolve()
58
+ for candidate in [current, *current.parents]:
59
+ pyproject = candidate / "pyproject.toml"
60
+ if pyproject.is_file():
61
+ return pyproject
62
+ return None
63
+
64
+
65
+ def _load_pyproject_table(pyproject: Path) -> dict[str, Any]:
66
+ with pyproject.open("rb") as fh:
67
+ data = tomllib.load(fh)
68
+ tool = data.get("tool", {})
69
+ return tool.get("pytest-testcontainers-django", {}) or {}
70
+
71
+
72
+ def _resolve_init_scripts(raw: list[str | Path] | None, project_root: Path) -> list[Path]:
73
+ if not raw:
74
+ return []
75
+ out: list[Path] = []
76
+ for entry in raw:
77
+ path = Path(entry)
78
+ if not path.is_absolute():
79
+ path = project_root / path
80
+ out.append(path)
81
+ return out
82
+
83
+
84
+ def _config_from_table(table: dict[str, Any], project_root: Path) -> DjangoContainerConfig:
85
+ """Translate a parsed ``[tool.pytest-testcontainers-django]`` table.
86
+
87
+ Unknown keys are ignored — TOML errors should be loud, but typos in
88
+ nested optional keys shouldn't break the plugin's bootstrap. (We do
89
+ raise on structurally impossible values, e.g. wrong types.)
90
+ """
91
+ db_env_names = dict(DEFAULT_DB_ENV_NAMES)
92
+ db_env_names_overrides = {
93
+ "host": table.get("db_host_env"),
94
+ "port": table.get("db_port_env"),
95
+ "name": table.get("db_name_env"),
96
+ "user": table.get("db_user_env"),
97
+ "password": table.get("db_password_env"),
98
+ "test_template": table.get("db_test_template_env"),
99
+ }
100
+ for key, value in db_env_names_overrides.items():
101
+ if value is not None:
102
+ db_env_names[key] = str(value)
103
+
104
+ postgres = PostgresService(
105
+ image=str(table.get("postgres_image", PostgresService.image)),
106
+ user=str(table.get("postgres_user", PostgresService.user)),
107
+ password=str(table.get("postgres_password", PostgresService.password)),
108
+ database=str(table.get("postgres_database", PostgresService.database)),
109
+ internal_port=int(table.get("postgres_internal_port", PostgresService.internal_port)),
110
+ template=(
111
+ str(table["postgres_template"]) if table.get("postgres_template") is not None else None
112
+ ),
113
+ init_scripts=_resolve_init_scripts(table.get("postgres_init_scripts"), project_root),
114
+ env=dict(table.get("postgres_env", {}) or {}),
115
+ env_names=db_env_names,
116
+ )
117
+
118
+ redis: RedisService | None = None
119
+ if table.get("redis_enabled", False):
120
+ redis_env_names = dict(DEFAULT_REDIS_ENV_NAMES)
121
+ if (override := table.get("redis_host_env")) is not None:
122
+ redis_env_names["host"] = str(override)
123
+ if (override := table.get("redis_port_env")) is not None:
124
+ redis_env_names["port"] = str(override)
125
+ redis = RedisService(
126
+ image=str(table.get("redis_image", RedisService.image)),
127
+ internal_port=int(table.get("redis_internal_port", RedisService.internal_port)),
128
+ env_names=redis_env_names,
129
+ )
130
+
131
+ return DjangoContainerConfig(
132
+ postgres=postgres,
133
+ redis=redis,
134
+ skip_dotenv_env=str(table.get("skip_dotenv_env", DjangoContainerConfig.skip_dotenv_env)),
135
+ disable_env=str(table.get("disable_env", DjangoContainerConfig.disable_env)),
136
+ reuse_env=str(table.get("reuse_env", DjangoContainerConfig.reuse_env)),
137
+ use_django_pg_baseline=bool(table.get("use_django_pg_baseline", False)),
138
+ )
139
+
140
+
141
+ def load_config(rootdir: Path) -> DjangoContainerConfig:
142
+ """Load configuration. ``register()`` overrides pyproject; defaults fill gaps."""
143
+ if _REGISTERED is not None:
144
+ # register() expresses an override of *defaults*, not of pyproject;
145
+ # but per SPEC §4.2 the documented behavior is "register() wins" —
146
+ # so the registered config replaces the table wholesale.
147
+ return _REGISTERED
148
+
149
+ pyproject = _find_pyproject(rootdir)
150
+ if pyproject is None:
151
+ return DjangoContainerConfig()
152
+ table = _load_pyproject_table(pyproject)
153
+ return _config_from_table(table, project_root=pyproject.parent)
154
+
155
+
156
+ def apply_template_default(config: DjangoContainerConfig) -> DjangoContainerConfig:
157
+ """SPEC §10.6: when init_scripts non-empty and template unset, default to ``database``.
158
+
159
+ Returns a (possibly) new config; never mutates the input.
160
+ """
161
+ pg = config.postgres
162
+ if pg.init_scripts and pg.template is None:
163
+ new_pg = replace(pg, template=pg.database)
164
+ return replace(config, postgres=new_pg)
165
+ return config
166
+
167
+
168
+ def validate(config: DjangoContainerConfig) -> None:
169
+ """SPEC §5.6 validation.
170
+
171
+ Raises :class:`ConfigError` when the configuration is structurally bad.
172
+ """
173
+ pg = config.postgres
174
+ required_db_keys = {"host", "port", "name", "user", "password"}
175
+ missing = [k for k in required_db_keys if not pg.env_names.get(k)]
176
+ if missing:
177
+ raise ConfigError(f"PostgresService.env_names is missing required keys: {sorted(missing)}")
178
+
179
+ for path in pg.init_scripts:
180
+ if not path.is_file():
181
+ raise ConfigError(f"postgres_init_scripts entry does not exist: {path}")
182
+
183
+ if pg.template is not None and not pg.init_scripts:
184
+ raise ConfigError(
185
+ "postgres_template is set but postgres_init_scripts is empty — "
186
+ "cloning an empty template DB just slows test creation. "
187
+ "Either add init scripts or remove the template setting."
188
+ )
189
+
190
+ if config.redis is not None:
191
+ redis_env = config.redis.env_names
192
+ for key in ("host", "port"):
193
+ if not redis_env.get(key):
194
+ raise ConfigError(f"RedisService.env_names is missing required key: {key!r}")
195
+
196
+ if pg.internal_port <= 0 or pg.internal_port > 65535:
197
+ raise ConfigError(f"postgres_internal_port out of range: {pg.internal_port}")
198
+ if config.redis is not None and (
199
+ config.redis.internal_port <= 0 or config.redis.internal_port > 65535
200
+ ):
201
+ raise ConfigError(f"redis_internal_port out of range: {config.redis.internal_port}")
202
+
203
+
204
+ def is_disabled(config: DjangoContainerConfig, args: list[str]) -> bool:
205
+ """Active by default. SPEC §6.5 step 5.
206
+
207
+ Disabled by ``--no-testcontainers`` CLI flag or by setting
208
+ ``config.disable_env`` to a truthy-falsy value (``1``, ``true``, ``yes``).
209
+ """
210
+ if "--no-testcontainers" in args:
211
+ return True
212
+ if not config.disable_env:
213
+ return False
214
+ raw = os.environ.get(config.disable_env, "").strip().lower()
215
+ return raw in {"1", "true", "yes", "on"}
216
+
217
+
218
+ def is_reuse_enabled(config: DjangoContainerConfig) -> bool:
219
+ if not config.reuse_env:
220
+ return False
221
+ raw = os.environ.get(config.reuse_env, "").strip().lower()
222
+ return raw in {"1", "true", "yes", "on"}
223
+
224
+
225
+ __all__ = [
226
+ "apply_template_default",
227
+ "is_disabled",
228
+ "is_reuse_enabled",
229
+ "load_config",
230
+ "register",
231
+ "validate",
232
+ ]
@@ -0,0 +1,252 @@
1
+ """Container start/stop bridge.
2
+
3
+ This is the only module that talks to ``testcontainers-python`` directly.
4
+ The Django plugin code calls :func:`start_postgres` / :func:`start_redis`
5
+ and gets back a small handle exposing ``host``, ``port``, ``stop()``.
6
+
7
+ Why not simply call ``pytest_testcontainers.make_postgres``? Two
8
+ practical reasons specific to the Django timing dance:
9
+
10
+ * We need ``with_volume_mapping`` calls **before** ``.start()`` to mount
11
+ init scripts into ``/docker-entrypoint-initdb.d/`` (SPEC §10). #1's
12
+ ``make_postgres`` doesn't expose pre-start mutation.
13
+ * We want to detect "we attached to a pre-existing container" so we can
14
+ emit the SPEC §10.7 reuse warning.
15
+
16
+ We *do* lean on #1 for: daemon ping (:class:`pytest_testcontainers.DockerNotRunningError`),
17
+ atexit registration helpers, Ryuk-disable choreography, and
18
+ named-container reuse lookup. Those are imported from #1's surface so
19
+ breakage is contained at one spot if #1 evolves.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from pytest_testcontainers import DockerNotRunningError
30
+ from pytest_testcontainers.containers import (
31
+ bind_to_running,
32
+ deregister_atexit_stop,
33
+ disable_ryuk_once,
34
+ find_existing_container,
35
+ register_atexit_stop,
36
+ restart_stopped_or_raise,
37
+ start_or_raise,
38
+ )
39
+ from pytest_testcontainers.reuse import reuse_name_for
40
+
41
+ from pytest_testcontainers_django.errors import ReuseStaleContainerError
42
+
43
+ logger = logging.getLogger("pytest_testcontainers_django")
44
+
45
+
46
+ @dataclass
47
+ class ContainerHandle:
48
+ """Minimal handle returned by :func:`start_postgres` / :func:`start_redis`."""
49
+
50
+ host: str
51
+ port: int
52
+ name: str | None
53
+ is_bound_to_existing: bool
54
+ _instance: Any # testcontainers DockerContainer; we only call .stop() on it
55
+ _owns_instance: bool # False when bound to a pre-existing reuse container
56
+
57
+ def stop(self) -> None:
58
+ if not self._owns_instance:
59
+ return
60
+ deregister_atexit_stop(self._instance)
61
+ try:
62
+ self._instance.stop()
63
+ except Exception:
64
+ logger.exception("cleanup: container.stop() failed")
65
+
66
+
67
+ def _check_daemon() -> None:
68
+ """Wrap #1's daemon ping so callers get a stable exception class."""
69
+ import docker
70
+ import docker.errors
71
+
72
+ try:
73
+ docker.from_env().ping()
74
+ except docker.errors.DockerException as exc:
75
+ raise DockerNotRunningError(str(exc)) from exc
76
+
77
+
78
+ def _start_or_attach(
79
+ container: Any,
80
+ *,
81
+ reuse_name: str | None,
82
+ ) -> tuple[Any, bool]:
83
+ """Start *container*, or attach to a pre-existing one when reuse is on.
84
+
85
+ Returns ``(instance, is_bound_to_existing)``. When bound to existing,
86
+ the caller MUST NOT register atexit stop and MUST NOT call ``stop()``.
87
+ """
88
+ if reuse_name is None:
89
+ return container, False
90
+
91
+ disable_ryuk_once()
92
+ existing = find_existing_container(reuse_name)
93
+ if existing is None:
94
+ container.with_name(reuse_name)
95
+ return container, False
96
+
97
+ container_cls = type(container)
98
+ status = getattr(existing, "status", "")
99
+ if status == "running":
100
+ return bind_to_running(container_cls, existing), True
101
+ if status in {"exited", "created", "paused"}:
102
+ restart_stopped_or_raise(existing, reuse_name)
103
+ return bind_to_running(container_cls, existing), True
104
+ # "dead" / "removing" / unknown: a fresh start would fail with a 409
105
+ # name conflict against the stale container, so surface an actionable
106
+ # error. The user has to remove the stale container manually because
107
+ # we won't take destructive action on shared docker state.
108
+ raise ReuseStaleContainerError(
109
+ f"reuse-mode container {reuse_name!r} exists but is in state {status!r} — "
110
+ f"it cannot be restarted and starting a fresh container would conflict on "
111
+ f"the name. Remove it manually:\n"
112
+ f" docker rm -f {reuse_name}"
113
+ )
114
+
115
+
116
+ def start_postgres(
117
+ *,
118
+ image: str,
119
+ user: str,
120
+ password: str,
121
+ database: str,
122
+ internal_port: int,
123
+ env: dict[str, str],
124
+ init_scripts: list[Path],
125
+ reuse_name: str | None,
126
+ ) -> ContainerHandle:
127
+ """Construct + start (or attach to) a PostgresContainer."""
128
+ from testcontainers.postgres import PostgresContainer
129
+
130
+ _check_daemon()
131
+
132
+ container = PostgresContainer(
133
+ image=image,
134
+ username=user,
135
+ password=password,
136
+ dbname=database,
137
+ port=internal_port,
138
+ driver=None,
139
+ )
140
+ for key, value in env.items():
141
+ container.with_env(key, value)
142
+ for index, script in enumerate(init_scripts):
143
+ # SPEC §10.2: NN-name.sql, ordered.
144
+ container.with_volume_mapping(
145
+ str(script),
146
+ f"/docker-entrypoint-initdb.d/{index:02d}-{script.name}",
147
+ "ro",
148
+ )
149
+
150
+ instance, bound = _start_or_attach(container, reuse_name=reuse_name)
151
+ if bound:
152
+ host, port = _read_existing_host_port(instance, internal_port)
153
+ return ContainerHandle(
154
+ host=host,
155
+ port=port,
156
+ name=reuse_name,
157
+ is_bound_to_existing=True,
158
+ _instance=instance,
159
+ _owns_instance=False,
160
+ )
161
+
162
+ start_or_raise(instance, image)
163
+ if reuse_name is None:
164
+ register_atexit_stop(instance)
165
+
166
+ host = instance.get_container_host_ip()
167
+ port = int(instance.get_exposed_port(internal_port))
168
+ return ContainerHandle(
169
+ host=_normalize_host(host),
170
+ port=port,
171
+ name=reuse_name,
172
+ is_bound_to_existing=False,
173
+ _instance=instance,
174
+ _owns_instance=reuse_name is None,
175
+ )
176
+
177
+
178
+ def start_redis(
179
+ *,
180
+ image: str,
181
+ internal_port: int,
182
+ reuse_name: str | None,
183
+ ) -> ContainerHandle:
184
+ """Construct + start (or attach to) a RedisContainer."""
185
+ from testcontainers.redis import RedisContainer
186
+
187
+ _check_daemon()
188
+
189
+ container = RedisContainer(image=image, port=internal_port)
190
+ instance, bound = _start_or_attach(container, reuse_name=reuse_name)
191
+ if bound:
192
+ host, port = _read_existing_host_port(instance, internal_port)
193
+ return ContainerHandle(
194
+ host=host,
195
+ port=port,
196
+ name=reuse_name,
197
+ is_bound_to_existing=True,
198
+ _instance=instance,
199
+ _owns_instance=False,
200
+ )
201
+
202
+ start_or_raise(instance, image)
203
+ if reuse_name is None:
204
+ register_atexit_stop(instance)
205
+
206
+ host = instance.get_container_host_ip()
207
+ port = int(instance.get_exposed_port(internal_port))
208
+ return ContainerHandle(
209
+ host=_normalize_host(host),
210
+ port=port,
211
+ name=reuse_name,
212
+ is_bound_to_existing=False,
213
+ _instance=instance,
214
+ _owns_instance=reuse_name is None,
215
+ )
216
+
217
+
218
+ def _normalize_host(host: str) -> str:
219
+ if host in ("", "0.0.0.0"):
220
+ return "localhost"
221
+ return host
222
+
223
+
224
+ def _read_existing_host_port(instance: Any, internal_port: int) -> tuple[str, int]:
225
+ """Read host/port from a docker-py container the testcontainers wrapper holds.
226
+
227
+ ``bind_to_running`` sets ``instance._container`` to the docker-py
228
+ Container object; that's where the port mapping lives.
229
+ """
230
+ docker_container = getattr(instance, "_container", None)
231
+ if docker_container is None: # pragma: no cover — defensive
232
+ host = instance.get_container_host_ip()
233
+ port = int(instance.get_exposed_port(internal_port))
234
+ return _normalize_host(host), port
235
+ ports = docker_container.attrs["NetworkSettings"]["Ports"]
236
+ binding = (ports.get(f"{internal_port}/tcp") or [{}])[0]
237
+ host = binding.get("HostIp", "localhost")
238
+ return _normalize_host(host), int(binding["HostPort"])
239
+
240
+
241
+ def reuse_name(service_slug: str) -> str:
242
+ """Compose a reuse-name for the eager-start path. Single-shared per controller."""
243
+ return reuse_name_for(service_slug)
244
+
245
+
246
+ __all__ = [
247
+ "ContainerHandle",
248
+ "DockerNotRunningError",
249
+ "reuse_name",
250
+ "start_postgres",
251
+ "start_redis",
252
+ ]
@@ -0,0 +1,31 @@
1
+ """Public exception classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class PytestTestcontainersDjangoError(Exception):
7
+ """Base class for all errors raised by this plugin."""
8
+
9
+
10
+ class ConfigError(PytestTestcontainersDjangoError):
11
+ """Raised when configuration is invalid (SPEC §5.6)."""
12
+
13
+
14
+ class BaselineMissingError(PytestTestcontainersDjangoError):
15
+ """Raised when ``use_django_pg_baseline = true`` but ``django-pg-baseline`` is unavailable."""
16
+
17
+
18
+ class ReuseStaleContainerError(PytestTestcontainersDjangoError):
19
+ """Raised when reuse mode finds a pre-existing container that can't be revived
20
+ (e.g. status ``dead``/``removing``). Starting a fresh container with the same
21
+ name would fail with a Docker name conflict, so we surface a clear actionable
22
+ error instead.
23
+ """
24
+
25
+
26
+ __all__ = [
27
+ "BaselineMissingError",
28
+ "ConfigError",
29
+ "PytestTestcontainersDjangoError",
30
+ "ReuseStaleContainerError",
31
+ ]