endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.6.1__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.

Files changed (36) hide show
  1. endoreg_db/management/commands/load_ai_model_data.py +2 -1
  2. endoreg_db/management/commands/setup_endoreg_db.py +11 -7
  3. endoreg_db/models/media/pdf/raw_pdf.py +241 -97
  4. endoreg_db/models/media/video/pipe_1.py +30 -33
  5. endoreg_db/models/media/video/video_file.py +300 -187
  6. endoreg_db/models/metadata/model_meta_logic.py +15 -1
  7. endoreg_db/models/metadata/sensitive_meta_logic.py +391 -70
  8. endoreg_db/serializers/__init__.py +26 -55
  9. endoreg_db/serializers/misc/__init__.py +1 -1
  10. endoreg_db/serializers/misc/file_overview.py +65 -35
  11. endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
  12. endoreg_db/serializers/video_examination.py +198 -0
  13. endoreg_db/services/lookup_service.py +228 -58
  14. endoreg_db/services/lookup_store.py +174 -30
  15. endoreg_db/services/pdf_import.py +585 -282
  16. endoreg_db/services/video_import.py +340 -101
  17. endoreg_db/urls/__init__.py +36 -23
  18. endoreg_db/urls/label_video_segments.py +2 -0
  19. endoreg_db/urls/media.py +3 -2
  20. endoreg_db/views/__init__.py +6 -3
  21. endoreg_db/views/media/pdf_media.py +3 -1
  22. endoreg_db/views/media/video_media.py +1 -1
  23. endoreg_db/views/media/video_segments.py +187 -259
  24. endoreg_db/views/pdf/__init__.py +5 -8
  25. endoreg_db/views/pdf/pdf_stream.py +187 -0
  26. endoreg_db/views/pdf/reimport.py +110 -94
  27. endoreg_db/views/requirement/lookup.py +171 -287
  28. endoreg_db/views/video/__init__.py +0 -2
  29. endoreg_db/views/video/video_examination_viewset.py +202 -289
  30. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/METADATA +1 -1
  31. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/RECORD +33 -34
  32. endoreg_db/views/pdf/pdf_media.py +0 -239
  33. endoreg_db/views/pdf/pdf_stream_views.py +0 -127
  34. endoreg_db/views/video/video_media.py +0 -158
  35. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/WHEEL +0 -0
  36. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,35 +1,81 @@
1
+ """
2
+ Lookup Service Module
3
+
4
+ This module provides server-side evaluation and lookup functionality for patient examinations.
5
+ It handles requirement set evaluation, finding availability, and status computation for
6
+ medical examination workflows.
7
+
8
+ The lookup system uses a token-based approach where client sessions are stored in Django cache,
9
+ allowing for efficient state management and recomputation of derived data.
10
+
11
+ Key Components:
12
+ - PatientExamination loading with optimized prefetching
13
+ - Requirement set resolution and evaluation
14
+ - Status computation for requirements and requirement sets
15
+ - Suggested actions for unsatisfied requirements
16
+ - Cache-based session management
17
+
18
+ Architecture:
19
+ 1. LookupStore: Handles cache-based session storage
20
+ 2. lookup_service: Core business logic for evaluation
21
+ 3. LookupViewSet: Django REST API endpoints
22
+ """
23
+
1
24
  # services/lookup_service.py
2
25
  from __future__ import annotations
3
- from typing import Dict, Any, List
26
+
27
+ from typing import Any, Dict, List
28
+
4
29
  from django.db.models import Prefetch
5
- from endoreg_db.models.medical.patient.patient_examination import PatientExamination
30
+
6
31
  from endoreg_db.models.medical.examination import ExaminationRequirementSet
32
+ from endoreg_db.models.medical.patient.patient_examination import PatientExamination
7
33
  from endoreg_db.models.requirement.requirement_set import RequirementSet
34
+
8
35
  from .lookup_store import LookupStore
9
36
 
10
37
 
11
38
  def load_patient_exam_for_eval(pk: int) -> PatientExamination:
