endoreg-db 0.8.3.3__py3-none-any.whl → 0.8.6.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +23 -1
- endoreg_db/data/setup_config.yaml +38 -0
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +1 -2
- endoreg_db/management/commands/load_ai_model_data.py +18 -15
- endoreg_db/management/commands/setup_endoreg_db.py +218 -33
- endoreg_db/models/media/pdf/raw_pdf.py +241 -97
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/video_file.py +300 -187
- endoreg_db/models/medical/hardware/endoscopy_processor.py +10 -1
- endoreg_db/models/metadata/model_meta_logic.py +34 -45
- endoreg_db/models/metadata/sensitive_meta_logic.py +555 -150
- endoreg_db/serializers/__init__.py +26 -55
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +65 -35
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/lookup_service.py +228 -58
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +585 -282
- endoreg_db/services/video_import.py +493 -240
- endoreg_db/urls/__init__.py +36 -23
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +103 -66
- endoreg_db/utils/setup_config.py +177 -0
- endoreg_db/views/__init__.py +5 -3
- endoreg_db/views/media/pdf_media.py +3 -1
- endoreg_db/views/media/video_media.py +1 -1
- endoreg_db/views/media/video_segments.py +187 -259
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +186 -0
- endoreg_db/views/pdf/reimport.py +110 -94
- endoreg_db/views/requirement/lookup.py +171 -287
- endoreg_db/views/video/__init__.py +0 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/METADATA +1 -2
- {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/RECORD +38 -37
- endoreg_db/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/video/video_media.py +0 -158
- {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,483 +1,367 @@
|
|
|
1
1
|
# api/viewsets/lookup.py
|
|
2
|
+
import logging
|
|
3
|
+
from ast import literal_eval
|
|
4
|
+
from collections.abc import Mapping
|
|
2
5
|
|
|
3
|
-
from
|
|
4
|
-
|
|
6
|
+
from django.core.cache import cache
|
|
7
|
+
from rest_framework import status, viewsets
|
|
5
8
|
from rest_framework.decorators import action
|
|
6
|
-
|
|
9
|
+
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
|
7
10
|
from rest_framework.response import Response
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
from rest_framework.parsers import JSONParser, FormParser, MultiPartParser
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from endoreg_db.services.lookup_store import LookupStore, DEFAULT_TTL_SECONDS
|
|
14
|
-
|
|
15
|
-
|
|
16
12
|
# Use module import so tests can monkeypatch functions on the module
|
|
17
|
-
|
|
18
|
-
|
|
19
13
|
from endoreg_db.services import lookup_service as ls
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
from endoreg_db.services.lookup_store import DEFAULT_TTL_SECONDS, LookupStore
|
|
22
15
|
from endoreg_db.utils.permissions import EnvironmentAwarePermission
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
from django.core.cache import cache
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import logging
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
from ast import literal_eval
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
from collections.abc import Mapping
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
17
|
ORIGIN_MAP_PREFIX = "lookup:origin:"
|
|
41
|
-
|
|
42
|
-
|
|
43
18
|
ISSUED_MAP_PREFIX = "lookup:issued_for_internal:"
|
|
44
19
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
20
|
logger = logging.getLogger(__name__)
|
|
50
21
|
|
|
51
22
|
|
|
52
|
-
|
|
53
23
|
class LookupViewSet(viewsets.ViewSet):
|
|
54
|
-
|
|
24
|
+
"""
|
|
25
|
+
Django REST Framework ViewSet for managing lookup sessions.
|
|
26
|
+
|
|
27
|
+
This ViewSet provides REST API endpoints for the lookup system, which
|
|
28
|
+
evaluates medical examination requirements against patient data. It uses
|
|
29
|
+
token-based sessions stored in Django cache to maintain state across
|
|
30
|
+
multiple client requests.
|
|
31
|
+
|
|
32
|
+
Key features:
|
|
33
|
+
- Session initialization with patient examination data
|
|
34
|
+
- Retrieval of lookup data by token
|
|
35
|
+
- Partial updates to session data with automatic recomputation
|
|
36
|
+
- Manual recomputation of derived data
|
|
37
|
+
- Automatic session recovery for expired tokens
|
|
38
|
+
|
|
39
|
+
The API supports both internal service tokens and public client tokens,
|
|
40
|
+
with origin mapping to enable session restart functionality.
|
|
41
|
+
|
|
42
|
+
Endpoints:
|
|
43
|
+
- POST /init: Initialize new lookup session
|
|
44
|
+
- GET /{token}/all: Retrieve complete session data
|
|
45
|
+
- GET/PATCH /{token}/parts: Get/update partial session data
|
|
46
|
+
- POST /{token}/recompute: Manually trigger recomputation
|
|
47
|
+
"""
|
|
55
48
|
|
|
56
49
|
permission_classes = [EnvironmentAwarePermission]
|
|
57
|
-
|
|
58
|
-
|
|
59
50
|
parser_classes = (JSONParser, FormParser, MultiPartParser)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
INPUT_KEYS = {"patient_examination_id", "selectedRequirementSetIds", "selectedChoices"}
|
|
70
|
-
|
|
71
|
-
|
|
51
|
+
INPUT_KEYS = {
|
|
52
|
+
"patient_examination_id",
|
|
53
|
+
"selectedRequirementSetIds",
|
|
54
|
+
"selectedChoices",
|
|
55
|
+
}
|
|
72
56
|
|
|
73
57
|
@action(detail=False, methods=["post"])
|
|
74
|
-
|
|
75
58
|
def init(self, request):
|
|
59
|
+
"""
|
|
60
|
+
Initialize a new lookup session for a patient examination.
|
|
76
61
|
|
|
62
|
+
Creates a new token-based session containing initial lookup data
|
|
63
|
+
for the specified patient examination. Handles malformed payloads
|
|
64
|
+
and supports multiple initialization requests for the same examination
|
|
65
|
+
by issuing fresh public tokens while maintaining internal state.
|
|
77
66
|
|
|
67
|
+
Request body:
|
|
68
|
+
patient_examination_id: Integer ID of the patient examination
|
|
78
69
|
|
|
70
|
+
Returns:
|
|
71
|
+
JSON response with session token
|
|
79
72
|
|
|
73
|
+
Raises:
|
|
74
|
+
400: Invalid patient_examination_id or creation failure
|
|
75
|
+
"""
|
|
80
76
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
debug_data = getattr(request, 'data', None)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
raw_post = getattr(getattr(request, '_request', None), 'POST', None)
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
debug_data = getattr(request, "data", None)
|
|
78
|
+
raw_post = getattr(getattr(request, "_request", None), "POST", None)
|
|
89
79
|
body_preview = None
|
|
90
|
-
|
|
91
|
-
|
|
92
80
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
body = getattr(getattr(request, '_request', None), 'body', b'')
|
|
96
|
-
|
|
97
|
-
|
|
81
|
+
body = getattr(getattr(request, "_request", None), "body", b"")
|
|
98
82
|
body_preview = body[:200]
|
|
99
83
|
|
|
100
|
-
|
|
101
84
|
except Exception:
|
|
102
|
-
|
|
103
|
-
|
|
104
85
|
body_preview = None
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
86
|
+
logger.debug(
|
|
87
|
+
"lookup.init incoming: data=%r POST=%r body[:200]=%r",
|
|
88
|
+
debug_data,
|
|
89
|
+
raw_post,
|
|
90
|
+
body_preview,
|
|
91
|
+
)
|
|
109
92
|
|
|
110
93
|
except Exception:
|
|
111
|
-
|
|
112
|
-
|
|
113
94
|
pass
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
95
|
# Prefer DRF data
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
96
|
+
raw_pe = (
|
|
97
|
+
request.data.get("patient_examination_id")
|
|
98
|
+
if hasattr(request, "data")
|
|
99
|
+
else None
|
|
100
|
+
)
|
|
128
101
|
# Fallback: parse malformed form payload where the entire dict was sent as a single key string
|
|
129
102
|
|
|
130
|
-
|
|
131
103
|
if raw_pe is None:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
104
|
+
for candidate in (
|
|
105
|
+
getattr(getattr(request, "_request", None), "POST", None),
|
|
106
|
+
request.data if hasattr(request, "data") else None,
|
|
107
|
+
):
|
|
137
108
|
try:
|
|
138
|
-
|
|
139
|
-
|
|
140
109
|
if isinstance(candidate, Mapping) and len(candidate.keys()) == 1:
|
|
141
|
-
|
|
142
|
-
|
|
143
110
|
only_key = next(iter(candidate.keys()))
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
111
|
+
if (
|
|
112
|
+
isinstance(only_key, str)
|
|
113
|
+
and only_key.startswith("{")
|
|
114
|
+
and only_key.endswith("}")
|
|
115
|
+
):
|
|
149
116
|
try:
|
|
150
|
-
|
|
151
|
-
|
|
152
117
|
parsed = literal_eval(only_key)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
118
|
+
if (
|
|
119
|
+
isinstance(parsed, dict)
|
|
120
|
+
and "patient_examination_id" in parsed
|
|
121
|
+
):
|
|
122
|
+
raw_pe = parsed.get("patient_examination_id")
|
|
123
|
+
logger.debug(
|
|
124
|
+
"lookup.init recovered pe_id from malformed payload: %r",
|
|
125
|
+
raw_pe,
|
|
126
|
+
)
|
|
164
127
|
break
|
|
165
128
|
|
|
166
|
-
|
|
167
129
|
except Exception:
|
|
168
|
-
|
|
169
|
-
|
|
170
130
|
pass
|
|
171
131
|
|
|
172
|
-
|
|
173
132
|
except Exception:
|
|
174
|
-
|
|
175
|
-
|
|
176
133
|
pass
|
|
177
134
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
135
|
# Fallback to query params
|
|
183
|
-
|
|
184
|
-
|
|
185
136
|
if raw_pe is None:
|
|
186
|
-
|
|
187
|
-
|
|
188
137
|
raw_pe = request.query_params.get("patient_examination_id")
|
|
189
138
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
139
|
logger.debug("lookup.init raw_pe=%r type=%s", raw_pe, type(raw_pe))
|
|
195
140
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
141
|
# Normalize potential list/tuple inputs (e.g., from form submissions)
|
|
201
|
-
|
|
202
|
-
|
|
203
142
|
if isinstance(raw_pe, (list, tuple)):
|
|
204
|
-
|
|
205
|
-
|
|
206
143
|
raw_pe = raw_pe[0] if raw_pe else None
|
|
207
144
|
|
|
208
|
-
|
|
209
145
|
if raw_pe in (None, ""):
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
146
|
+
return Response(
|
|
147
|
+
{"detail": "patient_examination_id must be an integer"},
|
|
148
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
149
|
+
)
|
|
217
150
|
|
|
218
151
|
# Coerce to int robustly
|
|
219
|
-
|
|
220
|
-
|
|
221
152
|
try:
|
|
222
|
-
|
|
223
|
-
|
|
224
153
|
pe_id = int(str(raw_pe))
|
|
225
154
|
|
|
226
|
-
|
|
227
155
|
except (TypeError, ValueError):
|
|
228
|
-
|
|
229
|
-
|
|
230
156
|
logger.warning("lookup.init failed to int() raw_pe=%r", raw_pe)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
157
|
+
return Response(
|
|
158
|
+
{"detail": "patient_examination_id must be an integer"},
|
|
159
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
160
|
+
)
|
|
235
161
|
|
|
236
162
|
if pe_id <= 0:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
163
|
+
return Response(
|
|
164
|
+
{"detail": "patient_examination_id must be positive"},
|
|
165
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
166
|
+
)
|
|
244
167
|
|
|
245
168
|
try:
|
|
246
|
-
|
|
247
|
-
|
|
248
169
|
# Create internal session via service (may seed its own token/cache)
|
|
249
|
-
|
|
250
|
-
|
|
251
170
|
internal_token = ls.create_lookup_token_for_pe(pe_id)
|
|
252
|
-
|
|
253
|
-
|
|
254
171
|
internal_data = LookupStore(token=internal_token).get_all()
|
|
255
172
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
173
|
issued_key = f"{ISSUED_MAP_PREFIX}{internal_token}"
|
|
261
174
|
|
|
262
|
-
|
|
263
175
|
issued_count = cache.get(issued_key, 0)
|
|
264
176
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
177
|
if issued_count == 0:
|
|
270
|
-
|
|
271
|
-
|
|
272
178
|
# First issuance: return the service token directly
|
|
273
179
|
|
|
274
|
-
|
|
275
180
|
token_to_return = internal_token
|
|
276
181
|
|
|
277
|
-
|
|
278
182
|
cache.set(issued_key, 1, DEFAULT_TTL_SECONDS)
|
|
279
183
|
|
|
280
|
-
|
|
281
184
|
else:
|
|
282
|
-
|
|
283
|
-
|
|
284
185
|
# Subsequent inits for same internal token: issue a fresh public token seeded with internal data
|
|
285
186
|
|
|
286
|
-
|
|
287
187
|
public_store = LookupStore()
|
|
288
188
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
189
|
+
token_to_return = public_store.init(
|
|
190
|
+
initial=internal_data, ttl=DEFAULT_TTL_SECONDS
|
|
191
|
+
)
|
|
292
192
|
|
|
293
193
|
cache.set(issued_key, issued_count + 1, DEFAULT_TTL_SECONDS)
|
|
294
194
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
195
|
# Persist origin mapping so we can restart expired sessions
|
|
300
196
|
|
|
301
|
-
|
|
302
|
-
|
|
197
|
+
cache.set(
|
|
198
|
+
f"{ORIGIN_MAP_PREFIX}{token_to_return}", pe_id, DEFAULT_TTL_SECONDS
|
|
199
|
+
)
|
|
303
200
|
|
|
304
201
|
except Exception as e:
|
|
305
|
-
|
|
306
202
|
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
307
203
|
|
|
308
|
-
|
|
309
204
|
return Response({"token": token_to_return}, status=status.HTTP_201_CREATED)
|
|
310
205
|
|
|
311
|
-
|
|
312
|
-
|
|
313
206
|
@action(detail=True, methods=["get"], url_path="all")
|
|
314
|
-
|
|
315
207
|
def get_all(self, request, pk=None):
|
|
208
|
+
"""
|
|
209
|
+
Retrieve complete lookup data for a session token.
|
|
316
210
|
|
|
211
|
+
Returns all stored data for the given token. If data is not found,
|
|
212
|
+
attempts automatic session recovery using persisted origin mapping.
|
|
317
213
|
|
|
318
|
-
|
|
214
|
+
Args:
|
|
215
|
+
pk: Session token
|
|
319
216
|
|
|
217
|
+
Returns:
|
|
218
|
+
Complete lookup data dictionary
|
|
320
219
|
|
|
321
|
-
|
|
220
|
+
Raises:
|
|
221
|
+
404: Token not found and recovery failed
|
|
222
|
+
"""
|
|
322
223
|
|
|
323
|
-
|
|
224
|
+
if not pk:
|
|
225
|
+
return Response(
|
|
226
|
+
{"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND
|
|
227
|
+
)
|
|
324
228
|
|
|
229
|
+
store = LookupStore(token=pk)
|
|
325
230
|
|
|
326
231
|
try:
|
|
327
|
-
|
|
328
|
-
|
|
329
232
|
validated_data = store.validate_and_recover_data(pk)
|
|
330
233
|
|
|
331
|
-
|
|
332
234
|
except Exception:
|
|
333
|
-
|
|
334
|
-
|
|
335
235
|
validated_data = None
|
|
336
236
|
|
|
337
237
|
if validated_data is None:
|
|
338
|
-
|
|
339
|
-
|
|
340
238
|
# Try automatic restart once using persisted origin mapping
|
|
341
239
|
|
|
342
|
-
|
|
343
240
|
pe_id = cache.get(f"{ORIGIN_MAP_PREFIX}{pk}")
|
|
344
241
|
|
|
345
|
-
|
|
346
242
|
if pe_id:
|
|
347
|
-
|
|
348
|
-
|
|
349
243
|
try:
|
|
350
|
-
|
|
351
|
-
|
|
352
244
|
internal_token = ls.create_lookup_token_for_pe(int(pe_id))
|
|
353
245
|
|
|
354
|
-
|
|
355
246
|
new_data = LookupStore(token=internal_token).get_all()
|
|
356
247
|
|
|
357
|
-
|
|
358
248
|
if not new_data:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
249
|
+
return Response(
|
|
250
|
+
{
|
|
251
|
+
"error": "Lookup data not available after restart",
|
|
252
|
+
"token": pk,
|
|
253
|
+
},
|
|
254
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
255
|
+
)
|
|
363
256
|
|
|
364
257
|
return Response(new_data, status=status.HTTP_200_OK)
|
|
365
258
|
|
|
366
|
-
|
|
367
259
|
except Exception:
|
|
368
|
-
|
|
369
|
-
|
|
370
260
|
pass
|
|
371
261
|
|
|
372
|
-
|
|
373
|
-
|
|
262
|
+
return Response(
|
|
263
|
+
{"error": "Lookup data not found or expired", "token": pk},
|
|
264
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
265
|
+
)
|
|
374
266
|
|
|
375
267
|
return Response(store.get_all())
|
|
376
268
|
|
|
377
|
-
|
|
378
|
-
|
|
379
269
|
@action(detail=True, methods=["get", "patch"], url_path="parts")
|
|
380
|
-
|
|
381
270
|
def parts(self, request, pk=None):
|
|
271
|
+
"""
|
|
272
|
+
Get or update partial lookup data for a session.
|
|
382
273
|
|
|
274
|
+
GET: Retrieve specific keys from the session data.
|
|
275
|
+
PATCH: Update session data and trigger recomputation if input keys changed.
|
|
383
276
|
|
|
384
|
-
|
|
277
|
+
GET query params:
|
|
278
|
+
keys: Comma-separated list of keys to retrieve
|
|
385
279
|
|
|
280
|
+
PATCH body:
|
|
281
|
+
updates: Dictionary of key-value pairs to update
|
|
386
282
|
|
|
387
|
-
|
|
283
|
+
Args:
|
|
284
|
+
pk: Session token
|
|
388
285
|
|
|
389
|
-
|
|
286
|
+
Returns:
|
|
287
|
+
GET: Dictionary with requested keys
|
|
288
|
+
PATCH: Success confirmation
|
|
390
289
|
|
|
290
|
+
Raises:
|
|
291
|
+
404: Token not found
|
|
292
|
+
400: Invalid request parameters
|
|
293
|
+
"""
|
|
391
294
|
|
|
295
|
+
if not pk:
|
|
296
|
+
return Response(
|
|
297
|
+
{"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND
|
|
298
|
+
)
|
|
392
299
|
|
|
393
|
-
|
|
300
|
+
store = LookupStore(token=pk)
|
|
394
301
|
|
|
302
|
+
if request.method == "GET":
|
|
395
303
|
keys_param = request.query_params.get("keys", "")
|
|
396
304
|
|
|
397
305
|
keys = [k.strip() for k in keys_param.split(",") if k.strip()]
|
|
398
306
|
|
|
399
307
|
if not keys:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
308
|
+
return Response(
|
|
309
|
+
{"detail": "Provide ?keys=key1,key2"},
|
|
310
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
311
|
+
)
|
|
403
312
|
|
|
404
313
|
try:
|
|
405
|
-
|
|
406
|
-
|
|
407
314
|
return Response(store.get_many(keys))
|
|
408
315
|
|
|
409
|
-
|
|
410
316
|
except Exception:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
317
|
+
return Response(
|
|
318
|
+
{"detail": "Lookup data not found or expired"},
|
|
319
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
320
|
+
)
|
|
416
321
|
|
|
417
322
|
# PATCH
|
|
418
323
|
|
|
419
324
|
updates = request.data.get("updates", {})
|
|
420
325
|
|
|
421
|
-
|
|
422
326
|
if not isinstance(updates, dict) or not updates:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
327
|
+
return Response(
|
|
328
|
+
{"detail": "updates must be a non-empty object"},
|
|
329
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
330
|
+
)
|
|
428
331
|
|
|
429
332
|
store.set_many(updates)
|
|
430
333
|
|
|
431
|
-
|
|
432
|
-
|
|
433
334
|
if any(key in self.INPUT_KEYS for key in updates.keys()):
|
|
434
|
-
|
|
435
335
|
try:
|
|
436
|
-
|
|
437
|
-
|
|
438
336
|
ls.recompute_lookup(pk)
|
|
439
337
|
|
|
440
338
|
except Exception as e:
|
|
441
|
-
|
|
442
339
|
import logging
|
|
443
340
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
341
|
+
logging.getLogger(__name__).error(
|
|
342
|
+
"Failed to recompute after patch for token %s: %s", pk, e
|
|
343
|
+
)
|
|
448
344
|
|
|
449
345
|
return Response({"ok": True, "token": pk}, status=status.HTTP_200_OK)
|
|
450
346
|
|
|
451
|
-
|
|
452
|
-
|
|
453
347
|
@action(detail=True, methods=["post"], url_path="recompute")
|
|
454
|
-
|
|
455
348
|
def recompute(self, request, pk=None):
|
|
456
|
-
|
|
457
349
|
"""Recompute lookup data based on current PatientExamination and user selections"""
|
|
458
350
|
|
|
459
|
-
|
|
460
351
|
if not pk:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
352
|
+
return Response(
|
|
353
|
+
{"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND
|
|
354
|
+
)
|
|
464
355
|
|
|
465
356
|
try:
|
|
466
|
-
|
|
467
|
-
|
|
468
357
|
updates = ls.recompute_lookup(pk)
|
|
469
358
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
359
|
+
return Response(
|
|
360
|
+
{"ok": True, "token": pk, "updates": updates}, status=status.HTTP_200_OK
|
|
361
|
+
)
|
|
476
362
|
|
|
477
363
|
except ValueError as e:
|
|
478
|
-
|
|
479
364
|
return Response({"detail": str(e)}, status=status.HTTP_404_NOT_FOUND)
|
|
480
365
|
|
|
481
366
|
except Exception as e:
|
|
482
|
-
|
|
483
|
-
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
367
|
+
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
|
@@ -35,10 +35,8 @@ __all__ = [
|
|
|
35
35
|
'VideoApplyMaskView',
|
|
36
36
|
'VideoRemoveFramesView',
|
|
37
37
|
|
|
38
|
-
# Phase 1.2: Media Management Views ✅ IMPLEMENTED
|
|
39
38
|
'VideoMediaView',
|
|
40
39
|
|
|
41
|
-
# TODO Phase 1.2+: Future views
|
|
42
40
|
'VideoCorrectionView',
|
|
43
41
|
# 'TaskStatusView',
|
|
44
42
|
# 'VideoDownloadProcessedView',
|