Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
"observability-utils>=0.1.4",
"pyjwt[crypto]",
"tomlkit",
"httpx>=0.28.1",
]
dynamic = ["version"]
license.file = "LICENSE"
Expand All @@ -51,6 +52,7 @@ dev = [
"pyright",
"pytest-cov",
"pytest-asyncio",
"pytest-httpx>=0.35.0",
"responses",
"ruff",
"semver",
Expand Down
15 changes: 8 additions & 7 deletions src/blueapi/client/numtracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path
from textwrap import dedent

import requests
import httpx
from pydantic import Field

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

self._headers = headers

def create_scan(
async def create_scan(
self, instrument_session: str, instrument: str
) -> NumtrackerScanMutationResponse:
"""
Expand Down Expand Up @@ -92,11 +92,12 @@ def create_scan(
""")
}

response = requests.post(
self._url,
headers=self._headers,
json=query,
)
async with httpx.AsyncClient() as client:
response = await client.post(
self._url,
headers=self._headers,
json=query,
)

response.raise_for_status()
json = response.json()
Expand Down
4 changes: 2 additions & 2 deletions src/blueapi/service/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ def numtracker_client() -> NumtrackerClient | None:
return None


def _update_scan_num(md: dict[str, Any]) -> int:
async def _update_scan_num(md: dict[str, Any]) -> int:
numtracker = numtracker_client()
if numtracker is not None:
scan = numtracker.create_scan(md["instrument_session"], md["instrument"])
scan = await numtracker.create_scan(md["instrument_session"], md["instrument"])
md["data_session_directory"] = str(scan.scan.directory.path)
return scan.scan.scan_number
else:
Expand Down
139 changes: 6 additions & 133 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio
import base64
import time
from collections.abc import Iterable
from pathlib import Path
from textwrap import dedent
from typing import Any, cast
Expand All @@ -20,7 +19,6 @@
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.trace import get_tracer_provider
from responses.matchers import json_params_matcher

from blueapi.config import ApplicationConfig, OIDCConfig
from blueapi.service.model import Cache
Expand Down Expand Up @@ -335,12 +333,9 @@ def mock_jwks_fetch(json_web_keyset: JWK):
return patch("jwt.PyJWKClient.fetch_data", mock)


NOT_CONFIGURED_INSTRUMENT = "p100"


