Skip to content

Commit dea2012

Browse files
committed
MOD: Python mock live server enhancement
1 parent 282b62c commit dea2012

File tree

13 files changed

+972
-999
lines changed

13 files changed

+972
-999
lines changed

databento/common/cram.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
import hashlib
77
import os
88
import sys
9+
from typing import Final
910

1011

11-
BUCKET_ID_LENGTH = 5
12+
BUCKET_ID_LENGTH: Final = 5
1213

1314

1415
def get_challenge_response(challenge: str, key: str) -> str:

databento/live/gateway.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
from io import BytesIO
66
from operator import attrgetter
7+
from typing import SupportsBytes
78
from typing import TypeVar
89

910
from databento_dbn import Encoding
@@ -20,16 +21,21 @@
2021

2122

2223
@dataclasses.dataclass
23-
class GatewayControl:
24+
class GatewayControl(SupportsBytes):
2425
"""
2526
Base class for gateway control messages.
2627
"""
2728

2829
@classmethod
29-
def parse(cls: type[T], line: str) -> T:
30+
def parse(cls: type[T], line: str | bytes) -> T:
3031
"""
3132
Parse a message of type `T` from a string.
3233
34+
Parameters
35+
----------
36+
line : str | bytes
37+
The data to parse into a GatewayControl message.
38+
3339
Returns
3440
-------
3541
T
@@ -40,17 +46,20 @@ def parse(cls: type[T], line: str) -> T:
4046
If the line fails to parse.
4147
4248
"""
49+
if isinstance(line, bytes):
50+
line = line.decode("utf-8")
51+
4352
if not line.endswith("\n"):
44-
raise ValueError(f"`{line.strip()}` does not end with a newline")
53+
raise ValueError(f"'{line!r}' does not end with a newline")
4554

46-
split_tokens = [t.partition("=") for t in line[:-1].split("|")]
55+
split_tokens = [t.partition("=") for t in line.strip().split("|")]
4756
data_dict = {k: v for k, _, v in split_tokens}
4857

4958
try:
5059
return cls(**data_dict)
5160
except TypeError:
5261
raise ValueError(
53-
f"`{line.strip()} is not a parsible {cls.__name__}",
62+
f"'{line!r}'is not a parsible {cls.__name__}",
5463
) from None
5564

5665
def __str__(self) -> str:
@@ -154,7 +163,7 @@ def parse_gateway_message(line: str) -> GatewayControl:
154163
return message_cls.parse(line)
155164
except ValueError:
156165
continue
157-
raise ValueError(f"`{line.strip()}` is not a parsible gateway message")
166+
raise ValueError(f"'{line.strip()}' is not a parsible gateway message")
158167

159168

160169
class GatewayDecoder:

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,13 @@ zstandard = ">=0.21.0"
4747
black = "^23.9.1"
4848
mypy = "1.5.1"
4949
pytest = "^7.4.2"
50-
pytest-asyncio = ">=0.21.0"
50+
pytest-asyncio = "==0.21.1"
5151
ruff = "^0.0.291"
5252
types-requests = "^2.30.0.0"
5353
tomli = "^2.0.1"
5454
teamcity-messages = "^1.32"
5555
types-pytz = "^2024.1.0.20240203"
56+
types-aiofiles = "^23.2.0.20240403"
5657

5758
[build-system]
5859
requires = ["poetry-core"]

tests/conftest.py

Lines changed: 45 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,25 @@
55
from __future__ import annotations
66

77
import asyncio
8+
import logging
89
import pathlib
910
import random
1011
import string
11-
import threading
12+
from collections.abc import AsyncGenerator
1213
from collections.abc import Generator
1314
from collections.abc import Iterable
1415
from typing import Callable
1516

17+
import databento.live.session
1618
import pytest
1719
from databento import historical
1820
from databento import live
1921
from databento.common.publishers import Dataset
20-
from databento.live import session
2122
from databento_dbn import Schema
2223

2324
from tests import TESTS_ROOT
24-
from tests.mock_live_server import MockLiveServer
25+
from tests.mockliveserver.fixture import MockLiveServerInterface
26+
from tests.mockliveserver.fixture import fixture_mock_live_server # noqa
2527

2628

