Skip to content

Commit ba6a7b6

Browse files
committed
Populate firstExperimendId and dewarRegistryId in dewars
1 parent ad7d213 commit ba6a7b6

File tree

5 files changed

+132
-35
lines changed

5 files changed

+132
-35
lines changed

src/scaup/models/top_level_containers.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@
33
from datetime import datetime
44
from typing import Any, List, Optional
55

6-
from pydantic import (
7-
AliasChoices,
8-
BaseModel,
9-
ConfigDict,
10-
Field,
11-
computed_field,
12-
field_validator,
13-
)
6+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator
147

158
from ..utils.models import BaseExternal
169

@@ -59,6 +52,9 @@ class TopLevelContainerExternal(BaseExternal):
5952
comments: str
6053
code: str
6154
barCode: uuid.UUID
55+
firstExperimentId: int | None = None
56+
weight: float = 18
57+
dewarRegistryId: int | None = None
6258

6359
@computed_field
6460
def facilityCode(self) -> str:

src/scaup/utils/crud.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ def edit_item(
5656
inner_db.session.commit()
5757

5858
if updated_item and updated_item.externalId is not None:
59-
ext_obj = ExternalObject(updated_item, item_id)
59+
ext_obj = ExternalObject(token, updated_item, item_id)
6060

6161
ExternalRequest.request(
6262
token,
6363
method="PATCH",
6464
url=f"{ext_obj.external_link_prefix}{updated_item.externalId}",
65-
json=ext_obj.item_body.model_dump(mode="json"),
65+
json=ext_obj.item_body.model_dump(mode="json", exclude=ext_obj.to_exclude),
6666
)
6767

6868
return updated_item

src/scaup/utils/external.py

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import requests
55
from fastapi import HTTPException, status
66
from lims_utils.logging import app_logger
7-
from sqlalchemy import update
7+
from sqlalchemy import func, select, update
88

99
from ..models.containers import ContainerExternal
1010
from ..models.inner_db.tables import (
@@ -31,16 +31,57 @@
3131
}
3232

3333

34+
# TODO: possibly replace this with middleware, or httpx client instances
35+
class ExternalRequest:
36+
@staticmethod
37+
def request(
38+
token,
39+
base_url=Config.ispyb_api.url,
40+
*args,
41+
**kwargs,
42+
):
43+
"""Wrapper for request object. Since the URL is validated before any
44+
auth actions happen, we cannot wrap this in a custom auth implementation,
45+
we must do all the preparation work before the actual request."""
46+
47+
kwargs["url"] = f"{base_url}{kwargs['url']}"
48+
kwargs["method"] = kwargs.get("method", "GET")
49+
kwargs["headers"] = {"Authorization": f"Bearer {token}"}
50+
51+
return requests.request(**kwargs)
52+
53+
54+
def _get_resource_from_ispyb(token: str, url: str):
55+
response = ExternalRequest.request(token, url=url)
56+
57+
if response.status_code != 200:
58+
app_logger.error(
59+
(
60+
f"Failed getting session information from ISPyB at URL {url}, service returned "
61+
f"{response.status_code}: {response.text}"
62+
)
63+
)
64+
65+
raise HTTPException(
66+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
67+
detail="Received invalid response from upstream service",
68+
)
69+
70+
return response.json()
71+
72+
3473
class ExternalObject:
3574
"""Object representing a link to the ISPyB instance of the object"""
3675

3776
item_body: OrmBaseModel = OrmBaseModel()
3877
external_link_prefix = ""
3978
external_key = ""
4079
url = ""
80+
to_exclude: set[str] = set()
4181

4282
def __init__(
4383
self,
84+
token: str,
4485
item: AvailableTable,
4586
item_id: int | str | None,
4687
root_id: int | None = None,
@@ -66,6 +107,36 @@ def __init__(
66107
self.url = f"/shipments/{item_id}/dewars"
67108
self.external_link_prefix = "/dewars/"
68109
self.item_body = TopLevelContainerExternal.model_validate(item)
110+
111+
shipment = inner_db.session.execute(
112+
select(
113+
func.concat(Shipment.proposalCode, Shipment.proposalNumber).label("proposal"),
114+
Shipment.visitNumber,
115+
).filter(Shipment.id == item.shipmentId)
116+
).one()
117+
118+
# When creating the dewar in ISPyB, since ISPyB has no concept of shipments belonging to sessions,
119+
# dewars have to be assigned to sessions instead, and this is done through the firstExperimentId
120+
# column, which despite the cryptic name, points to the BLSession table.
121+
if item.externalId is None:
122+
session = _get_resource_from_ispyb(
123+
token,
124+
f"/proposals/{shipment.proposal}/sessions/{shipment.visitNumber}",
125+
)
126+
self.item_body.firstExperimentId = session["sessionId"]
127+
else:
128+
self.to_exclude = {"firstExperimentId"}
129+
130+
# We store the dewar's facility code, but not the numeric dewar registry ID that ISPyB also expects.
131+
# Even though the alphanumeric code is a primary key in the DewarRegistry table, the dewar table still
132+
# expects a numeric dewarRegistryId which is used in some systems.
133+
# Since the facility code can be changed by the user, we need to update this even if it was already
134+
# pushed to ISPyB
135+
dewar_reg = _get_resource_from_ispyb(
136+
token, f"/proposals/{shipment.proposal}/dewar-registry/{item.code}"
137+
)
138+
139+
self.item_body.dewarRegistryId = dewar_reg["dewarRegistryId"]
69140
self.external_key = "dewarId"
70141
case Sample():
71142
if item_id is None:
@@ -80,26 +151,6 @@ def __init__(
80151
raise NotImplementedError()
81152

82153

83-
# TODO: possibly replace this with middleware, or httpx client instances
84-
class ExternalRequest:
85-
@staticmethod
86-
def request(
87-
token,
88-
base_url=Config.ispyb_api.url,
89-
*args,
90-
**kwargs,
91-
):
92-
"""Wrapper for request object. Since the URL is validated before any
93-
auth actions happen, we cannot wrap this in a custom auth implementation,
94-
we must do all the preparation work before the actual request."""
95-
96-
kwargs["url"] = f"{base_url}{kwargs['url']}"
97-
kwargs["method"] = kwargs.get("method", "GET")
98-
kwargs["headers"] = {"Authorization": f"Bearer {token}"}
99-
100-
return requests.request(**kwargs)
101-
102-
103154
class Expeye:
104155
@classmethod
105156
def upsert(
@@ -119,7 +170,7 @@ def upsert(
119170
Returns:
120171
External link and external ID"""
121172

122-
ext_obj = ExternalObject(item, parent_id, root_id)
173+
ext_obj = ExternalObject(token, item, parent_id, root_id)
123174
method = "POST"
124175

125176
if item.externalId:
@@ -130,7 +181,7 @@ def upsert(
130181
token,
131182
method=method,
132183
url=ext_obj.url,
133-
json=ext_obj.item_body.model_dump(mode="json"),
184+
json=ext_obj.item_body.model_dump(mode="json", exclude=ext_obj.to_exclude),
134185
)
135186

136187
if response.status_code not in [201, 200]:

tests/shipments/top_level_containers/responses.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ def registered_dewar_callback(request: PreparedRequest):
99
# Return valid response for dewar with facility code DLS-EM-0000, return 404 for all else
1010
dewar_id = get_match(registered_dewar_regex, request.url, 2)
1111

12-
if dewar_id == "DLS-EM-0000" or dewar_id == "DLS-EM-0001":
13-
return (200, {}, json.dumps({}))
12+
if dewar_id in ["DLS-EM-0000", "DLS-EM-0001", "DLS-4", "DLS-1"]:
13+
return (200, {}, json.dumps({"dewarRegistryId": 456}))
1414

1515
return (404, {}, "")
1616

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import copy
2+
from datetime import datetime
3+
from uuid import UUID
4+
5+
import pytest
6+
import responses
7+
from fastapi import HTTPException
8+
9+
from scaup.models.inner_db.tables import TopLevelContainer
10+
from scaup.utils.config import Config
11+
from scaup.utils.external import ExternalObject, _get_resource_from_ispyb
12+
13+
base_dewar = TopLevelContainer(
14+
code="DLS-EM-0001",
15+
id=1,
16+
shipmentId=1,
17+
type="dewar",
18+
name="Test_Dewar",
19+
barCode=UUID(bytes=b"ffffffffffffffff"),
20+
creationDate=datetime.now(),
21+
)
22+
23+
24+
@responses.activate
25+
def test_new_top_level_container(client):
26+
"""Should get session information from ISPyB and populate it in dewar before pushing it"""
27+
dewar = ExternalObject("token", base_dewar, 1, 5)
28+
29+
assert dewar.item_body.firstExperimentId == 1
30+
assert dewar.item_body.dewarRegistryId == 456
31+
32+
@responses.activate
33+
def test_update_top_level_container(client):
34+
"""Should not get session ID if item has already been pushed to ISPyB"""
35+
external_dewar = copy.deepcopy(base_dewar)
36+
external_dewar.externalId = 1
37+
dewar = ExternalObject("token", external_dewar, 1, 5)
38+
39+
assert dewar.item_body.firstExperimentId is None
40+
assert dewar.to_exclude == {"firstExperimentId"}
41+
42+
@responses.activate
43+
def test_upstream_request_fail():
44+
"""Should raise exception if Expeye returns invalid response"""
45+
responses.get(
46+
f"{Config.ispyb_api.url}/foo", status=404
47+
)
48+
49+
with pytest.raises(HTTPException, match="Received invalid response from upstream service"):
50+
_get_resource_from_ispyb("token", "/foo")

0 commit comments

Comments
 (0)