@pytest.fixture(scope="module")
def mock_numtracker_server() -> Iterable[responses.RequestsMock]:
query_working = {
@pytest.fixture
def nt_query() -> dict[str, str]:
return {
"query": dedent("""
mutation{
scan(
Expand All @@ -358,94 +353,11 @@ def mock_numtracker_server() -> Iterable[responses.RequestsMock]:
}
""")
}
query_400 = {
"query": dedent("""
mutation{
scan(
instrument: "p47",
instrumentSession: "ab123"
) {
directory{
instrumentSession
instrument
path
}
scanFile
scanNumber
}
}
""")
}
query_500 = {
"query": dedent("""
mutation{
scan(
instrument: "p48",
instrumentSession: "ab123"
) {
directory{
instrumentSession
instrument
path
}
scanFile
scanNumber
}
}
""")
}
query_key_error = {
"query": dedent("""
mutation{
scan(
instrument: "p49",
instrumentSession: "ab123"
) {
directory{
instrumentSession
instrument
path
}
scanFile
scanNumber
}
}
""")
}
query_200_with_errors = {
"query": dedent(f"""
mutation{{
scan(
instrument: "{NOT_CONFIGURED_INSTRUMENT}",
instrumentSession: "ab123"
) {{
directory{{
instrumentSession
instrument
path
}}
scanFile
scanNumber
}}
}}
""")
}

response_with_errors = {
"data": None,
"errors": [
{
"message": (
"No configuration available for instrument "
f'"{NOT_CONFIGURED_INSTRUMENT}"'
),
"locations": [{"line": 3, "column": 5}],
"path": ["scan"],
}
],
}

working_response = {
@pytest.fixture
def nt_response() -> dict[str, Any]:
return {
"data": {
"scan": {
"scanFile": "p46-11",
Expand All @@ -458,42 +370,3 @@ def mock_numtracker_server() -> Iterable[responses.RequestsMock]:
}
}
}
empty_response = {}

with responses.RequestsMock(assert_all_requests_are_fired=False) as requests_mock:
requests_mock.add(
responses.POST,
url="https://numtracker-example.com/graphql",
match=[json_params_matcher(query_working)],
status=200,
json=working_response,
)
requests_mock.add(
responses.POST,
url="https://numtracker-example.com/graphql",
match=[json_params_matcher(query_400)],
status=400,
json=empty_response,
)
requests_mock.add(
responses.POST,
url="https://numtracker-example.com/graphql",
match=[json_params_matcher(query_500)],
status=500,
json=empty_response,
)
requests_mock.add(
responses.POST,
url="https://numtracker-example.com/graphql",
match=[json_params_matcher(query_key_error)],
status=200,
json=empty_response,
)
requests_mock.add(
responses.POST,
"https://numtracker-example.com/graphql",
match=[json_params_matcher(query_200_with_errors)],
status=200,
json=response_with_errors,
)
yield requests_mock
87 changes: 60 additions & 27 deletions tests/unit_tests/client/test_numtracker.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from pathlib import Path

import httpx
import pytest
import responses
from requests import HTTPError
from tests.conftest import NOT_CONFIGURED_INSTRUMENT
from pytest_httpx import HTTPXMock

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


def test_create_scan(
numtracker: NumtrackerClient,
mock_numtracker_server: responses.RequestsMock,
URL = "https://numtracker-example.com/graphql"

EMPTY = {}

ERRORS = {
"data": None,
"errors": [
{
"message": "No configuration available for instrument p46",
"locations": [{"line": 3, "column": 5}],
"path": ["scan"],
}
],
}


async def test_create_scan(
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query, nt_response
):
scan = numtracker.create_scan("ab123", "p46")
httpx_mock.add_response(
method="POST",
url=URL,
match_json=nt_query,
status_code=200,
json=nt_response,
)
scan = await numtracker.create_scan("ab123", "p46")
assert scan == NumtrackerScanMutationResponse(
scan=ScanPaths(
scanFile="p46-11",
Expand All @@ -36,42 +57,54 @@ def test_create_scan(
)


def test_create_scan_raises_400_error(
numtracker: NumtrackerClient,
mock_numtracker_server: responses.RequestsMock,
async def test_create_scan_raises_400_error(
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
):
httpx_mock.add_response(
method="POST", url=URL, match_json=nt_query, status_code=400, json=EMPTY
)
with pytest.raises(
HTTPError,
match="400 Client Error: Bad Request for url: https://numtracker-example.com/graphql",
httpx.HTTPStatusError,
match="Client error '400 Bad Request' for url 'https://numtracker-example.com/graphql'",
):
numtracker.create_scan("ab123", "p47")
await numtracker.create_scan("ab123", "p46")


def test_create_scan_raises_500_error(
numtracker: NumtrackerClient,
mock_numtracker_server: responses.RequestsMock,
async def test_create_scan_raises_500_error(
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
):
httpx_mock.add_response(
method="POST", url=URL, match_json=nt_query, status_code=500, json=EMPTY
)
with pytest.raises(
HTTPError,
match="500 Server Error: Internal Server Error for url: https://numtracker-example.com/graphql",
httpx.HTTPStatusError,
match="Server error '500 Internal Server Error' for url 'https://numtracker-example.com/graphql'",
):
numtracker.create_scan("ab123", "p48")
await numtracker.create_scan("ab123", "p46")


def test_create_scan_raises_key_error_on_incorrectly_formatted_responses(
numtracker: NumtrackerClient,
mock_numtracker_server: responses.RequestsMock,
async def test_create_scan_raises_key_error_on_incorrectly_formatted_responses(
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
):
httpx_mock.add_response(
method="POST", url=URL, match_json=nt_query, status_code=200, json=EMPTY
)
with pytest.raises(
KeyError,
match="data",
):
numtracker.create_scan("ab123", "p49")
await numtracker.create_scan("ab123", "p46")


def test_create_scan_raises_runtime_error_on_graphql_error(
numtracker: NumtrackerClient,
mock_numtracker_server: responses.RequestsMock,
async def test_create_scan_raises_runtime_error_on_graphql_error(
numtracker: NumtrackerClient, httpx_mock: HTTPXMock, nt_query
):
httpx_mock.add_response(
method="POST",
url=URL,
match_json=nt_query,
status_code=200,
json=ERRORS,
)
with pytest.raises(RuntimeError, match="Numtracker error:"):
numtracker.create_scan("ab123", NOT_CONFIGURED_INSTRUMENT)
await numtracker.create_scan("ab123", "p46")
Loading
Loading