Skip to content

Commit 0768490

Browse files
Tranquility2alexanderankin
andauthoredJun 18, 2024··
feat(core): Added ServerContainer (#595)
As part of the effort described, detailed and presented on #559 This is the seconds PR (out of 4) that should provide all the groundwork to support containers running a server. This would allow users to use custom images: ```python with DockerImage(path=".", tag="test:latest") as image: with ServerContainer(port=9000, image=image) as srv: # Test something with/on the server using port 9000 ``` Next in line are: `feat(core): Added FastAPI module` `feat(core): Added AWS Lambda module` --- Based on the work done on #585 Expended from issue #83 --------- Co-authored-by: David Ankin <daveankin@gmail.com>
1 parent 59cb6fc commit 0768490

File tree

11 files changed

+178
-56
lines changed

11 files changed

+178
-56
lines changed
 

‎Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ ${TESTS_DIND} : %/tests-dind : image
6363
docs :
6464
poetry run sphinx-build -nW . docs/_build
6565

66+
# Target to build docs watching for changes as per https://stackoverflow.com/a/21389615
67+
docs-watch :
68+
poetry run sphinx-autobuild . docs/_build # requires 'pip install sphinx-autobuild'
69+
6670
doctests : ${DOCTESTS}
6771
poetry run sphinx-build -b doctest . docs/_build
6872

‎core/README.rst

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
testcontainers-core
1+
Testcontainers Core
22
===================
33

44
:code:`testcontainers-core` is the core functionality for spinning up Docker containers in test environments.
55

66
.. autoclass:: testcontainers.core.container.DockerContainer
77

8-
.. autoclass:: testcontainers.core.image.DockerImage
9-
108
Using `DockerContainer` and `DockerImage` directly:
119

1210
.. doctest::
@@ -18,3 +16,15 @@ Using `DockerContainer` and `DockerImage` directly:
1816
>>> with DockerImage(path="./core/tests/image_fixtures/sample/", tag="test-sample:latest") as image:
1917
... with DockerContainer(str(image)) as container:
2018
... delay = wait_for_logs(container, "Test Sample Image")
19+
20+
---
21+
22+
.. autoclass:: testcontainers.core.image.DockerImage
23+
24+
---
25+
26+
.. autoclass:: testcontainers.core.generic.ServerContainer
27+
28+
---
29+
30+
.. autoclass:: testcontainers.core.generic.DbContainer

‎core/testcontainers/core/config.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ class TestcontainersConfiguration:
6161

6262
@property
6363
def docker_auth_config(self):
64-
if "DOCKER_AUTH_CONFIG" in _WARNINGS:
64+
config = self._docker_auth_config
65+
if config and "DOCKER_AUTH_CONFIG" in _WARNINGS:
6566
warning(_WARNINGS.pop("DOCKER_AUTH_CONFIG"))
66-
return self._docker_auth_config
67+
return config
6768

6869
@docker_auth_config.setter
6970
def docker_auth_config(self, value: str):

‎core/testcontainers/core/generic.py

+72-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
13-
from typing import Optional
13+
from typing import Optional, Union
14+
from urllib.error import HTTPError
1415
from urllib.parse import quote
16+
from urllib.request import urlopen
1517

1618
from testcontainers.core.container import DockerContainer
1719
from testcontainers.core.exceptions import ContainerStartException
20+
from testcontainers.core.image import DockerImage
1821
from testcontainers.core.utils import raise_for_deprecated_parameter
1922
from testcontainers.core.waiting_utils import wait_container_is_ready
2023

@@ -29,6 +32,8 @@
2932

3033
class DbContainer(DockerContainer):
3134
"""
35+
**DEPRECATED (for removal)**
36+
3237
Generic database container.
3338
"""
3439

@@ -79,3 +84,69 @@ def _configure(self) -> None:
7984

8085
def _transfer_seed(self) -> None:
8186
pass
87+
88+
89+
class ServerContainer(DockerContainer):
90+
"""
91+
**DEPRECATED - will be moved from core to a module (stay tuned for a final/stable import location)**
92+
93+
Container for a generic server that is based on a custom image.
94+
95+
Example:
96+
97+
.. doctest::
98+
99+
>>> import httpx
100+
>>> from testcontainers.core.generic import ServerContainer
101+
>>> from testcontainers.core.waiting_utils import wait_for_logs
102+
>>> from testcontainers.core.image import DockerImage
103+
104+
>>> with DockerImage(path="./core/tests/image_fixtures/python_server", tag="test-srv:latest") as image:
105+
... with ServerContainer(port=9000, image=image) as srv:
106+
... url = srv._create_connection_url()
107+
... response = httpx.get(f"{url}", timeout=5)
108+
... assert response.status_code == 200, "Response status code is not 200"
109+
... delay = wait_for_logs(srv, "GET / HTTP/1.1")
110+
111+
112+
:param path: Path to the Dockerfile to build the image
113+
:param tag: Tag for the image to be built (default: None)
114+
"""
115+
116+
def __init__(self, port: int, image: Union[str, DockerImage]) -> None:
117+
super().__init__(str(image))
118+
self.internal_port = port
119+
self.with_exposed_ports(self.internal_port)
120+
121+
@wait_container_is_ready(HTTPError)
122+
def _connect(self) -> None:
123+
# noinspection HttpUrlsUsage
124+
url = self._create_connection_url()
125+
try:
126+
with urlopen(url) as r:
127+
assert b"" in r.read()
128+
except HTTPError as e:
129+
# 404 is expected, as the server may not have the specific endpoint we are looking for
130+
if e.code == 404:
131+
pass
132+
else:
133+
raise
134+
135+
def get_api_url(self) -> str:
136+
raise NotImplementedError
137+
138+
def _create_connection_url(self) -> str:
139+
if self._container is None:
140+
raise ContainerStartException("container has not been started")
141+
host = self.get_container_host_ip()
142+
exposed_port = self.get_exposed_port(self.internal_port)
143+
url = f"http://{host}:{exposed_port}"
144+
return url
145+
146+
def start(self) -> "ServerContainer":
147+
super().start()
148+
self._connect()
149+
return self
150+
151+
def stop(self, force=True, delete_volume=True) -> None:
152+
super().stop(force, delete_volume)

‎core/testcontainers/core/image.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import TYPE_CHECKING, Optional
1+
from os import PathLike
2+
from typing import TYPE_CHECKING, Optional, Union
23

34
from typing_extensions import Self
45

@@ -28,23 +29,24 @@ class DockerImage:
2829

2930
def __init__(
3031
self,
31-
path: str,
32+
path: Union[str, PathLike],
3233
docker_client_kw: Optional[dict] = None,
3334
tag: Optional[str] = None,
3435
clean_up: bool = True,
3536
**kwargs,
3637
) -> None:
3738
self.tag = tag
3839
self.path = path
39-
self.id = None
4040
self._docker = DockerClient(**(docker_client_kw or {}))
4141
self.clean_up = clean_up
4242
self._kwargs = kwargs
43+
self._image = None
44+
self._logs = None
4345

4446
def build(self, **kwargs) -> Self:
4547
logger.info(f"Building image from {self.path}")
4648
docker_client = self.get_docker_client()
47-
self._image, self._logs = docker_client.build(path=self.path, tag=self.tag, **kwargs)
49+
self._image, self._logs = docker_client.build(path=str(self.path), tag=self.tag, **kwargs)
4850
logger.info(f"Built image {self.short_id} with tag {self.tag}")
4951
return self
5052

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM python:3-alpine
2+
EXPOSE 9000
3+
CMD ["python", "-m", "http.server", "9000"]

‎core/tests/test_generics.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import re
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
import pytest
6+
from httpx import get
7+
8+
from testcontainers.core.waiting_utils import wait_for_logs
9+
from testcontainers.core.image import DockerImage
10+
from testcontainers.core.generic import ServerContainer
11+
12+
TEST_DIR = Path(__file__).parent
13+
14+
15+
@pytest.mark.parametrize("test_image_cleanup", [True, False])
16+
@pytest.mark.parametrize("test_image_tag", [None, "custom-image:test"])
17+
def test_srv_container(test_image_tag: Optional[str], test_image_cleanup: bool, check_for_image, port=9000):
18+
with (
19+
DockerImage(
20+
path=TEST_DIR / "image_fixtures/python_server",
21+
tag=test_image_tag,
22+
clean_up=test_image_cleanup,
23+
#
24+
) as docker_image,
25+
ServerContainer(port=port, image=docker_image) as srv,
26+
):
27+
image_short_id = docker_image.short_id
28+
image_build_logs = docker_image.get_logs()
29+
# check if dict is in any of the logs
30+
assert {"stream": f"Step 2/3 : EXPOSE {port}"} in image_build_logs, "Image logs mismatch"
31+
assert (port, None) in srv.ports.items(), "Port mismatch"
32+
with pytest.raises(NotImplementedError):
33+
srv.get_api_url()
34+
test_url = srv._create_connection_url()
35+
assert re.match(r"http://localhost:\d+", test_url), "Connection URL mismatch"
36+
37+
check_for_image(image_short_id, test_image_cleanup)
38+
39+
40+
def test_like_doctest():
41+
with DockerImage(path=TEST_DIR / "image_fixtures/python_server", tag="test-srv:latest") as image:
42+
with ServerContainer(port=9000, image=image) as srv:
43+
url = srv._create_connection_url()
44+
response = get(f"{url}", timeout=5)
45+
assert response.status_code == 200, "Response status code is not 200"
46+
delay = wait_for_logs(srv, "GET / HTTP/1.1")
47+
print(delay)

‎index.rst

+5-33
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,10 @@ testcontainers-python
1313
testcontainers-python facilitates the use of Docker containers for functional and integration testing. The collection of packages currently supports the following features.
1414

1515
.. toctree::
16+
:maxdepth: 1
1617

1718
core/README
18-
modules/arangodb/README
19-
modules/azurite/README
20-
modules/cassandra/README
21-
modules/chroma/README
22-
modules/clickhouse/README
23-
modules/elasticsearch/README
24-
modules/google/README
25-
modules/influxdb/README
26-
modules/k3s/README
27-
modules/kafka/README
28-
modules/keycloak/README
29-
modules/localstack/README
30-
modules/memcached/README
31-
modules/milvus/README
32-
modules/minio/README
33-
modules/mongodb/README
34-
modules/mqtt/README
35-
modules/mssql/README
36-
modules/mysql/README
37-
modules/nats/README
38-
modules/neo4j/README
39-
modules/nginx/README
40-
modules/opensearch/README
41-
modules/oracle-free/README
42-
modules/postgres/README
43-
modules/qdrant/README
44-
modules/rabbitmq/README
45-
modules/redis/README
46-
modules/registry/README
47-
modules/selenium/README
48-
modules/vault/README
49-
modules/weaviate/README
19+
modules/index
5020

5121
Getting Started
5222
---------------
@@ -190,4 +160,6 @@ Testcontainers is a collection of `implicit namespace packages <https://peps.pyt
190160
Contributing a New Feature
191161
^^^^^^^^^^^^^^^^^^^^^^^^^^
192162

193-
You want to contribute a new feature or container? Great! You can do that in six steps as outlined `here <https://github.com/testcontainers/testcontainers-python/blob/main/.github/PULL_REQUEST_TEMPLATE/new_container.md>__`.
163+
You want to contribute a new feature or container?
164+
Great! You can do that in six steps as outlined
165+
`here <https://github.com/testcontainers/testcontainers-python/blob/main/.github/PULL_REQUEST_TEMPLATE/new_container.md>`_.

‎modules/index.rst

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Community Modules
2+
=================
3+
4+
..
5+
glob:
6+
https://stackoverflow.com/a/44572883/4971476
7+
8+
.. toctree::
9+
:glob:
10+
11+
*/README

‎poetry.lock

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml

+10-9
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,20 @@ mypy = "1.7.1"
142142
pre-commit = "^3.6"
143143
pytest = "7.4.3"
144144
pytest-cov = "4.1.0"
145-
sphinx = "^7.2.6"
146-
twine = "^4.0.2"
147-
anyio = "^4.3.0"
145+
sphinx = "7.2.6"
146+
twine = "4.0.2"
147+
anyio = "4.3.0"
148148
# for tests only
149-
psycopg2-binary = "*"
150-
pg8000 = "*"
151-
sqlalchemy = "*"
152-
psycopg = "*"
153-
cassandra-driver = "*"
149+
psycopg2-binary = "2.9.9"
150+
pg8000 = "1.30.5"
151+
sqlalchemy = "2.0.28"
152+
psycopg = "3.1.18"
153+
cassandra-driver = "3.29.1"
154154
pytest-asyncio = "0.23.5"
155155
kafka-python-ng = "^2.2.0"
156-
hvac = "*"
156+
hvac = "2.1.0"
157157
pymilvus = "2.4.3"
158+
httpx = "0.27.0"
158159
paho-mqtt = "2.1.0"
159160

160161
[[tool.poetry.source]]

0 commit comments

Comments
 (0)
Please sign in to comment.