Skip to content

Commit 2f1b34b

Browse files
committed
Make calls to numtracker async
1 parent 61ef66f commit 2f1b34b

File tree

6 files changed

+112
-192
lines changed

6 files changed

+112
-192
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies = [
3434
"observability-utils>=0.1.4",
3535
"pyjwt[crypto]",
3636
"tomlkit",
37+
"httpx>=0.28.1",
3738
]
3839
dynamic = ["version"]
3940
license.file = "LICENSE"
@@ -51,6 +52,7 @@ dev = [
5152
"pyright",
5253
"pytest-cov",
5354
"pytest-asyncio",
55+
"pytest-httpx>=0.35.0",
5456
"responses",
5557
"ruff",
5658
"semver",

src/blueapi/client/numtracker.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pathlib import Path
44
from textwrap import dedent
55

6-
import requests
6+
import httpx
77
from pydantic import Field
88

99
from blueapi.utils import BlueapiBaseModel
@@ -60,7 +60,7 @@ def set_headers(self, headers: Mapping[str, str]) -> None:
6060

6161
self._headers = headers
6262

63-
def create_scan(
63+
async def create_scan(
6464
self, instrument_session: str, instrument: str
6565
) -> NumtrackerScanMutationResponse:
6666
"""
@@ -92,11 +92,12 @@ def create_scan(
9292
""")
9393
}
9494

95-
response = requests.post(
96-
self._url,
97-
headers=self._headers,
98-
json=query,
99-
)
95+
async with httpx.AsyncClient() as client:
96+
response = await client.post(
97+
self._url,
98+
headers=self._headers,
99+
json=query,
100+
)
100101

101102
response.raise_for_status()
102103
json = response.json()

src/blueapi/service/interface.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ def numtracker_client() -> NumtrackerClient | None:
110110
return None
111111

112112

113-
def _update_scan_num(md: dict[str, Any]) -> int:
113+
async def _update_scan_num(md: dict[str, Any]) -> int:
114114
numtracker = numtracker_client()
115115
if numtracker is not None:
116-
scan = numtracker.create_scan(md["instrument_session"], md["instrument"])
116+
scan = await numtracker.create_scan(md["instrument_session"], md["instrument"])
117117
md["data_session_directory"] = str(scan.scan.directory.path)
118118
return scan.scan.scan_number
119119
else:

tests/conftest.py

Lines changed: 24 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import base64
33
import os
44
import time
5-
from collections.abc import Iterable
65
from pathlib import Path
76
from textwrap import dedent
87
from typing import Any, cast
@@ -21,7 +20,6 @@
2120
from opentelemetry.sdk.trace import TracerProvider
2221
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
2322
from opentelemetry.trace import get_tracer_provider
24-
from responses.matchers import json_params_matcher
2523

2624
from blueapi.config import ApplicationConfig, OIDCConfig
2725
from blueapi.service.model import Cache
@@ -336,30 +334,9 @@ def mock_jwks_fetch(json_web_keyset: JWK):
336334
return patch("jwt.PyJWKClient.fetch_data", mock)
337335

338336

