|
1 |
| -import os |
2 |
| -import socket |
3 |
| -import ssl |
4 |
| -from collections.abc import Iterable |
5 |
| -from enum import Enum, auto |
6 |
| -from typing import Callable, Optional |
7 |
| -from urllib.error import HTTPError, URLError |
8 |
| -from urllib.request import urlopen |
| 1 | +from .mongodb import MongoDBEmulatorContainer |
| 2 | +from .nosql import NoSQLEmulatorContainer |
9 | 3 |
|
10 |
| -from azure.core.exceptions import ServiceRequestError |
11 |
| -from azure.cosmos import CosmosClient as SyncCosmosClient |
12 |
| -from azure.cosmos.aio import CosmosClient as AsyncCosmosClient |
13 |
| -from typing_extensions import Self |
14 |
| - |
15 |
| -from testcontainers.core.container import DockerContainer |
16 |
| -from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs |
17 |
| - |
18 |
| -__all__ = ["CosmosDBEmulatorContainer", "Endpoints"] |
19 |
| - |
20 |
| - |
21 |
| -class Endpoints(Enum): |
22 |
| - MongoDB = auto() |
23 |
| - |
24 |
| - |
25 |
| -# Ports mostly derived from https://docs.microsoft.com/en-us/azure/cosmos-db/emulator-command-line-parameters |
26 |
| -EMULATOR_PORT = 8081 |
27 |
| -endpoint_ports = { |
28 |
| - Endpoints.MongoDB: frozenset([10255]), |
29 |
| -} |
30 |
| - |
31 |
| - |
32 |
| -def is_truthy_string(s: str): |
33 |
| - return s.lower().strip() in {"true", "yes", "y", "1"} |
34 |
| - |
35 |
| - |
36 |
| -class CosmosDBEmulatorContainer(DockerContainer): |
37 |
| - """ |
38 |
| - CosmosDB Emulator container. |
39 |
| -
|
40 |
| - Example: |
41 |
| - .. doctest:: |
42 |
| - >>> from testcontainers.cosmosdb import CosmosDBEmulatorContainer |
43 |
| - >>> with CosmosDBEmulatorContainer() as cosmosdb: |
44 |
| - ... db = cosmosdb.insecure_sync_client().create_database_if_not_exists("test") |
45 |
| -
|
46 |
| - .. doctest:: |
47 |
| - >>> from testcontainers.cosmosdb import CosmosDBEmulatorContainer |
48 |
| - >>> with CosmosDBEmulatorContainer() as emulator: |
49 |
| - ... cosmosdb = CosmosClient(url=emulator.url, credential=emulator.key, connection_verify=False) |
50 |
| - ... db = cosmosdb.create_database_if_not_exists("test") |
51 |
| -
|
52 |
| - .. doctest:: |
53 |
| - >>> from testcontainers.cosmosdb import CosmosDBEmulatorContainer, Endpoints |
54 |
| - >>> with CosmosDBEmulatorContainer(endpoints=[Endpoints.MongoDB], mongodb_version="4.0") as emulator: |
55 |
| - ... print(f"Point yout MongoDB client to {emulator.host}:{next(iter(emulator.ports(Endpoints.MongoDB)))}") |
56 |
| - """ |
57 |
| - |
58 |
| - def __init__( |
59 |
| - self, |
60 |
| - image: str = os.getenv( |
61 |
| - "AZURE_COSMOS_EMULATOR_IMAGE", "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest" |
62 |
| - ), |
63 |
| - partition_count: int = os.getenv("AZURE_COSMOS_EMULATOR_PARTITION_COUNT", None), |
64 |
| - enable_data_persistence: bool = is_truthy_string( |
65 |
| - os.getenv("AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE", "false") |
66 |
| - ), |
67 |
| - bind_ports: bool = is_truthy_string(os.getenv("AZURE_COSMOS_EMULATOR_BIND_PORTS", "true")), |
68 |
| - key: str = os.getenv( |
69 |
| - "AZURE_COSMOS_EMULATOR_KEY", |
70 |
| - "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", |
71 |
| - ), |
72 |
| - endpoints: Iterable[Endpoints] = [], # the emulator image does not support host-container port mapping |
73 |
| - mongodb_version: Optional[str] = None, |
74 |
| - **docker_client_kw, |
75 |
| - ): |
76 |
| - super().__init__(image=image, **docker_client_kw) |
77 |
| - self.partition_count = partition_count |
78 |
| - self.key = key |
79 |
| - self.enable_data_persistence = enable_data_persistence |
80 |
| - self.endpoints = frozenset(endpoints) |
81 |
| - self.bind_ports = bind_ports |
82 |
| - assert (Endpoints.MongoDB not in self.endpoints) or ( |
83 |
| - mongodb_version is not None |
84 |
| - ), "A MongoDB version is required to use the MongoDB Endpoint" |
85 |
| - self.mongodb_version = mongodb_version |
86 |
| - |
87 |
| - @property |
88 |
| - def url(self) -> str: |
89 |
| - """ |
90 |
| - The url to the CosmosDB server |
91 |
| - """ |
92 |
| - return f"https://{self.host}:{self.get_exposed_port(EMULATOR_PORT)}" |
93 |
| - |
94 |
| - @property |
95 |
| - def host(self) -> str: |
96 |
| - return self.get_container_host_ip() |
97 |
| - |
98 |
| - @property |
99 |
| - def certificate_pem(self) -> bytes: |
100 |
| - """ |
101 |
| - PEM-encoded certificate of the CosmosDB server |
102 |
| - """ |
103 |
| - return self._cert_pem_bytes |
104 |
| - |
105 |
| - def ports(self, endpoint: Endpoints) -> Iterable[int]: |
106 |
| - """ |
107 |
| - Returns the set of exposed ports for a given endpoint. |
108 |
| - If bind_ports is True, the returned ports will be the NAT-ed ports reachable from the host. |
109 |
| - """ |
110 |
| - assert endpoint in self.endpoints, f"Endpoint {endpoint} is not exposed" |
111 |
| - return {self.get_exposed_port(p) for p in endpoint_ports[endpoint]} |
112 |
| - |
113 |
| - def insecure_async_client(self) -> AsyncCosmosClient: |
114 |
| - """ |
115 |
| - Returns an asynchronous CosmosClient instance |
116 |
| - """ |
117 |
| - return AsyncCosmosClient(url=self.url, credential=self.key, connection_verify=False) |
118 |
| - |
119 |
| - def insecure_sync_client(self) -> SyncCosmosClient: |
120 |
| - """ |
121 |
| - Returns a synchronous CosmosClient instance |
122 |
| - """ |
123 |
| - return SyncCosmosClient(url=self.url, credential=self.key, connection_verify=False) |
124 |
| - |
125 |
| - def start(self) -> Self: |
126 |
| - self._configure() |
127 |
| - super().start() |
128 |
| - self._wait_until_ready() |
129 |
| - self._cert_pem_bytes = self._download_cert() |
130 |
| - return self |
131 |
| - |
132 |
| - def _configure(self) -> None: |
133 |
| - self.with_bind_ports(EMULATOR_PORT, EMULATOR_PORT) |
134 |
| - |
135 |
| - endpoints_ports = [] |
136 |
| - for endpoint in self.endpoints: |
137 |
| - endpoints_ports.extend(endpoint_ports[endpoint]) |
138 |
| - |
139 |
| - if self.bind_ports: |
140 |
| - [self.with_bind_ports(port, port) for port in endpoints_ports] |
141 |
| - else: |
142 |
| - self.with_exposed_ports(*endpoints_ports) |
143 |
| - |
144 |
| - ( |
145 |
| - self.with_env("AZURE_COSMOS_EMULATOR_PARTITION_COUNT", str(self.partition_count)) |
146 |
| - .with_env("AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE", socket.gethostbyname(socket.gethostname())) |
147 |
| - .with_env("AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE", str(self.enable_data_persistence)) |
148 |
| - .with_env("AZURE_COSMOS_EMULATOR_KEY", str(self.key)) |
149 |
| - ) |
150 |
| - |
151 |
| - if Endpoints.MongoDB in self.endpoints: |
152 |
| - self.with_env("AZURE_COSMOS_EMULATOR_ENABLE_MONGODB_ENDPOINT", self.mongodb_version) |
153 |
| - |
154 |
| - def _wait_until_ready(self) -> Self: |
155 |
| - """ |
156 |
| - Waits until the CosmosDB Emulator image is ready to be used. |
157 |
| - """ |
158 |
| - ( |
159 |
| - self._wait_for_logs(container=self, predicate="Started\\s*$") |
160 |
| - ._wait_for_url(f"{self.url}/_explorer/index.html") |
161 |
| - ._wait_for_query_success(lambda sync_client: list(sync_client.list_databases())) |
162 |
| - ) |
163 |
| - return self |
164 |
| - |
165 |
| - @wait_container_is_ready(HTTPError, URLError) |
166 |
| - def _wait_for_url(self, url: str) -> Self: |
167 |
| - with urlopen(url, context=ssl._create_unverified_context()) as response: |
168 |
| - response.read() |
169 |
| - return self |
170 |
| - |
171 |
| - def _wait_for_logs(self, *args, **kwargs) -> Self: |
172 |
| - wait_for_logs(*args, **kwargs) |
173 |
| - return self |
174 |
| - |
175 |
| - @wait_container_is_ready(ServiceRequestError) |
176 |
| - def _wait_for_query_success(self, query: Callable[[SyncCosmosClient], None]) -> Self: |
177 |
| - with self.insecure_sync_client() as c: |
178 |
| - query(c) |
179 |
| - return self |
180 |
| - |
181 |
| - def _download_cert(self) -> bytes: |
182 |
| - with urlopen(f"{self.url}/_explorer/emulator.pem", context=ssl._create_unverified_context()) as response: |
183 |
| - return response.read() |
| 4 | +__all__ = ["MongoDBEmulatorContainer", "NoSQLEmulatorContainer"] |
0 commit comments