2729
def pytest_addoption(parser: pytest.Parser) -> None:
@@ -88,6 +90,14 @@ def pytest_collection_modifyitems(
8890
item.add_marker(skip_release)
8991

9092

93+
@pytest.fixture(name="event_loop", scope="module")
94+
def fixture_event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
95+
policy = asyncio.get_event_loop_policy()
96+
loop = policy.new_event_loop()
97+
yield loop
98+
loop.close()
99+
100+
91101
@pytest.fixture(name="live_test_data_path")
92102
def fixture_live_test_data_path() -> pathlib.Path:
93103
"""
@@ -199,74 +209,13 @@ def fixture_test_api_key() -> str:
199209
return f"db-{random_str}"
200210

201211

202-
@pytest.fixture(name="thread_loop", scope="session")
203-
def fixture_thread_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
204-
"""
205-
Fixture for a threaded event loop.
206-
207-
Yields
208-
------
209-
asyncio.AbstractEventLoop
210-
211-
"""
212-
loop = asyncio.new_event_loop()
213-
thread = threading.Thread(
214-
name="MockLiveServer",
215-
target=loop.run_forever,
216-
args=(),
217-
daemon=True,
218-
)
219-
thread.start()
220-
yield loop
221-
loop.stop()
222-
223-
224-
@pytest.fixture(name="mock_live_server")
225-
def fixture_mock_live_server(
226-
thread_loop: asyncio.AbstractEventLoop,
212+
@pytest.fixture(name="test_live_api_key")
213+
async def fixture_test_live_api_key(
227214
test_api_key: str,
228-
caplog: pytest.LogCaptureFixture,
229-
unused_tcp_port: int,
230-
monkeypatch: pytest.MonkeyPatch,
231-
) -> Generator[MockLiveServer, None, None]:
232-
"""
233-
Fixture for a MockLiveServer instance.
234-
235-
Yields
236-
------
237-
MockLiveServer
238-
239-
"""
240-
monkeypatch.setenv(
241-
name="DATABENTO_API_KEY",
242-
value=test_api_key,
243-
)
244-
monkeypatch.setattr(
245-
session,
246-
"AUTH_TIMEOUT_SECONDS",
247-
1,
248-
)
249-
monkeypatch.setattr(
250-
session,
251-
"CONNECT_TIMEOUT_SECONDS",
252-
1,
253-
)
254-
with caplog.at_level("DEBUG"):
255-
mock_live_server = asyncio.run_coroutine_threadsafe(
256-
coro=MockLiveServer.create(
257-
host="127.0.0.1",
258-
port=unused_tcp_port,
259-
dbn_path=TESTS_ROOT / "data",
260-
),
261-
loop=thread_loop,
262-
).result()
263-
264-
yield mock_live_server
265-
266-
asyncio.run_coroutine_threadsafe(
267-
coro=mock_live_server.stop(),
268-
loop=thread_loop,
269-
).result()
215+
mock_live_server: MockLiveServerInterface,
216+
) -> AsyncGenerator[str, None]:
217+
async with mock_live_server.api_key_context(test_api_key):
218+
yield test_api_key
270219

271220

272221
@pytest.fixture(name="historical_client")
@@ -289,10 +238,12 @@ def fixture_historical_client(
289238

290239

291240
@pytest.fixture(name="live_client")
292-
def fixture_live_client(
293-
test_api_key: str,
294-
mock_live_server: MockLiveServer,
295-
) -> Generator[live.client.Live, None, None]:
241+
async def fixture_live_client(
242+
test_live_api_key: str,
243+
mock_live_server: MockLiveServerInterface,
244+
caplog: pytest.LogCaptureFixture,
245+
monkeypatch: pytest.MonkeyPatch,
246+
) -> AsyncGenerator[live.client.Live, None]:
296247
"""
297248
Fixture for a Live client to connect to the MockLiveServer.
298249
@@ -301,11 +252,25 @@ def fixture_live_client(
301252
Live
302253
303254
"""
304-
test_client = live.client.Live(
305-
key=test_api_key,
306-
gateway=mock_live_server.host,
307-
port=mock_live_server.port,
255+
monkeypatch.setattr(
256+
databento.live.session,
257+
"AUTH_TIMEOUT_SECONDS",
258+
0.5,
308259
)
309-
yield test_client
310-
if test_client.is_connected():
260+
monkeypatch.setattr(
261+
databento.live.session,
262+
"CONNECT_TIMEOUT_SECONDS",
263+
0.5,
264+
)
265+
266+
with caplog.at_level(logging.DEBUG):
267+
test_client = live.client.Live(
268+
key=test_live_api_key,
269+
gateway=mock_live_server.host,
270+
port=mock_live_server.port,
271+
)
272+
273+
with mock_live_server.test_context():
274+
yield test_client
275+
311276
test_client.stop()

0 commit comments

Comments
 (0)