339-
# Prevent pytest from catching exceptions when debugging in vscode so that break on
340-
# exception works correctly (see: https://github.com/pytest-dev/pytest/issues/7409)
341-
if os.getenv("PYTEST_RAISE", "0") == "1":
342-
343-
@pytest.hookimpl(tryfirst=True)
344-
def pytest_exception_interact(call: pytest.CallInfo[Any]):
345-
if call.excinfo is not None:
346-
raise call.excinfo.value
347-
else:
348-
raise RuntimeError(
349-
f"{call} has no exception data, an unknown error has occurred"
350-
)
351-
352-
@pytest.hookimpl(tryfirst=True)
353-
def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]):
354-
raise excinfo.value
355-
356-
357-
NOT_CONFIGURED_INSTRUMENT = "p100"
358-
359-
360-
@pytest.fixture(scope="module")
361-
def mock_numtracker_server() -> Iterable[responses.RequestsMock]:
362-
query_working = {
337+
@pytest.fixture
338+
def nt_query() -> dict[str, str]:
339+
return {
363340
"query": dedent("""
364341
mutation{
365342
scan(
@@ -377,94 +354,11 @@ def mock_numtracker_server() -> Iterable[responses.RequestsMock]:
377354
}
378355
""")
379356
}
380-
query_400 = {
381-
"query": dedent("""
382-
mutation{
383-
scan(
384-
instrument: "p47",
385-
instrumentSession: "ab123"
386-
) {
387-
directory{
388-
instrumentSession
389-
instrument
390-
path
391-
}
392-
scanFile
393-
scanNumber
394-
}
395-
}
396-
""")
397-
}
398-
query_500 = {
399-
"query": dedent("""
400-
mutation{
401-
scan(
402-
instrument: "p48",
403-
instrumentSession: "ab123"
404-
) {
405-
directory{
406-
instrumentSession
407-
instrument
408-
path
409-
}
410-
scanFile
411-
scanNumber
412-
}
413-
}
414-
""")
415-
}
416-
query_key_error = {
417-
"query": dedent("""
418-
mutation{
419-
scan(
420-
instrument: "p49",
421-
instrumentSession: "ab123"
422-
) {
423-
directory{
424-
instrumentSession
425-
instrument
426-
path
427-
}
428-
scanFile
429-
scanNumber
430-
}
431-
}
432-
""")
433-
}
434-
query_200_with_errors = {
435-
"query": dedent(f"""
436-
mutation{{
437-
scan(
438-
instrument: "{NOT_CONFIGURED_INSTRUMENT}",
439-
instrumentSession: "ab123"
440-
) {{
441-
directory{{
442-
instrumentSession
443-
instrument
444-
path
445-
}}
446-
scanFile
447-
scanNumber
448-
}}
449-
}}
450-
""")
451-
}
452357

453-
response_with_errors = {
454-
"data": None,
455-
"errors": [
456-
{
457-
"message": (
458-
"No configuration available for instrument "
459-
f'"{NOT_CONFIGURED_INSTRUMENT}"'
460-
),
461-
"locations": [{"line": 3, "column": 5}],
462-
"path": ["scan"],
463-
}
464-
],
465-
}
466358

467-
working_response = {
359+
@pytest.fixture
360+
def nt_response() -> dict[str, Any]:
361+
return {
468362
"data": {
469363
"scan": {
470364
"scanFile": "p46-11",
@@ -477,42 +371,21 @@ def mock_numtracker_server() -> Iterable[responses.RequestsMock]:
477371
}
478372
}
479373
}
480-
empty_response = {}
481-
482-
with responses.RequestsMock(assert_all_requests_are_fired=False) as requests_mock:
483-
requests_mock.add(
484-
responses.POST,
485-
url="https://numtracker-example.com/graphql",
486-
match=[json_params_matcher(query_working)],
487-
status=200,
488-
json=working_response,
489-
)
490-
requests_mock.add(
491-
responses.POST,
492-
url="https://numtracker-example.com/graphql",
493-
match=[json_params_matcher(query_400)],
494-
status=400,
495-
json=empty_response,
496-
)
497-
requests_mock.add(
498-
responses.POST,
499-
url="https://numtracker-example.com/graphql",
500-
match=[json_params_matcher(query_500)],
501-
status=500,
502-
json=empty_response,
503-
)
504-
requests_mock.add(
505-
responses.POST,
506-
url="https://numtracker-example.com/graphql",
507-
match=[json_params_matcher(query_key_error)],
508-
status=200,
509-
json=empty_response,
510-
)
511-
requests_mock.add(
512-
responses.POST,
513-
"https://numtracker-example.com/graphql",
514-
match=[json_params_matcher(query_200_with_errors)],
515-
status=200,
516-
json=response_with_errors,
517-
)
518-
yield requests_mock
374+
375+
376+
# Prevent pytest from catching exceptions when debugging in vscode so that break on
377+
# exception works correctly (see: https://github.com/pytest-dev/pytest/issues/7409)
378+
if os.getenv("PYTEST_RAISE", "0") == "1":
379+
380+
@pytest.hookimpl(tryfirst=True)
381+
def pytest_exception_interact(call: pytest.CallInfo[Any]):
382+
if call.excinfo is not None:
383+
raise call.excinfo.value
384+
else:
385+
raise RuntimeError(
386+
f"{call} has no exception data, an unknown error has occurred"
387+
)
388+
389+
@pytest.hookimpl(tryfirst=True)
390+
def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]):
391+
raise excinfo.value
Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from pathlib import Path
22

3+
import httpx
34
import pytest
4-
import responses
5-
from requests import HTTPError
6-
from tests.conftest import NOT_CONFIGURED_INSTRUMENT
5+
from pytest_httpx import HTTPXMock
76

