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,122 +1,266 @@
|
|
|
1
1
|
# services/lookup_store.py
|
|
2
2
|
from __future__ import annotations
|
|
3
|
+
|
|
3
4
|
import uuid
|
|
4
5
|
from typing import Any, Dict, Iterable, Optional
|
|
5
|
-
|
|
6
|
+
|
|
6
7
|
from django.conf import settings
|
|
8
|
+
from django.core.cache import cache
|
|
7
9
|
|
|
8
10
|
# Align TTL with Django cache TIMEOUT for consistency in tests and runtime
|
|
9
11
|
try:
|
|
10
|
-
DEFAULT_TTL_SECONDS = int(
|
|
12
|
+
DEFAULT_TTL_SECONDS = int(
|
|
13
|
+
settings.CACHES.get("default", {}).get("TIMEOUT", 60 * 30)
|
|
14
|
+
)
|
|
11
15
|
except Exception:
|
|
12
16
|
DEFAULT_TTL_SECONDS = 60 * 30 # 30 minutes fallback
|
|
13
17
|
|
|
18
|
+
|
|
14
19
|
class LookupStore:
|
|
15
20
|
"""
|
|
16
|
-
Server-side lookup
|
|
17
|
-
|
|
21
|
+
Server-side storage for lookup session data using Django cache.
|
|
22
|
+
|
|
23
|
+
This class manages token-based sessions for the lookup system, providing
|
|
24
|
+
a cache-backed storage layer that allows clients to maintain state across
|
|
25
|
+
multiple API requests. Each session is identified by a unique token and
|
|
26
|
+
stores derived lookup data including requirement evaluations, statuses,
|
|
27
|
+
and suggested actions.
|
|
28
|
+
|
|
29
|
+
Key features:
|
|
30
|
+
- Token-based session management with automatic UUID generation
|
|
31
|
+
- Django cache integration with configurable TTL
|
|
32
|
+
- Data validation and recovery for corrupted sessions
|
|
33
|
+
- Reentrancy protection for recomputation operations
|
|
34
|
+
- Atomic updates to prevent data corruption
|
|
35
|
+
|
|
36
|
+
The store uses Django's default cache backend and aligns TTL settings
|
|
37
|
+
with Django's cache configuration for consistency across environments.
|
|
18
38
|
"""
|
|
39
|
+
|
|
19
40
|
def __init__(self, token: Optional[str] = None):
|
|
41
|
+
"""
|
|
42
|
+
Initialize a LookupStore instance with an optional token.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
token: Optional session token. If not provided, a new UUID token
|
|
46
|
+
will be generated automatically.
|
|
47
|
+
"""
|
|
20
48
|
self.token = token or uuid.uuid4().hex
|
|
21
49
|
|
|
22
50
|
@property
|
|
23
51
|
def cache_key(self) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Generate the cache key for this lookup session.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Cache key string in format 'lookup:{token}'
|
|
57
|
+
"""
|
|
24
58
|
return f"lookup:{self.token}"
|
|
25
59
|
|
|
26
|
-
def init(
|
|
60
|
+
def init(
|
|
61
|
+
self, initial: Optional[Dict[str, Any]] = None, ttl: int = DEFAULT_TTL_SECONDS
|
|
62
|
+
) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Initialize a new lookup session in cache.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
initial: Optional initial data dictionary to store
|
|
68
|
+
ttl: Time-to-live in seconds for the cache entry
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The session token for this lookup store
|
|
72
|
+
"""
|
|
27
73
|
cache.set(self.cache_key, initial or {}, ttl)
|
|
28
74
|
return self.token
|
|
29
75
|
|
|
30
76
|
def get_all(self) -> Dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Retrieve all data for this lookup session.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Complete dictionary of stored lookup data, or empty dict if not found
|
|
82
|
+
"""
|
|
31
83
|
return cache.get(self.cache_key, {})
|
|
32
84
|
|
|
33
85
|
def get_many(self, keys: Iterable[str]) -> Dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Retrieve multiple specific keys from the lookup session.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
keys: Iterable of key names to retrieve
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dictionary containing only the requested keys and their values
|
|
94
|
+
"""
|
|
34
95
|
data = self.get_all()
|
|
35
96
|
return {k: data.get(k) for k in keys}
|
|
36
97
|
|
|
37
98
|
def set_many(self, updates: Dict[str, Any], ttl: int = DEFAULT_TTL_SECONDS) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Update multiple keys in the lookup session atomically.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
updates: Dictionary of key-value pairs to update
|
|
104
|
+
ttl: Time-to-live in seconds for the updated cache entry
|
|
105
|
+
"""
|
|
38
106
|
data = self.get_all()
|
|
39
107
|
data.update(updates)
|
|
40
108
|
cache.set(self.cache_key, data, ttl)
|
|
41
109
|
|
|
42
110
|
def get(self, key: str, default: Any = None) -> Any:
|
|
111
|
+
"""
|
|
112
|
+
Retrieve a single key from the lookup session.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
key: Key name to retrieve
|
|
116
|
+
default: Default value if key not found
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Value of the key, or default if not found
|
|
120
|
+
"""
|
|
43
121
|
return self.get_all().get(key, default)
|
|
44
122
|
|
|
45
123
|
def set(self, key: str, value: Any, ttl: int = DEFAULT_TTL_SECONDS) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Set a single key in the lookup session.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
key: Key name to set
|
|
129
|
+
value: Value to store
|
|
130
|
+
ttl: Time-to-live in seconds for the updated cache entry
|
|
131
|
+
"""
|
|
46
132
|
data = self.get_all()
|
|
47
133
|
data[key] = value
|
|
48
134
|
cache.set(self.cache_key, data, ttl)
|
|
49
135
|
|
|
50
136
|
def delete(self) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Delete the entire lookup session from cache.
|
|
139
|
+
"""
|
|
51
140
|
cache.delete(self.cache_key)
|
|
52
|
-
|
|
141
|
+
|
|
53
142
|
def patch(self, updates: Dict[str, Any], ttl: int = DEFAULT_TTL_SECONDS) -> None:
|
|
54
|
-
"""
|
|
143
|
+
"""
|
|
144
|
+
Update existing data with new values (alias for set_many).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
updates: Dictionary of key-value pairs to update
|
|
148
|
+
ttl: Time-to-live in seconds for the updated cache entry
|
|
149
|
+
"""
|
|
55
150
|
data = self.get_all()
|
|
56
151
|
data.update(updates)
|
|
57
152
|
cache.set(self.cache_key, data, ttl)
|
|
58
|
-
|
|
59
153
|
|
|
60
154
|
def validate_and_recover_data(self, token):
|
|
61
|
-
"""
|
|
155
|
+
"""
|
|
156
|
+
Validate stored lookup data and attempt recovery if corrupted.
|
|
157
|
+
|
|
158
|
+
Checks for required fields and attempts to recover missing data,
|
|
159
|
+
particularly the patient_examination_id. Logs warnings for missing
|
|
160
|
+
fields but does not trigger automatic recomputation to avoid loops.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
token: Session token for logging purposes
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Validated data dictionary, or None if no data exists
|
|
167
|
+
"""
|
|
62
168
|
data = self.get_all()
|
|
63
|
-
|
|
169
|
+
|
|
64
170
|
if not data:
|
|
65
171
|
return None
|
|
66
|
-
|
|
172
|
+
|
|
67
173
|
# Check if required fields are present
|
|
68
|
-
required_fields = [
|
|
174
|
+
required_fields = [
|
|
175
|
+
"patient_examination_id",
|
|
176
|
+
"requirementsBySet",
|
|
177
|
+
"requirementStatus",
|
|
178
|
+
]
|
|
69
179
|
missing_fields = [field for field in required_fields if field not in data]
|
|
70
|
-
|
|
180
|
+
|
|
71
181
|
if missing_fields:
|
|
72
182
|
import logging
|
|
183
|
+
|
|
73
184
|
logger = logging.getLogger(__name__)
|
|
74
|
-
logger.warning(
|
|
75
|
-
|
|
185
|
+
logger.warning(
|
|
186
|
+
f"Missing fields in lookup data for token {token}: {missing_fields}"
|
|
187
|
+
)
|
|
188
|
+
|
|
76
189
|
# Try to recover patient_examination_id from token or related data
|
|
77
|
-
if
|
|
190
|
+
if "patient_examination_id" in missing_fields:
|
|
78
191
|
# Attempt to extract from token or find related examination
|
|
79
192
|
recovered_id = self._recover_patient_examination_id(token)
|
|
80
193
|
if recovered_id:
|
|
81
|
-
data[
|
|
82
|
-
logger.info(
|
|
83
|
-
|
|
194
|
+
data["patient_examination_id"] = recovered_id
|
|
195
|
+
logger.info(
|
|
196
|
+
f"Recovered patient_examination_id {recovered_id} for token {token}"
|
|
197
|
+
)
|
|
198
|
+
|
|
84
199
|
# Do not automatically recompute here to avoid loops
|
|
85
200
|
# Recompute is only triggered by PATCH or explicit POST /recompute/
|
|
86
201
|
# For now, just return the data as is
|
|
87
|
-
|
|
202
|
+
|
|
88
203
|
return data
|
|
89
204
|
|
|
90
205
|
def _recover_patient_examination_id(self, token: str) -> Optional[str]:
|
|
91
206
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
207
|
+
Attempt to recover the patient examination ID for corrupted sessions.
|
|
208
|
+
|
|
209
|
+
This is a placeholder implementation. In a real system, this might
|
|
210
|
+
query a database or another service to find the examination ID
|
|
211
|
+
associated with the token.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
token: Session token
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Recovered patient examination ID, or None if recovery fails
|
|
94
218
|
"""
|
|
95
219
|
# In a real implementation, you might query a database or another service.
|
|
96
220
|
# For now, we return None as recovery logic is not defined.
|
|
97
221
|
return None
|
|
98
|
-
|
|
222
|
+
|
|
99
223
|
def should_recompute(self, token):
|
|
100
|
-
"""
|
|
224
|
+
"""
|
|
225
|
+
Determine if recomputation is needed based on data freshness.
|
|
226
|
+
|
|
227
|
+
Checks the last recomputation timestamp and only allows recomputation
|
|
228
|
+
if more than 30 seconds have passed since the last one. This prevents
|
|
229
|
+
excessive recomputation while allowing for necessary updates.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
token: Session token (for future use)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if recomputation should be performed, False otherwise
|
|
236
|
+
"""
|
|
101
237
|
data = self.get_all()
|
|
102
238
|
if not data:
|
|
103
239
|
return True
|
|
104
|
-
|
|
240
|
+
|
|
105
241
|
# Check if we have a last_recompute timestamp
|
|
106
|
-
last_recompute = data.get(
|
|
242
|
+
last_recompute = data.get("_last_recompute")
|
|
107
243
|
if not last_recompute:
|
|
108
244
|
return True
|
|
109
|
-
|
|
245
|
+
|
|
110
246
|
# Only recompute if it's been more than 30 seconds since last recompute
|
|
111
247
|
# This prevents excessive recomputation while allowing for updates
|
|
112
248
|
from datetime import datetime, timedelta
|
|
249
|
+
|
|
113
250
|
try:
|
|
114
251
|
last_recompute_time = datetime.fromisoformat(last_recompute)
|
|
115
252
|
return datetime.now() - last_recompute_time > timedelta(seconds=30)
|
|
116
253
|
except (ValueError, TypeError):
|
|
117
254
|
return True
|
|
118
|
-
|
|
255
|
+
|
|
119
256
|
def mark_recompute_done(self):
|
|
120
|
-
"""
|
|
257
|
+
"""
|
|
258
|
+
Mark that recomputation has been completed by updating the timestamp.
|
|
259
|
+
|
|
260
|
+
Sets the _last_recompute field to the current timestamp in ISO format.
|
|
261
|
+
This timestamp is used by should_recompute() to determine if another
|
|
262
|
+
recomputation is needed.
|
|
263
|
+
"""
|
|
121
264
|
from datetime import datetime
|
|
122
|
-
|
|
265
|
+
|
|
266
|
+
self.set("_last_recompute", datetime.now().isoformat())
|