|
1 | 1 | import sys |
2 | | -from dataclasses import asdict, dataclass, field, fields, is_dataclass |
| 2 | +from dataclasses import asdict, dataclass, field |
3 | 3 | from functools import cached_property |
4 | 4 | from json import loads |
5 | 5 | from logging import getLogger, warning |
|
11 | 11 | from types import TracebackType |
12 | 12 | from typing import Any, Callable, Literal, Optional, TypeVar, Union, cast |
13 | 13 |
|
14 | | -from testcontainers.core.docker_client import get_docker_host_hostname |
| 14 | +from testcontainers.core.docker_client import DockerClient, get_docker_host_hostname |
15 | 15 | from testcontainers.core.exceptions import ContainerIsNotRunning, NoSuchPortExposed |
| 16 | +from testcontainers.core.inspect import ContainerInspectInfo, _ignore_properties |
16 | 17 | from testcontainers.core.waiting_utils import WaitStrategy |
17 | 18 |
|
18 | | -_IPT = TypeVar("_IPT") |
19 | 19 | _WARNINGS = {"DOCKER_COMPOSE_GET_CONFIG": "get_config is experimental, see testcontainers/testcontainers-python#669"} |
20 | 20 |
|
21 | 21 | logger = getLogger(__name__) |
22 | 22 |
|
23 | 23 |
|
24 | | -def _ignore_properties(cls: type[_IPT], dict_: Any) -> _IPT: |
25 | | - """omits extra fields like @JsonIgnoreProperties(ignoreUnknown = true) |
26 | | -
|
27 | | - https://gist.github.com/alexanderankin/2a4549ac03554a31bef6eaaf2eaf7fd5""" |
28 | | - if isinstance(dict_, cls): |
29 | | - return dict_ |
30 | | - if not is_dataclass(cls): |
31 | | - raise TypeError(f"Expected a dataclass type, got {cls}") |
32 | | - class_fields = {f.name for f in fields(cls)} |
33 | | - filtered = {k: v for k, v in dict_.items() if k in class_fields} |
34 | | - return cls(**filtered) |
35 | | - |
36 | | - |
37 | 24 | @dataclass |
38 | 25 | class PublishedPortModel: |
39 | 26 | """ |
@@ -93,6 +80,7 @@ class ComposeContainer: |
93 | 80 | ExitCode: Optional[int] = None |
94 | 81 | Publishers: list[PublishedPortModel] = field(default_factory=list) |
95 | 82 | _docker_compose: Optional["DockerCompose"] = field(default=None, init=False, repr=False) |
| 83 | + _cached_container_info: Optional[ContainerInspectInfo] = field(default=None, init=False, repr=False) |
96 | 84 |
|
97 | 85 | def __post_init__(self) -> None: |
98 | 86 | if self.Publishers: |
@@ -159,6 +147,28 @@ def reload(self) -> None: |
159 | 147 | # each time through get_container(), but we need this method for compatibility |
160 | 148 | pass |
161 | 149 |
|
| 150 | + def get_container_info(self) -> Optional[ContainerInspectInfo]: |
| 151 | + """Get container information via docker inspect (lazy loaded). |
| 152 | +
|
| 153 | + Returns: |
| 154 | + Container inspect information or None if container is not started. |
| 155 | + """ |
| 156 | + if self._cached_container_info is not None: |
| 157 | + return self._cached_container_info |
| 158 | + |
| 159 | + if not self._docker_compose or not self.ID: |
| 160 | + return None |
| 161 | + |
| 162 | + try: |
| 163 | + docker_client = self._docker_compose._get_docker_client() |
| 164 | + self._cached_container_info = docker_client.get_container_inspect_info(self.ID) |
| 165 | + |
| 166 | + except Exception as e: |
| 167 | + logger.warning(f"Failed to get container info for {self.ID}: {e}") |
| 168 | + self._cached_container_info = None |
| 169 | + |
| 170 | + return self._cached_container_info |
| 171 | + |
162 | 172 | @property |
163 | 173 | def status(self) -> str: |
164 | 174 | """Get container status for compatibility with wait strategies.""" |
@@ -233,6 +243,7 @@ class DockerCompose: |
233 | 243 | quiet_pull: bool = False |
234 | 244 | quiet_build: bool = False |
235 | 245 | _wait_strategies: Optional[dict[str, Any]] = field(default=None, init=False, repr=False) |
| 246 | + _docker_client: Optional[DockerClient] = field(default=None, init=False, repr=False) |
236 | 247 |
|
237 | 248 | def __post_init__(self) -> None: |
238 | 249 | if isinstance(self.compose_file_name, str): |
@@ -597,3 +608,9 @@ def wait_for(self, url: str) -> "DockerCompose": |
597 | 608 | with urlopen(url) as response: |
598 | 609 | response.read() |
599 | 610 | return self |
| 611 | + |
| 612 | + def _get_docker_client(self) -> DockerClient: |
| 613 | + """Get Docker client instance.""" |
| 614 | + if self._docker_client is None: |
| 615 | + self._docker_client = DockerClient() |
| 616 | + return self._docker_client |
0 commit comments