87
from blueapi.client.numtracker import (
98
DirectoryPath,
@@ -18,11 +17,33 @@ def numtracker() -> NumtrackerClient:
1817
return NumtrackerClient("https://numtracker-example.com/graphql")
1918

2019

21-
def test_create_scan(
22-
numtracker: NumtrackerClient,
23-
mock_numtracker_server: responses.RequestsMock,
20+
URL = "https://numtracker-example.com/graphql"
21+
22+
EMPTY = {}
23+
24+
ERRORS = {
25+
"data": None,
26+
"errors": [
27+
{
28+
"message": "No configuration available for instrument p46",
29+
"locations": [{"line": 3, "column": 5}],
30+
"path": ["scan"],
31+
}
32+
],
33+
}
34+
35+
36+
async def test_create_scan(
37+
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query, nt_response
2438
):
25-
scan = numtracker.create_scan("ab123", "p46")
39+
httpx_mock.add_response(
40+
method="POST",
41+
url=URL,
42+
match_json=nt_query,
43+
status_code=200,
44+
json=nt_response,
45+
)
46+
scan = await numtracker.create_scan("ab123", "p46")
2647
assert scan == NumtrackerScanMutationResponse(
2748
scan=ScanPaths(
2849
scanFile="p46-11",
@@ -36,42 +57,54 @@ def test_create_scan(
3657
)
3758

3859

39-
def test_create_scan_raises_400_error(
40-
numtracker: NumtrackerClient,
41-
mock_numtracker_server: responses.RequestsMock,
60+
async def test_create_scan_raises_400_error(
61+
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
4262
):
63+
httpx_mock.add_response(
64+
method="POST", url=URL, match_json=nt_query, status_code=400, json=EMPTY
65+
)
4366
with pytest.raises(
44-
HTTPError,
45-
match="400 Client Error: Bad Request for url: https://numtracker-example.com/graphql",
67+
httpx.HTTPStatusError,
68+
match="Client error '400 Bad Request' for url 'https://numtracker-example.com/graphql'",
4669
):
47-
numtracker.create_scan("ab123", "p47")
70+
await numtracker.create_scan("ab123", "p46")
4871

4972

50-
def test_create_scan_raises_500_error(
51-
numtracker: NumtrackerClient,
52-
mock_numtracker_server: responses.RequestsMock,
73+
async def test_create_scan_raises_500_error(
74+
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
5375
):
76+
httpx_mock.add_response(
77+
method="POST", url=URL, match_json=nt_query, status_code=500, json=EMPTY
78+
)
5479
with pytest.raises(
55-
HTTPError,
56-
match="500 Server Error: Internal Server Error for url: https://numtracker-example.com/graphql",
80+
httpx.HTTPStatusError,
81+
match="Server error '500 Internal Server Error' for url 'https://numtracker-example.com/graphql'",
5782
):
58-
numtracker.create_scan("ab123", "p48")
83+
await numtracker.create_scan("ab123", "p46")
5984

6085

61-
def test_create_scan_raises_key_error_on_incorrectly_formatted_responses(
62-
numtracker: NumtrackerClient,
63-
mock_numtracker_server: responses.RequestsMock,
86+
async def test_create_scan_raises_key_error_on_incorrectly_formatted_responses(
87+
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
6488
):
89+
httpx_mock.add_response(
90+
method="POST", url=URL, match_json=nt_query, status_code=200, json=EMPTY
91+
)
6592
with pytest.raises(
6693
KeyError,
6794
match="data",
6895
):
69-
numtracker.create_scan("ab123", "p49")
96+
await numtracker.create_scan("ab123", "p46")
7097

7198

72-
def test_create_scan_raises_runtime_error_on_graphql_error(
73-
numtracker: NumtrackerClient,
74-
mock_numtracker_server: responses.RequestsMock,
99+
async def test_create_scan_raises_runtime_error_on_graphql_error(
100+
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
75101
):
102+
httpx_mock.add_response(
103+
method="POST",
104+
url=URL,
105+
match_json=nt_query,
106+
status_code=200,
107+
json=ERRORS,
108+
)
76109
with pytest.raises(RuntimeError, match="Numtracker error:"):
77-
numtracker.create_scan("ab123", NOT_CONFIGURED_INSTRUMENT)
110+
await numtracker.create_scan("ab123", "p46")

0 commit comments

Comments
 (0)