Skip to content

Commit ec2ac38

Browse files
adds support for automated observations and care authn. (#21)
Co-authored-by: Aakash Singh <mail@singhaakash.dev>
1 parent fb9feb9 commit ec2ac38

File tree

14 files changed

+492
-246
lines changed

14 files changed

+492
-246
lines changed

common/authentication.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ class CareAuthentication(JWTAuthentication):
4848
token provided in a request header.
4949
"""
5050

51-
facility_header = "X-Facility-Id"
5251
auth_header_type = "Care_Bearer"
5352
auth_header_type_bytes = auth_header_type.encode(HTTP_HEADER_ENCODING)
5453

@@ -82,7 +81,7 @@ def authenticate(self, request):
8281
if raw_token is None:
8382
return None
8483

85-
open_id_url = settings.CARE_JWK_URL
84+
open_id_url = f"{settings.CARE_API}/api/gateway_device/jwks.json/"
8685
validated_token = self.get_validated_token(open_id_url, raw_token)
8786

8887
return self.get_user(validated_token), validated_token

core/settings.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,9 @@
205205
CELERY_BROKER_URL = env("CELERY_BROKER_URL", default=REDIS_URL)
206206

207207
# Configs
208-
CARE_URL = env("CARE_URL")
209208
CARE_API = env("CARE_API")
210-
FACILITY_ID = env("FACILITY_ID")
211-
CARE_JWK_URL = env("CARE_JWK_URL")
212-
CARE_VERIFY_TOKEN_URL = env("CARE_VERIFY_TOKEN_URL")
213-
209+
CARE_API_TIMEOUT = env.int("CARE_API_TIMEOUT", default=25)
210+
GATEWAY_DEVICE_ID = env("GATEWAY_DEVICE_ID")
214211

215212
JWKS = JsonWebKey.import_key_set(
216213
json.loads(base64.b64decode(env("JWKS_BASE64", default=generate_encoded_jwks())))
@@ -225,7 +222,7 @@
225222

226223
# Observations
227224
REDIS_OBSERVATIONS_KEY = "observations"
228-
UPDATE_INTERVAL = env.int("UPDATE_INTERVAL", default=60)
225+
AUTOMATED_OBSERVATIONS_INTERVAL = env.int("AUTOMATED_OBSERVATIONS_INTERVAL", default=60)
229226

230227

231228
# Cameras

core/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
path("admin/", admin.site.urls),
3333
path("", home, name="home"),
3434
path("", include(router.urls)),
35-
path(".well-known/openid-configuration/", PublicJWKsView.as_view()),
35+
path("openid-configuration/", PublicJWKsView.as_view()),
3636
path("", include("middleware.observation.urls")),
3737
path("", include("middleware.camera.urls")),
3838
path("", include("middleware.stream.urls")),

docker-compose.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@ services:
66
- ./.env
77
volumes:
88
- postgres-data:/var/lib/postgresql/data
9-
ports:
10-
- "5432:5432"
119

1210
redis:
1311
image: redis:7.2
1412
restart: unless-stopped
15-
ports:
16-
- "6379:6379"
1713

1814
# rtsptoweb:
1915
stream-server:
@@ -24,7 +20,6 @@ services:
2420
ports:
2521
- "8080:8080"
2622

27-
2823
# teleicu-middleware
2924
teleicu-middleware:
3025
restart: unless-stopped

middleware/care_client.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import json
2+
import logging
3+
4+
import requests
5+
from django.conf import settings
6+
from django.http import HttpResponse
7+
from rest_framework import status
8+
from rest_framework.exceptions import APIException
9+
10+
from middleware.utils import generate_jwt
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class CareClient:
16+
auth_header_type = "Gateway_Bearer"
17+
18+
def __init__(self):
19+
self.base_url = settings.CARE_API
20+
self.timeout = settings.CARE_API_TIMEOUT
21+
22+
def _get_url(self, endpoint):
23+
return f"{self.base_url}{endpoint}"
24+
25+
def _get_headers(self):
26+
return {
27+
"Authorization": f"{self.auth_header_type} {generate_jwt()}",
28+
"X-Gateway-Id": settings.GATEWAY_DEVICE_ID,
29+
"Accept": "application/json",
30+
}
31+
32+
def _make_request(
33+
self, method: str, url: str, as_http_response=False, **request_kwargs
34+
) -> HttpResponse | dict:
35+
"""
36+
Execute the HTTP request and validate the response.
37+
38+
Args:
39+
method: HTTP method (GET, POST, etc.)
40+
url: Full URL to request
41+
as_http_response: If True, return HttpResponse object instead of JSON
42+
**request_kwargs: Additional arguments to pass to requests method
43+
44+
Returns:
45+
HttpResponse or dict depending on as_http_response flag
46+
47+
Raises:
48+
APIException: For all request failures with appropriate error messages
49+
"""
50+
try:
51+
response = requests.request(
52+
method, url, timeout=self.timeout, **request_kwargs
53+
)
54+
55+
# Handle response based on format requested
56+
if as_http_response:
57+
return HttpResponse(
58+
response.content,
59+
content_type=response.headers.get(
60+
"content-type", "application/json"
61+
),
62+
status=response.status_code,
63+
)
64+
65+
if response.status_code >= status.HTTP_400_BAD_REQUEST:
66+
raise APIException(response.text, response.status_code)
67+
68+
return response.json()
69+
70+
except requests.Timeout as e:
71+
raise APIException(
72+
{"error": f"Request timed out after {self.timeout} seconds"},
73+
status.HTTP_504_GATEWAY_TIMEOUT,
74+
) from e
75+
except (
76+
requests.ConnectionError,
77+
requests.exceptions.SSLError,
78+
requests.exceptions.TooManyRedirects,
79+
requests.RequestException,
80+
) as e:
81+
logger.error(f"Gateway connection error: {str(e)}")
82+
raise APIException(
83+
{"error": "Failed to connect to gateway device"},
84+
status.HTTP_503_SERVICE_UNAVAILABLE,
85+
) from e
86+
except json.decoder.JSONDecodeError as e:
87+
raise APIException(
88+
{"error": "Invalid JSON response from gateway device"},
89+
status.HTTP_502_BAD_GATEWAY,
90+
) from e
91+
except Exception as e:
92+
logger.error(f"Unexpected error during gateway request: {str(e)}")
93+
raise APIException(
94+
{"error": "An unexpected error occurred during gateway request"},
95+
status.HTTP_500_INTERNAL_SERVER_ERROR,
96+
) from e
97+
98+
def get(self, endpoint, data=None, as_http_response=False):
99+
url = self._get_url(endpoint)
100+
return self._make_request(
101+
"GET",
102+
url,
103+
as_http_response=as_http_response,
104+
params=data,
105+
headers=self._get_headers(),
106+
)
107+
108+
def post(self, endpoint, data=None, as_http_response=False):
109+
url = self._get_url(endpoint)
110+
return self._make_request(
111+
"POST",
112+
url,
113+
as_http_response=as_http_response,
114+
json=data,
115+
headers=self._get_headers(),
116+
)

middleware/observation/types.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,63 @@ class ObservationID(str, Enum):
2424
WAVEFORM_RESPIRATION = "waveform_Respiration"
2525

2626

27+
class Coding(BaseModel):
28+
"""Represents a code from a code system"""
29+
30+
system: str | None = None
31+
version: str | None = None
32+
code: str
33+
display: str | None = None
34+
35+
36+
class ObservationValueType(str, Enum):
37+
boolean = "boolean"
38+
decimal = "decimal"
39+
integer = "integer"
40+
string = "string"
41+
date = "date"
42+
datetime = "dateTime"
43+
time = "time"
44+
45+
46+
class ObservationValue(BaseModel):
47+
value: str | None = None
48+
unit: Coding | None = None
49+
50+
51+
class ObservationStatus(str, Enum):
52+
final = "final"
53+
amended = "amended"
54+
entered_in_error = "entered_in_error"
55+
56+
57+
VitalSignsCoding = Coding(
58+
code="vital-signs",
59+
system="http://terminology.hl7.org/CodeSystem/observation-category",
60+
display="Vital Signs",
61+
)
62+
63+
64+
class ReferenceRange(BaseModel):
65+
min: float | None = None
66+
max: float | None = None
67+
unit: str | None = None
68+
interpretation: str
69+
value: str | None = None
70+
71+
72+
class ObservationWriteSpec(BaseModel):
73+
status: ObservationStatus = ObservationStatus.final
74+
category: Coding = VitalSignsCoding
75+
main_code: Coding
76+
effective_datetime: datetime
77+
value_type: ObservationValueType
78+
value: ObservationValue
79+
note: str | None = None
80+
reference_range: list[ReferenceRange] = []
81+
interpretation: str | None = None
82+
83+
2784
class Status(str, Enum):
2885
FINAL = "final"
2986
LEADS_OFF = "Message-Leads Off"
@@ -60,6 +117,7 @@ class WaveName(str, Enum):
60117
PLETH = "Pleth"
61118
RESPIRATION = "Respiration"
62119

120+
63121
class BloodPressure(BaseModel):
64122
value: Optional[float] = None
65123
unit: Optional[str] = None

0 commit comments

Comments
 (0)