Skip to content

Commit c68b45e

Browse files
committed
use "brewblox_history_" prefix for config
1 parent ddb772b commit c68b45e

File tree

8 files changed

+118
-53
lines changed

8 files changed

+118
-53
lines changed

brewblox_history/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def flatten(d, parent_key=''):
3333
class ServiceConfig(BaseSettings):
3434
model_config = SettingsConfigDict(
3535
env_file='.appenv',
36-
env_prefix='brewblox_',
36+
env_prefix='brewblox_history_',
3737
case_sensitive=False,
3838
json_schema_extra='ignore',
3939
)

brewblox_history/timeseries_api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,12 @@ async def timeseries_stream(ws: WebSocket):
199199
'message': msg,
200200
})
201201

202-
except WebSocketDisconnect:
202+
except WebSocketDisconnect: # pragma: no cover
203203
pass
204204

205205
finally:
206206
# Coverage complains about next line -> exit not being covered
207207
for task in streams.values(): # pragma: no cover
208208
task.cancel()
209+
await asyncio.gather(*streams.values(),
210+
return_exceptions=True)

brewblox_history/utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ def now() -> datetime: # pragma: no cover
123123
return datetime.now(timezone.utc)
124124

125125

126-
def select_timeframe(start, duration, end, min_step) -> tuple[str, str, str]:
126+
def select_timeframe(start: DatetimeSrc_,
127+
duration: str,
128+
end: DatetimeSrc_,
129+
min_step: timedelta,
130+
) -> tuple[str, str, str]:
127131
"""Calculate start, end, and step for given start, duration, and end
128132
129133
The returned `start` and `end` strings are either empty,

docker-compose.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ services:
1212
image: victoriametrics/victoria-metrics:v1.88.0
1313
labels:
1414
- traefik.http.services.victoria.loadbalancer.server.port=8428
15+
command: --envflag.enable=true --envflag.prefix=VM_
16+
environment:
17+
- VM_retentionPeriod=100y
18+
- VM_influxMeasurementFieldSeparator=/
19+
- VM_http_pathPrefix=/victoria
20+
- VM_search_latencyOffset=10s
1521
volumes:
1622
- type: bind
1723
source: ./victoria
1824
target: /victoria-metrics-data
19-
command: >-
20-
--retentionPeriod=100y
21-
--influxMeasurementFieldSeparator=/
22-
--http.pathPrefix=/victoria
23-
--search.latencyOffset=10s
2425

2526
redis:
2627
image: redis:6.0
@@ -41,9 +42,12 @@ services:
4142
- type: bind
4243
source: ./parse_appenv.py
4344
target: /app/parse_appenv.py
45+
- type: bind
46+
source: ./entrypoint.sh
47+
target: /app/entrypoint.sh
4448
environment:
45-
- BREWBLOX_DEBUG=True
4649
- UVICORN_RELOAD=True
50+
- BREWBLOX_HISTORY_DEBUG=True
4751
command: --unknown=flappy
4852
ports:
4953
- "5000:5000"

poetry.lock

Lines changed: 32 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pytest-docker = "*"
3333
pytest-asyncio = "*"
3434
pytest-httpx = "*"
3535
asgi-lifespan = "*"
36+
httpx-ws = "*"
3637

3738
[build-system]
3839
requires = ["poetry-core>=1.0.0"]

test/conftest.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66

7+
import asyncio
78
import logging
89
from collections.abc import Generator
910
from pathlib import Path
@@ -12,9 +13,9 @@
1213
from asgi_lifespan import LifespanManager
1314
from fastapi import FastAPI
1415
from httpx import AsyncClient
16+
from httpx_ws.transport import ASGIWebSocketTransport
1517
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
1618
from pytest_docker.plugin import Services as DockerServices
17-
from starlette.testclient import TestClient
1819

1920
from brewblox_history import app_factory, utils
2021
from brewblox_history.models import ServiceConfig
@@ -65,6 +66,23 @@ def config(monkeypatch: pytest.MonkeyPatch,
6566
yield cfg
6667

6768

69+
@pytest.fixture(autouse=True)
70+
def m_sleep(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest):
71+
"""
72+
Allows keeping track of calls to asyncio sleep.
73+
For tests, we want to reduce all sleep durations.
74+
Set a breakpoint in the wrapper to track all calls.
75+
"""
76+
real_func = asyncio.sleep
77+
78+
async def wrapper(delay: float, *args, **kwargs):
79+
if delay > 0.1:
80+
print(f'asyncio.sleep({delay}) in {request.node.name}')
81+
return await real_func(delay, *args, **kwargs)
82+
monkeypatch.setattr('asyncio.sleep', wrapper)
83+
yield
84+
85+
6886
@pytest.fixture(autouse=True)
6987
def setup_logging(config):
7088
app_factory.setup_logging(True)
@@ -83,32 +101,28 @@ def app() -> FastAPI:
83101

84102

85103
@pytest.fixture
86-
async def client(app: FastAPI) -> Generator[AsyncClient, None, None]:
104+
async def manager(app: FastAPI) -> Generator[LifespanManager, None, None]:
87105
"""
88-
The default test client for making REST API calls.
89-
Using this fixture will also guarantee that lifespan startup has happened.
106+
AsyncClient does not automatically send ASGI lifespan events to the app
107+
https://asgi.readthedocs.io/en/latest/specs/lifespan.html
90108
91-
Do not use `client` and `sync_client` at the same time.
109+
For testing, this ensures that lifespan() functions are handled.
110+
If you don't need to make HTTP requests, you can use the manager
111+
without the `client` fixture.
92112
"""
93-
# AsyncClient does not automatically send ASGI lifespan events to the app
94-
# https://asgi.readthedocs.io/en/latest/specs/lifespan.html
95-
async with LifespanManager(app):
96-
async with AsyncClient(app=app,
97-
base_url='http://test') as ac:
98-
yield ac
113+
async with LifespanManager(app) as mgr:
114+
yield mgr
99115

100116

101117
@pytest.fixture
102-
def sync_client(app: FastAPI) -> Generator[TestClient, None, None]:
118+
async def client(app: FastAPI, manager: LifespanManager) -> Generator[AsyncClient, None, None]:
103119
"""
104-
The alternative test client for making REST API calls.
120+
The default test client for making REST API calls.
105121
Using this fixture will also guarantee that lifespan startup has happened.
106-
107-
`sync_client` is provided because `client` cannot make websocket requests.
108-
109-
Do not use `client` and `sync_client` at the same time.
110122
"""
111-
# The Starlette TestClient does send lifespan events
112-
# and does not need a separate LifespanManager
113-
with TestClient(app=app, base_url='http://test') as c:
114-
yield c
123+
# AsyncClient does not automatically send ASGI lifespan events to the app
124+
# https://asgi.readthedocs.io/en/latest/specs/lifespan.html
125+
async with AsyncClient(app=app,
126+
base_url='http://test',
127+
transport=ASGIWebSocketTransport(app)) as ac:
128+
yield ac

test/test_timeseries_api.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
Tests brewblox_history.timeseries_api
33
"""
44

