-
Notifications
You must be signed in to change notification settings - Fork 292
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): Added Generic module (#612)
As part of the effort described, detailed and presented on #559 This is the third PR (out of 4) that should provide all the groundwork to support containers running a server. As discussed on #595 this PR aims to refactor the `ServerContainer` under a new dedicated module called "generic". ![image](https://github.com/testcontainers/testcontainers-python/assets/7189138/b7a3395b-ce3c-40ef-8baa-dfa3eff1b056) The idea is that this module could include multiple generic implementations such as ```server.py``` with the proper documentation and examples to allow users simpler usage and QOL. This PR adds the original FastAPI implementation as a simple doc example, I think this aligns better following #595 Next in line is ```feat(core): Added AWS Lambda module``` Based on the work done on #585 and #595 Expended from issue #83 --- Please note an extra commit is included to simulate the relations when importing between and with other modules.
- Loading branch information
1 parent
3519f4b
commit e575b28
Showing
18 changed files
with
232 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
:code:`testcontainers-generic` is a set of generic containers modules that can be used to creat containers. | ||
|
||
.. autoclass:: testcontainers.generic.ServerContainer | ||
.. title:: testcontainers.generic.ServerContainer | ||
|
||
FastAPI container that is using :code:`ServerContainer` | ||
|
||
.. doctest:: | ||
|
||
>>> from testcontainers.generic import ServerContainer | ||
>>> from testcontainers.core.waiting_utils import wait_for_logs | ||
|
||
>>> with DockerImage(path="./modules/generic/tests/samples/fastapi", tag="fastapi-test:latest") as image: | ||
... with ServerContainer(port=80, image=image) as fastapi_server: | ||
... delay = wait_for_logs(fastapi_server, "Uvicorn running on http://0.0.0.0:80") | ||
... fastapi_server.get_api_url = lambda: fastapi_server._create_connection_url() + "/api/v1/" | ||
... client = fastapi_server.get_client() | ||
... response = client.get("/") | ||
... assert response.status_code == 200 | ||
... assert response.json() == {"Status": "Working"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .server import ServerContainer # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
from typing import Union | ||
from urllib.error import HTTPError | ||
from urllib.request import urlopen | ||
|
||
import httpx | ||
|
||
from testcontainers.core.container import DockerContainer | ||
from testcontainers.core.exceptions import ContainerStartException | ||
from testcontainers.core.image import DockerImage | ||
from testcontainers.core.waiting_utils import wait_container_is_ready | ||
|
||
|
||
class ServerContainer(DockerContainer): | ||
""" | ||
Container for a generic server that is based on a custom image. | ||
Example: | ||
.. doctest:: | ||
>>> import httpx | ||
>>> from testcontainers.generic import ServerContainer | ||
>>> from testcontainers.core.waiting_utils import wait_for_logs | ||
>>> from testcontainers.core.image import DockerImage | ||
>>> with DockerImage(path="./modules/generic/tests/samples/python_server", tag="test-srv:latest") as image: | ||
... with ServerContainer(port=9000, image=image) as srv: | ||
... url = srv._create_connection_url() | ||
... response = httpx.get(f"{url}", timeout=5) | ||
... assert response.status_code == 200, "Response status code is not 200" | ||
... delay = wait_for_logs(srv, "GET / HTTP/1.1") | ||
:param path: Path to the Dockerfile to build the image | ||
:param tag: Tag for the image to be built (default: None) | ||
""" | ||
|
||
def __init__(self, port: int, image: Union[str, DockerImage]) -> None: | ||
super().__init__(str(image)) | ||
self.internal_port = port | ||
self.with_exposed_ports(self.internal_port) | ||
|
||
@wait_container_is_ready(HTTPError) | ||
def _connect(self) -> None: | ||
# noinspection HttpUrlsUsage | ||
url = self._create_connection_url() | ||
try: | ||
with urlopen(url) as r: | ||
assert b"" in r.read() | ||
except HTTPError as e: | ||
# 404 is expected, as the server may not have the specific endpoint we are looking for | ||
if e.code == 404: | ||
pass | ||
else: | ||
raise | ||
|
||
def get_api_url(self) -> str: | ||
raise NotImplementedError | ||
|
||
def _create_connection_url(self) -> str: | ||
if self._container is None: | ||
raise ContainerStartException("container has not been started") | ||
host = self.get_container_host_ip() | ||
exposed_port = self.get_exposed_port(self.internal_port) | ||
url = f"http://{host}:{exposed_port}" | ||
return url | ||
|
||
def start(self) -> "ServerContainer": | ||
super().start() | ||
self._connect() | ||
return self | ||
|
||
def stop(self, force=True, delete_volume=True) -> None: | ||
super().stop(force, delete_volume) | ||
|
||
def get_client(self) -> httpx.Client: | ||
return httpx.Client(base_url=self.get_api_url()) | ||
|
||
def get_stdout(self) -> str: | ||
return self.get_logs()[0].decode("utf-8") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import pytest | ||
from typing import Callable | ||
from testcontainers.core.container import DockerClient | ||
|
||
|
||
@pytest.fixture | ||
def check_for_image() -> Callable[[str, bool], None]: | ||
"""Warp the check_for_image function in a fixture""" | ||
|
||
def _check_for_image(image_short_id: str, cleaned: bool) -> None: | ||
""" | ||
Validates if the image is present or not. | ||
:param image_short_id: The short id of the image | ||
:param cleaned: True if the image should not be present, False otherwise | ||
""" | ||
client = DockerClient() | ||
images = client.client.images.list() | ||
found = any(image.short_id.endswith(image_short_id) for image in images) | ||
assert found is not cleaned, f'Image {image_short_id} was {"found" if cleaned else "not found"}' | ||
|
||
return _check_for_image |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
FROM python:3.9 | ||
|
||
WORKDIR /app | ||
|
||
RUN pip install fastapi | ||
|
||
COPY ./app /app | ||
|
||
EXPOSE 80 | ||
|
||
CMD ["fastapi", "run", "main.py", "--port", "80"] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from fastapi import FastAPI | ||
|
||
app = FastAPI() | ||
|
||
|
||
@app.get("/api/v1/") | ||
def read_root(): | ||
return {"Status": "Working"} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.. autoclass:: testcontainers.testmoduleimport.NewSubModuleContainer | ||
.. title:: testcontainers.testmoduleimport.NewSubModuleContainer |
1 change: 1 addition & 0 deletions
1
modules/testmoduleimport/testcontainers/testmoduleimport/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .new_sub_module import NewSubModuleContainer # noqa: F401 |
27 changes: 27 additions & 0 deletions
27
modules/testmoduleimport/testcontainers/testmoduleimport/new_sub_module.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from testcontainers.generic.server import ServerContainer | ||
|
||
|
||
class NewSubModuleContainer(ServerContainer): | ||
""" | ||
This class is a mock container for testing purposes. It is used to test importing from other modules. | ||
.. doctest:: | ||
>>> import httpx | ||
>>> from testcontainers.core.image import DockerImage | ||
>>> from testcontainers.testmoduleimport import NewSubModuleContainer | ||
>>> with DockerImage(path="./modules/generic/tests/samples/python_server", tag="test-mod:latest") as image: | ||
... with NewSubModuleContainer(port=9000, image=image) as srv: | ||
... url = srv._create_connection_url() | ||
... response = httpx.get(f"{url}", timeout=5) | ||
... assert response.status_code == 200, "Response status code is not 200" | ||
... assert srv.print_mock() == "NewSubModuleContainer" | ||
""" | ||
|
||
def __init__(self, port: int, image: str) -> None: | ||
super().__init__(port, image) | ||
|
||
def print_mock(self) -> str: | ||
return "NewSubModuleContainer" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import httpx | ||
|
||
from testcontainers.core.waiting_utils import wait_for_logs | ||
from testcontainers.core.image import DockerImage | ||
from testcontainers.testmoduleimport import NewSubModuleContainer | ||
|
||
|
||
def test_like_doctest(): | ||
with DockerImage(path="./modules/generic/tests/samples/python_server", tag="test-srv:latest") as image: | ||
with NewSubModuleContainer(port=9000, image=image) as srv: | ||
assert srv.print_mock() == "NewSubModuleContainer" | ||
url = srv._create_connection_url() | ||
response = httpx.get(f"{url}", timeout=5) | ||
assert response.status_code == 200, "Response status code is not 200" | ||
_ = wait_for_logs(srv, "GET / HTTP/1.1") |
Oops, something went wrong.