12
39
  """
13
- Fetch PatientExamination with everything needed for evaluation,
14
- following the *Examination → ExaminationRequirementSet → RequirementSet* graph.
40
+ Load a PatientExamination with all related data needed for evaluation.
41
+
42
+ This function performs optimized database queries to fetch a PatientExamination
43
+ along with all related objects required for requirement evaluation, including:
44
+ - Patient and examination details
45
+ - Patient findings
46
+ - Examination requirement sets and their requirements
47
+ - Nested requirement set relationships
48
+
49
+ The query uses select_related and prefetch_related to minimize database hits
50
+ and ensure all data is available for evaluation without additional queries.
51
+
52
+ Args:
53
+ pk: Primary key of the PatientExamination to load
54
+
55
+ Returns:
56
+ PatientExamination: Fully loaded instance with all related data prefetched
57
+
58
+ Raises:
59
+ PatientExamination.DoesNotExist: If no examination exists with the given pk
15
60
  """
16
61
  return (
17
- PatientExamination.objects
18
- .select_related("patient", "examination")
62
+ PatientExamination.objects.select_related("patient", "examination")
19
63
  .prefetch_related(
20
64
  "patient_findings",
21
65
  # Prefetch ERS groups on the Examination…
22
66
  Prefetch(
23
67
  "examination__exam_reqset_links",
24
- queryset=ExaminationRequirementSet.objects.only("id", "name", "enabled_by_default"),
68
+ queryset=ExaminationRequirementSet.objects.only(
69
+ "id", "name", "enabled_by_default"
70
+ ),
25
71
  ),
26
72
  # …and the RequirementSets reachable via those ERS groups.
27
73
  Prefetch(
28
74
  "examination__exam_reqset_links__requirement_set",
29
75
  queryset=(
30
- RequirementSet.objects
31
- .select_related("requirement_set_type")
32
- .prefetch_related(
76
+ RequirementSet.objects.select_related(
77
+ "requirement_set_type"
78
+ ).prefetch_related(
33
79
  "requirements",
34
80
  "links_to_sets",
35
81
  "links_to_sets__requirements",
@@ -44,27 +90,66 @@ def load_patient_exam_for_eval(pk: int) -> PatientExamination:
44
90
 
45
91
  def requirement_sets_for_patient_exam(pe: PatientExamination) -> List[RequirementSet]:
46
92
  """
47
- Correctly resolve RequirementSets for a PE via ERS hub:
48
- RequirementSet.objects.filter(reqset_exam_links__examinations=exam)
93
+ Get all requirement sets applicable to a patient examination.
94
+
95
+ This function resolves requirement sets through the examination's requirement set links.
96
+ It follows the relationship: PatientExamination → Examination → ExaminationRequirementSet → RequirementSet
97
+
98
+ Args:
99
+ pe: PatientExamination instance to get requirement sets for
100
+
101
+ Returns:
102
+ List of RequirementSet instances applicable to the examination, with related data prefetched
49
103
  """
50
104
  exam = pe.examination
51
105
  if not exam:
52
106
  return []
53
107
  return list(
54
- RequirementSet.objects
55
- .filter(reqset_exam_links__examinations=exam)
108
+ RequirementSet.objects.filter(reqset_exam_links__examinations=exam)
56
109
  .select_related("requirement_set_type")
57
110
  .prefetch_related("requirements")
58
111
  .distinct()
59
112
  )
60
113
 
114
+
61
115
  def build_initial_lookup(pe: PatientExamination) -> Dict[str, Any]:
62
116
  """
63
- Build the initial lookup dict you will return to the client.
64
- Keep keys small and stable; values must be JSON-serializable.
117
+ Build the initial lookup dictionary for a patient examination.
118
+
119
+ This function creates the base lookup data structure that will be stored in cache
120
+ and used by the client for requirement evaluation. It includes:
121
+
122
+ - Available findings for the examination type
123
+ - Required findings based on requirement defaults
124
+ - Requirement sets metadata
125
+ - Default findings and classification choices per requirement
126
+ - Empty placeholders for dynamic data (status, suggestions, etc.)
127
+
128
+ The returned dictionary is JSON-serializable and contains stable keys that
129
+ won't change between versions.
130
+
131
+ Args:
132
+ pe: PatientExamination instance to build lookup for
133
+
134
+ Returns:
135
+ Dictionary containing initial lookup data with the following keys:
136
+ - patient_examination_id: ID of the patient examination
137
+ - requirement_sets: List of available requirement sets with metadata
138
+ - availableFindings: List of finding IDs available for the examination
139
+ - requiredFindings: List of finding IDs that are required by defaults
140
+ - requirementDefaults: Default findings per requirement
141
+ - classificationChoices: Available classification choices per requirement
142
+ - requirementsBySet: Empty dict (populated on selection)
143
+ - requirementStatus: Empty dict (computed on evaluation)
144
+ - requirementSetStatus: Empty dict (computed on evaluation)
145
+ - suggestedActions: Empty dict (computed on evaluation)
65
146
  """
66
147
  # Available + required findings
67
- available_findings = [f.id for f in pe.examination.get_available_findings()] if pe.examination else []
148
+ available_findings = (
149
+ [f.id for f in pe.examination.get_available_findings()]
150
+ if pe.examination
151
+ else []
152
+ )
68
153
  required_findings: List[int] = [] # fill by scanning requirements below
69
154
 
70
155
  # Requirement sets: ids + meta
@@ -90,9 +175,13 @@ def build_initial_lookup(pe: PatientExamination) -> Dict[str, Any]:
90
175
  choices = getattr(req, "classification_choices", lambda pe: [])(pe)
91
176
  if defaults:
92
177
  req_defaults[str(req.id)] = defaults # list of {finding_id, payload...}
93
- required_findings.extend([d.get("finding_id") for d in defaults if "finding_id" in d])
178
+ required_findings.extend(
179
+ [d.get("finding_id") for d in defaults if "finding_id" in d]
180
+ )
94
181
  if choices:
95
- cls_choices[str(req.id)] = choices # list of {classification_id, label, ...}
182
+ cls_choices[str(req.id)] = (
183
+ choices # list of {classification_id, label, ...}
184
+ )
96
185
 
97
186
  # De-dup required
98
187
  required_findings = sorted(set(required_findings))
@@ -112,62 +201,129 @@ def build_initial_lookup(pe: PatientExamination) -> Dict[str, Any]:
112
201
  # You can add "selectedRequirementSetIds" as the user makes choices
113
202
  }
114
203
 
204
+
115
205
  def create_lookup_token_for_pe(pe_id: int) -> str:
206
+ """
207
+ Create a lookup token for a patient examination.
208
+
209
+ This function initializes a new lookup session for the given patient examination
210
+ by building the initial lookup data and storing it in the cache via LookupStore.
211
+
212
+ Args:
213
+ pe_id: Primary key of the PatientExamination
214
+
215
+ Returns:
216
+ String token that can be used to access the lookup session
217
+
218
+ Raises:
219
+ PatientExamination.DoesNotExist: If examination doesn't exist
220
+ Exception: For any other errors during initialization
221
+ """
116
222
  pe = load_patient_exam_for_eval(pe_id)
117
223
  token = LookupStore().init(build_initial_lookup(pe))
118
224
  return token
119
225
 
226
+
120
227
  def recompute_lookup(token: str) -> Dict[str, Any]:
228
+ """
229
+ Recompute derived lookup data based on current patient examination state and user selections.
230
+
231
+ This function performs the core evaluation logic for the lookup system. It:
232
+
233
+ 1. Validates and recovers corrupted lookup data
234
+ 2. Loads the current PatientExamination state from database
235
+ 3. Evaluates requirements against the current examination state
236
+ 4. Computes status for individual requirements and requirement sets
237
+ 5. Generates suggested actions for unsatisfied requirements
238
+ 6. Updates the cache with new derived data (idempotent)
239
+
240
+ The function includes reentrancy protection to prevent concurrent recomputation
241
+ of the same token.
242
+
243
+ Args:
244
+ token: Lookup session token
245
+
246
+ Returns:
247
+ Dictionary of updates containing:
248
+ - requirementsBySet: Requirements grouped by selected requirement sets
249
+ - requirementStatus: Boolean status for each requirement
250
+ - requirementSetStatus: Boolean status for each requirement set
251
+ - requirementDefaults: Default findings per requirement
252
+ - classificationChoices: Available choices per requirement
253
+ - suggestedActions: UI actions to satisfy unsatisfied requirements
254
+
255
+ Raises:
256
+ ValueError: If lookup data is invalid or patient examination not found
257
+ """
121
258
  import logging
259
+
122
260
  logger = logging.getLogger(__name__)
123
-
261
+
124
262
  store = LookupStore(token=token)
125
-
263
+
126
264
  # Simple reentrancy guard using data
127
265
  data = store.get_all()
128
- if data.get('_recomputing'):
266
+ if data.get("_recomputing"):
129
267
  logger.warning(f"Recompute already in progress for token {token}, skipping")
130
268
  return {}
131
-
132
- store.set('_recomputing', True)
133
-
269
+
270
+ store.set("_recomputing", True)
271
+
134
272
  try:
135
273
  # First validate and attempt to recover corrupted data
136
274
  validated_data = store.validate_and_recover_data(token)
137
275
  if validated_data is None:
138
276
  logger.error(f"No lookup data found for token {token}")
139
277
  raise ValueError(f"No lookup data found for token {token}")
140
-
278
+
141
279
  data = validated_data
142
- logger.debug(f"Recomputing lookup for token {token}, data keys: {list(data.keys())}")
143
-
280
+ logger.debug(
281
+ f"Recomputing lookup for token {token}, data keys: {list(data.keys())}"
282
+ )
283
+
144
284
  # Check if required data exists
145
285
  if "patient_examination_id" not in data:
146
- logger.error(f"Invalid lookup data for token {token}: missing patient_examination_id. Data: {data}")
147
- raise ValueError(f"Invalid lookup data for token {token}: missing patient_examination_id")
148
-
286
+ logger.error(
287
+ f"Invalid lookup data for token {token}: missing patient_examination_id. Data: {data}"
288
+ )
289
+ raise ValueError(
290
+ f"Invalid lookup data for token {token}: missing patient_examination_id"
291
+ )
292
+
149
293
  if not data.get("patient_examination_id"):
150
- logger.error(f"Invalid lookup data for token {token}: patient_examination_id is empty. Data: {data}")
151
- raise ValueError(f"Invalid lookup data for token {token}: patient_examination_id is empty")
294
+ logger.error(
295
+ f"Invalid lookup data for token {token}: patient_examination_id is empty. Data: {data}"
296
+ )
297
+ raise ValueError(
298
+ f"Invalid lookup data for token {token}: patient_examination_id is empty"
299
+ )
152
300
 
153
301
  pe_id = data["patient_examination_id"]
154
302
  logger.debug(f"Loading patient examination {pe_id} for token {token}")
155
-
303
+
156
304
  try:
157
305
  pe = load_patient_exam_for_eval(pe_id)
158
306
  except Exception as e:
159
- logger.error(f"Failed to load patient examination {pe_id} for token {token}: {e}")
307
+ logger.error(
308
+ f"Failed to load patient examination {pe_id} for token {token}: {e}"
309
+ )
160
310
  raise ValueError(f"Failed to load patient examination {pe_id}: {e}")
161
311
 
162
312
  selected_rs_ids: List[int] = data.get("selectedRequirementSetIds", [])
163
- logger.debug(f"Selected requirement set IDs for token {token}: {selected_rs_ids}")
164
-
165
- rs_objs = [rs for rs in requirement_sets_for_patient_exam(pe) if rs.id in selected_rs_ids]
313
+ logger.debug(
314
+ f"Selected requirement set IDs for token {token}: {selected_rs_ids}"
315
+ )
316
+
317
+ rs_objs = [
318
+ rs
319
+ for rs in requirement_sets_for_patient_exam(pe)
320
+ if rs.id in selected_rs_ids
321
+ ]
166
322
  logger.debug(f"Found {len(rs_objs)} requirement set objects for token {token}")
167
323
 
168
324
  # 1) requirements grouped by set (already prefetched in load func)
169
325
  requirements_by_set = {
170
- rs.id: [ {"id": r.id, "name": r.name} for r in rs.requirements.all() ]
326
+ rs.id: [{"id": r.id, "name": r.name} for r in rs.requirements.all()]
171
327
  for rs in rs_objs
172
328
  }
173
329
 
@@ -180,7 +336,9 @@ def recompute_lookup(token: str) -> Dict[str, Any]:
180
336
  ok = bool(r.evaluate(pe, mode="strict")) # or "loose" if you prefer
181
337
  requirement_status[str(r.id)] = ok
182
338
  req_results.append(ok)
183
- set_status[str(rs.id)] = rs.eval_function(req_results) if rs.eval_function else all(req_results)
339
+ set_status[str(rs.id)] = (
340
+ rs.eval_function(req_results) if rs.eval_function else all(req_results)
341
+ )
184
342
 
185
343
  # 3) suggestions per requirement (defaults + classification choices you already expose)
186
344
  suggested_actions: Dict[str, List[Dict[str, Any]]] = {}
@@ -189,8 +347,12 @@ def recompute_lookup(token: str) -> Dict[str, Any]:
189
347
 
190
348
  for rs in rs_objs:
191
349
  for r in rs.requirements.all():
192
- defaults = getattr(r, "default_findings", lambda pe: [])(pe) # [{finding_id, payload...}]
193
- choices = getattr(r, "classification_choices", lambda pe: [])(pe) # [{classification_id, label,...}]
350
+ defaults = getattr(r, "default_findings", lambda pe: [])(
351
+ pe
352
+ ) # [{finding_id, payload...}]
353
+ choices = getattr(r, "classification_choices", lambda pe: [])(
354
+ pe
355
+ ) # [{classification_id, label,...}]
194
356
  if defaults:
195
357
  req_defaults[str(r.id)] = defaults
196
358
  if choices:
@@ -200,15 +362,19 @@ def recompute_lookup(token: str) -> Dict[str, Any]:
200
362
  # turn default proposals into explicit UI actions
201
363
  acts = []
202
364
  for d in defaults or []:
203
- acts.append({
204
- "type": "add_finding",
205
- "finding_id": d.get("finding_id"),
206
- "classification_ids": d.get("classification_ids") or [],
207
- "note": "default"
208
- })
365
+ acts.append(
366
+ {
367
+ "type": "add_finding",
368
+ "finding_id": d.get("finding_id"),
369
+ "classification_ids": d.get("classification_ids") or [],
370
+ "note": "default",
371
+ }
372
+ )
209
373
  # If r expects patient edits, add an edit action hint
210
374
  if "PatientExamination" in [m.__name__ for m in r.expected_models]:
211
- acts.append({"type": "edit_patient", "fields": ["gender", "dob"]}) # example
375
+ acts.append(
376
+ {"type": "edit_patient", "fields": ["gender", "dob"]}
377
+ ) # example
212
378
  if acts:
213
379
  suggested_actions[str(r.id)] = acts
214
380
 
@@ -220,22 +386,26 @@ def recompute_lookup(token: str) -> Dict[str, Any]:
220
386
  "requirementsBySet": requirements_by_set,
221
387
  "requirementStatus": requirement_status,
222
388
  "requirementSetStatus": set_status,
223
- "requirementDefaults": req_defaults, # keep your existing key
224
- "classificationChoices": cls_choices, # keep your existing key
225
- "suggestedActions": suggested_actions, # new
389
+ "requirementDefaults": req_defaults, # keep your existing key
390
+ "classificationChoices": cls_choices, # keep your existing key
391
+ "suggestedActions": suggested_actions, # new
226
392
  }
227
-
228
- logger.debug(f"Updating store for token {token} with {len(updates)} update keys")
229
-
393
+
394
+ logger.debug(
395
+ f"Updating store for token {token} with {len(updates)} update keys"
396
+ )
397
+
230
398
  # Only write if changed (idempotent)
231
399
  prev_derived = store.get_many(list(updates.keys()))
232
400
  if prev_derived != updates:
233
401
  store.set_many(updates) # <-- does NOT call recompute
234
402
  logger.debug(f"Derived data changed, updated store for token {token}")
235
403
  else:
236
- logger.debug(f"Derived data unchanged, skipping store update for token {token}")
237
-
404
+ logger.debug(
405
+ f"Derived data unchanged, skipping store update for token {token}"
406
+ )
407
+
238
408
  store.mark_recompute_done()
239
409
  return updates
240
410
  finally:
241
- store.set('_recomputing', False)
411
+ store.set("_recomputing", False)