5+
import asyncio
56
from datetime import datetime, timezone
67
from time import time_ns
78
from unittest.mock import ANY, AsyncMock, Mock
89

910
import pytest
1011
from fastapi import FastAPI
1112
from httpx import AsyncClient
13+
from httpx_ws import aconnect_ws
1214
from pytest import approx
1315
from pytest_mock import MockerFixture
14-
from starlette.testclient import TestClient, WebSocketTestSession
1516

1617
from brewblox_history import app_factory, timeseries_api, utils
1718
from brewblox_history.models import (ServiceConfig, TimeSeriesCsvQuery,
@@ -166,7 +167,7 @@ async def csv_mock(args: TimeSeriesCsvQuery):
166167
assert resp.text == 'a,b,c\n'
167168

168169

169-
async def test_stream(sync_client: TestClient, config: ServiceConfig, m_victoria: Mock):
170+
async def test_stream(client: AsyncClient, config: ServiceConfig, m_victoria: Mock):
170171
config.ranges_interval = 0.001
171172
m_victoria.metrics.return_value = [
172173
TimeSeriesMetric(
@@ -200,18 +201,16 @@ async def test_stream(sync_client: TestClient, config: ServiceConfig, m_victoria
200201
),
201202
]
202203

203-
with sync_client.websocket_connect('/timeseries/stream') as ws:
204-
ws: WebSocketTestSession
205-
204+
async with aconnect_ws('/timeseries/stream', client) as ws:
206205
# Metrics
207-
ws.send_json({
206+
await ws.send_json({
208207
'id': 'test-metrics',
209208
'command': 'metrics',
210209
'query': {
211210
'fields': ['a', 'b', 'c'],
212211
},
213212
})
214-
resp = ws.receive_json()
213+
resp = await ws.receive_json()
215214
assert resp == {
216215
'id': 'test-metrics',
217216
'data': {
@@ -220,15 +219,15 @@ async def test_stream(sync_client: TestClient, config: ServiceConfig, m_victoria
220219
}
221220

222221
# Ranges
223-
ws.send_json({
222+
await ws.send_json({
224223
'id': 'test-ranges-once',
225224
'command': 'ranges',
226225
'query': {
227226
'fields': ['a', 'b', 'c'],
228227
'end': '2021-07-15T14:29:30.000Z',
229228
},
230229
})
231-
resp = ws.receive_json()
230+
resp = await ws.receive_json()
232231
assert resp == {
233232
'id': 'test-ranges-once',
234233
'data': {
@@ -238,23 +237,23 @@ async def test_stream(sync_client: TestClient, config: ServiceConfig, m_victoria
238237
}
239238

240239
# Live ranges
241-
ws.send_json({
240+
await ws.send_json({
242241
'id': 'test-ranges-live',
243242
'command': 'ranges',
244243
'query': {
245244
'fields': ['a', 'b', 'c'],
246245
'duration': '30m',
247246
},
248247
})
249-
resp = ws.receive_json()
248+
resp = await ws.receive_json()
250249
assert resp == {
251250
'id': 'test-ranges-live',
252251
'data': {
253252
'initial': True,
254253
'ranges': [ANY, ANY, ANY],
255254
},
256255
}
257-
resp = ws.receive_json()
256+
resp = await ws.receive_json()
258257
assert resp == {
259258
'id': 'test-ranges-live',
260259
'data': {
@@ -264,13 +263,19 @@ async def test_stream(sync_client: TestClient, config: ServiceConfig, m_victoria
264263
}
265264

266265
# Stop live ranges
267-
ws.send_json({
266+
await ws.send_json({
268267
'id': 'test-ranges-live',
269268
'command': 'stop',
270269
})
271270

271+
# https://github.com/frankie567/httpx-ws/issues/49
272+
await ws.close()
273+
await asyncio.gather(ws._background_receive_task,
274+
ws._background_keepalive_ping_task,
275+
return_exceptions=True)
276+
272277

273-
async def test_stream_error(sync_client: TestClient, m_victoria: Mock):
278+
async def test_stream_error(client: AsyncClient, m_victoria: Mock):
274279
dt = datetime(2021, 7, 15, 19, tzinfo=timezone.utc)
275280
m_victoria.ranges.side_effect = RuntimeError
276281
m_victoria.metrics.return_value = [
@@ -281,16 +286,14 @@ async def test_stream_error(sync_client: TestClient, m_victoria: Mock):
281286
),
282287
]
283288

284-
with sync_client.websocket_connect('/timeseries/stream') as ws:
285-
ws: WebSocketTestSession
286-
289+
async with aconnect_ws('/timeseries/stream', client) as ws:
287290
# Invalid request
288-
ws.send_json({'empty': True})
289-
resp = ws.receive_json()
291+
await ws.send_json({'empty': True})
292+
resp = await ws.receive_json()
290293
assert resp['error']
291294

292295
# Backend raises error
293-
ws.send_json({
296+
await ws.send_json({
294297
'id': 'test-ranges-once',
295298
'command': 'ranges',
296299
'query': {
@@ -300,15 +303,15 @@ async def test_stream_error(sync_client: TestClient, m_victoria: Mock):
300303
})
301304

302305
# Other command is OK
303-
ws.send_json({
306+
await ws.send_json({
304307
'id': 'test-metrics',
305308
'command': 'metrics',
306309
'query': {
307310
'fields': ['a', 'b', 'c'],
308311
},
309312
})
310313

311-
resp = ws.receive_json()
314+
resp = await ws.receive_json()
312315
assert resp == {
313316
'id': 'test-metrics',
314317
'data': {
@@ -319,3 +322,9 @@ async def test_stream_error(sync_client: TestClient, m_victoria: Mock):
319322
}],
320323
},
321324
}
325+
326+
# https://github.com/frankie567/httpx-ws/issues/49
327+
await ws.close()
328+
await asyncio.gather(ws._background_receive_task,
329+
ws._background_keepalive_ping_task,
330+
return_exceptions=True)

0 commit comments

Comments
 (0)