endoreg-db 0.8.2.2__py3-none-any.whl → 0.8.2.3__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/helpers/default_objects.py +29 -48
- endoreg_db/management/commands/import_video.py +3 -5
- endoreg_db/models/media/video/video_file.py +44 -53
- endoreg_db/models/metadata/sensitive_meta.py +6 -6
- endoreg_db/services/ollama_api_docs.py +1528 -0
- endoreg_db/services/pseudonym_service.py +1 -1
- endoreg_db/services/video_import.py +363 -280
- endoreg_db/utils/paths.py +2 -2
- {endoreg_db-0.8.2.2.dist-info → endoreg_db-0.8.2.3.dist-info}/METADATA +2 -2
- {endoreg_db-0.8.2.2.dist-info → endoreg_db-0.8.2.3.dist-info}/RECORD +12 -11
- {endoreg_db-0.8.2.2.dist-info → endoreg_db-0.8.2.3.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.2.2.dist-info → endoreg_db-0.8.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import random
|
|
2
|
-
from typing import Optional
|
|
3
2
|
from endoreg_db.models import (
|
|
4
3
|
Center,
|
|
5
4
|
Gender,
|
|
@@ -18,7 +17,6 @@ import shutil
|
|
|
18
17
|
from pathlib import Path
|
|
19
18
|
from django.conf import settings # Import settings
|
|
20
19
|
from django.core.files.storage import default_storage # Import default storage
|
|
21
|
-
from django.db.models.fields.files import FieldFile
|
|
22
20
|
|
|
23
21
|
from endoreg_db.utils import (
|
|
24
22
|
create_mock_patient_name,
|
|
@@ -46,10 +44,6 @@ DEFAULT_INDICATIONS = [
|
|
|
46
44
|
DEFAULT_SEGMENTATION_MODEL_NAME = "image_multilabel_classification_colonoscopy_default"
|
|
47
45
|
|
|
48
46
|
DEFAULT_GENDER = "unknown"
|
|
49
|
-
DEFAULT_PATIENT_FIRST_NAME = "TestFirst"
|
|
50
|
-
DEFAULT_PATIENT_LAST_NAME = "TestLast"
|
|
51
|
-
DEFAULT_PATIENT_GENDER_NAME = "female"
|
|
52
|
-
DEFAULT_PATIENT_BIRTH_DATE = date(1970, 1, 1)
|
|
53
47
|
|
|
54
48
|
def get_information_source_prediction():
|
|
55
49
|
"""
|
|
@@ -176,41 +170,33 @@ def get_default_center() -> Center:
|
|
|
176
170
|
return center
|
|
177
171
|
|
|
178
172
|
def generate_patient(**kwargs) -> Patient:
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
173
|
+
"""
|
|
174
|
+
Creates a Patient instance with randomized or specified attributes.
|
|
175
|
+
|
|
176
|
+
Randomly generates first name, last name, date of birth, gender, and center for the patient unless overridden by keyword arguments. Raises ValueError if the provided gender is invalid.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
**kwargs: Optional overrides for patient attributes such as 'first_name', 'last_name', 'birth_date', 'gender', and 'center'.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A Patient instance with the specified or randomly generated attributes.
|
|
183
|
+
"""
|
|
184
|
+
# Set default values
|
|
185
|
+
gender = kwargs.get("gender", get_random_gender())
|
|
186
|
+
if not isinstance(gender, Gender):
|
|
190
187
|
gender = Gender.objects.get(name=gender)
|
|
191
188
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
last_name = last_name or generated_last
|
|
201
|
-
|
|
202
|
-
dob = kwargs.get("dob")
|
|
203
|
-
if dob is None:
|
|
204
|
-
birth_date = kwargs.get("birth_date", DEFAULT_PATIENT_BIRTH_DATE)
|
|
205
|
-
if isinstance(birth_date, date):
|
|
206
|
-
dob = birth_date
|
|
207
|
-
else:
|
|
208
|
-
dob = date.fromisoformat(str(birth_date))
|
|
209
|
-
|
|
210
|
-
center = kwargs.get("center")
|
|
189
|
+
if not isinstance(gender, Gender):
|
|
190
|
+
raise ValueError("No Gender Found")
|
|
191
|
+
first_name, last_name = create_mock_patient_name(gender = gender.name)
|
|
192
|
+
first_name = kwargs.get("first_name", first_name)
|
|
193
|
+
last_name = kwargs.get("last_name", last_name)
|
|
194
|
+
birth_date = kwargs.get("birth_date", "1970-01-01")
|
|
195
|
+
dob = date.fromisoformat(birth_date)
|
|
196
|
+
center = kwargs.get("center", None)
|
|
211
197
|
if center is None:
|
|
212
198
|
center = get_default_center()
|
|
213
|
-
|
|
199
|
+
else:
|
|
214
200
|
center = Center.objects.get(name=center)
|
|
215
201
|
|
|
216
202
|
patient = Patient(
|
|
@@ -275,7 +261,6 @@ def get_default_egd_pdf():
|
|
|
275
261
|
shutil.copy(egd_path, temp_file_path)
|
|
276
262
|
|
|
277
263
|
pdf_file = None
|
|
278
|
-
file_field: Optional[FieldFile] = None
|
|
279
264
|
try:
|
|
280
265
|
# Create the PDF record using the temporary file.
|
|
281
266
|
# delete_source=True will ensure temp_file_path is deleted by create_from_file
|
|
@@ -290,11 +275,8 @@ def get_default_egd_pdf():
|
|
|
290
275
|
raise RuntimeError("Failed to create PDF file object")
|
|
291
276
|
|
|
292
277
|
# Use storage API to check existence
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
raise RuntimeError("RawPdfFile.file did not return a FieldFile instance")
|
|
296
|
-
if not default_storage.exists(file_field.path):
|
|
297
|
-
raise RuntimeError(f"PDF file does not exist in storage at {file_field.path}")
|
|
278
|
+
if not default_storage.exists(pdf_file.file.path):
|
|
279
|
+
raise RuntimeError(f"PDF file does not exist in storage at {pdf_file.file.path}")
|
|
298
280
|
|
|
299
281
|
# Check that the source temp file was deleted
|
|
300
282
|
if temp_file_path.exists():
|
|
@@ -326,11 +308,10 @@ def get_default_egd_pdf():
|
|
|
326
308
|
|
|
327
309
|
# pdf_file.file.path might fail if storage doesn't support direct paths (like S3)
|
|
328
310
|
# Prefer using storage API for checks. Logging path if available.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
logger.info(f"PDF file created: {file_field.name}, Path: (Not available from storage)")
|
|
311
|
+
try:
|
|
312
|
+
logger.info(f"PDF file created: {pdf_file.file.name}, Path: {pdf_file.file.path}")
|
|
313
|
+
except NotImplementedError:
|
|
314
|
+
logger.info(f"PDF file created: {pdf_file.file.name}, Path: (Not available from storage)")
|
|
334
315
|
|
|
335
316
|
|
|
336
317
|
return pdf_file
|
|
@@ -294,12 +294,10 @@ class Command(BaseCommand):
|
|
|
294
294
|
|
|
295
295
|
# Updated to handle new return signature (path, metadata)
|
|
296
296
|
cleaned_video_path, extracted_metadata = frame_cleaner.clean_video(
|
|
297
|
-
|
|
297
|
+
Path(video_file_obj.raw_file.path),
|
|
298
298
|
video_file_obj=video_file_obj, # Pass VideoFile object to store metadata
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
output_path=video_file_obj.get_processed_file_path(),
|
|
302
|
-
technique="mask_overlay" # Use mask overlay technique as default, if not set this will be inferred.
|
|
299
|
+
report_reader=report_reader,
|
|
300
|
+
device_name=processor_name
|
|
303
301
|
)
|
|
304
302
|
|
|
305
303
|
# Save the cleaned video using Django's FileField
|
|
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Optional, Union, cast
|
|
|
7
7
|
|
|
8
8
|
from django.db import models
|
|
9
9
|
from django.core.files import File
|
|
10
|
-
from django.db.models.fields.files import FieldFile
|
|
11
10
|
from django.core.validators import FileExtensionValidator
|
|
12
11
|
from django.db.models import F
|
|
13
12
|
from endoreg_db.utils.calc_duration_seconds import _calc_duration_vf
|
|
@@ -128,36 +127,36 @@ class VideoFile(models.Model):
|
|
|
128
127
|
sensitive_meta = models.OneToOneField(
|
|
129
128
|
"SensitiveMeta", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
|
|
130
129
|
) # type: ignore
|
|
131
|
-
center = models.ForeignKey("Center", on_delete=models.PROTECT)
|
|
130
|
+
center = models.ForeignKey("Center", on_delete=models.PROTECT)
|
|
132
131
|
processor = models.ForeignKey(
|
|
133
132
|
"EndoscopyProcessor", on_delete=models.PROTECT, blank=True, null=True
|
|
134
|
-
)
|
|
133
|
+
)
|
|
135
134
|
video_meta = models.OneToOneField(
|
|
136
135
|
"VideoMeta", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
|
|
137
|
-
)
|
|
136
|
+
)
|
|
138
137
|
examination = models.ForeignKey(
|
|
139
138
|
"PatientExamination",
|
|
140
139
|
on_delete=models.SET_NULL,
|
|
141
140
|
blank=True,
|
|
142
141
|
null=True,
|
|
143
142
|
related_name="video_files",
|
|
144
|
-
)
|
|
143
|
+
)
|
|
145
144
|
patient = models.ForeignKey(
|
|
146
145
|
"Patient",
|
|
147
146
|
on_delete=models.SET_NULL,
|
|
148
147
|
blank=True,
|
|
149
148
|
null=True,
|
|
150
149
|
related_name="video_files",
|
|
151
|
-
)
|
|
150
|
+
)
|
|
152
151
|
ai_model_meta = models.ForeignKey(
|
|
153
152
|
"ModelMeta", on_delete=models.SET_NULL, blank=True, null=True
|
|
154
|
-
)
|
|
153
|
+
)
|
|
155
154
|
state = models.OneToOneField(
|
|
156
155
|
"VideoState", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
|
|
157
|
-
)
|
|
156
|
+
)
|
|
158
157
|
import_meta = models.OneToOneField(
|
|
159
158
|
"VideoImportMeta", on_delete=models.CASCADE, blank=True, null=True
|
|
160
|
-
)
|
|
159
|
+
)
|
|
161
160
|
|
|
162
161
|
original_file_name = models.CharField(max_length=255, blank=True, null=True)
|
|
163
162
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
@@ -198,9 +197,7 @@ class VideoFile(models.Model):
|
|
|
198
197
|
"""
|
|
199
198
|
from endoreg_db.models import FFMpegMeta
|
|
200
199
|
if self.video_meta is not None:
|
|
201
|
-
|
|
202
|
-
return self.video_meta.ffmpeg_meta
|
|
203
|
-
raise AssertionError("Expected FFMpegMeta instance.")
|
|
200
|
+
return self.video_meta.ffmpeg_meta
|
|
204
201
|
else:
|
|
205
202
|
self.initialize_video_specs()
|
|
206
203
|
ffmpeg_meta = self.video_meta.ffmpeg_meta if self.video_meta else None
|
|
@@ -219,19 +216,20 @@ class VideoFile(models.Model):
|
|
|
219
216
|
Raises:
|
|
220
217
|
Value Error if no active VideoFile is available.
|
|
221
218
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
219
|
+
_file = self.active_file
|
|
220
|
+
assert _file is not None, "No active file available. VideoFile has neither raw nor processed file."
|
|
221
|
+
if not _file or not _file.name:
|
|
222
|
+
raise ValueError("Active file has no associated file.")
|
|
223
|
+
url = _file.url
|
|
224
|
+
|
|
225
|
+
return url
|
|
228
226
|
|
|
229
227
|
@property
|
|
230
|
-
def active_raw_file(self) ->
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
228
|
+
def active_raw_file(self) -> File:
|
|
229
|
+
if self.has_raw:
|
|
230
|
+
return self.raw_file
|
|
231
|
+
else:
|
|
232
|
+
raise ValueError("Has no raw file")
|
|
235
233
|
|
|
236
234
|
@property
|
|
237
235
|
def active_raw_file_url(self)-> str:
|
|
@@ -243,10 +241,12 @@ class VideoFile(models.Model):
|
|
|
243
241
|
|
|
244
242
|
Returns:
|
|
245
243
|
"""
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
_file = self.active_raw_file
|
|
245
|
+
assert _file is not None, "No active file available. VideoFile has neither raw nor processed file."
|
|
246
|
+
if not _file or not _file.name:
|
|
247
|
+
raise ValueError("Active file has no associated file.")
|
|
248
|
+
url = _file.url
|
|
249
|
+
return url
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
# Pipeline Functions
|
|
@@ -363,7 +363,7 @@ class VideoFile(models.Model):
|
|
|
363
363
|
|
|
364
364
|
|
|
365
365
|
@property
|
|
366
|
-
def active_file(self) ->
|
|
366
|
+
def active_file(self) -> File:
|
|
367
367
|
"""
|
|
368
368
|
Return the active video file, preferring the processed file if available.
|
|
369
369
|
|
|
@@ -373,15 +373,12 @@ class VideoFile(models.Model):
|
|
|
373
373
|
Raises:
|
|
374
374
|
ValueError: If neither a processed nor a raw file is available.
|
|
375
375
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return raw
|
|
383
|
-
|
|
384
|
-
raise ValueError("No active file available. VideoFile has neither raw nor processed file.")
|
|
376
|
+
if self.is_processed:
|
|
377
|
+
return self.processed_file
|
|
378
|
+
elif self.has_raw:
|
|
379
|
+
return self.raw_file
|
|
380
|
+
else:
|
|
381
|
+
raise ValueError("No active file available. VideoFile has neither raw nor processed file.")
|
|
385
382
|
|
|
386
383
|
|
|
387
384
|
@property
|
|
@@ -396,17 +393,13 @@ class VideoFile(models.Model):
|
|
|
396
393
|
ValueError: If neither a processed nor raw file is present.
|
|
397
394
|
"""
|
|
398
395
|
active = self.active_file
|
|
399
|
-
if active
|
|
400
|
-
|
|
401
|
-
elif active
|
|
402
|
-
|
|
396
|
+
if active == self.processed_file:
|
|
397
|
+
return _get_processed_file_path(self)
|
|
398
|
+
elif active == self.raw_file:
|
|
399
|
+
return _get_raw_file_path(self)
|
|
403
400
|
else:
|
|
404
401
|
raise ValueError("No active file path available. VideoFile has neither raw nor processed file.")
|
|
405
402
|
|
|
406
|
-
if path is None:
|
|
407
|
-
raise ValueError("Active file path could not be resolved.")
|
|
408
|
-
return path
|
|
409
|
-
|
|
410
403
|
|
|
411
404
|
@classmethod
|
|
412
405
|
def create_from_file(cls, file_path: Union[str, Path], center_name: str, **kwargs) -> Optional["VideoFile"]:
|
|
@@ -455,14 +448,13 @@ class VideoFile(models.Model):
|
|
|
455
448
|
"""
|
|
456
449
|
# Ensure frames are deleted before the main instance
|
|
457
450
|
_delete_frames(self)
|
|
458
|
-
|
|
451
|
+
|
|
459
452
|
# Call the original delete method to remove the instance from the database
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
453
|
+
logger.info(f"Deleting VideoFile: {self.uuid} - {self.active_file_path}")
|
|
454
|
+
|
|
463
455
|
# Delete associated files if they exist
|
|
464
|
-
if
|
|
465
|
-
|
|
456
|
+
if self.active_file_path.exists():
|
|
457
|
+
self.active_file_path.unlink(missing_ok=True)
|
|
466
458
|
|
|
467
459
|
# Delete file storage
|
|
468
460
|
if self.raw_file and self.raw_file.storage.exists(self.raw_file.name):
|
|
@@ -487,9 +479,8 @@ class VideoFile(models.Model):
|
|
|
487
479
|
|
|
488
480
|
try:
|
|
489
481
|
# Call parent delete with proper parameters
|
|
490
|
-
|
|
482
|
+
super().delete(using=using, keep_parents=keep_parents)
|
|
491
483
|
logger.info(f"VideoFile {self.uuid} deleted successfully.")
|
|
492
|
-
return result
|
|
493
484
|
except Exception as e:
|
|
494
485
|
logger.error(f"Error deleting VideoFile {self.uuid}: {e}")
|
|
495
486
|
raise
|
|
@@ -37,7 +37,7 @@ class SensitiveMeta(models.Model):
|
|
|
37
37
|
blank=True,
|
|
38
38
|
null=True,
|
|
39
39
|
help_text="FK to the pseudo-anonymized Patient record."
|
|
40
|
-
)
|
|
40
|
+
)
|
|
41
41
|
patient_first_name = models.CharField(max_length=255, blank=True, null=True)
|
|
42
42
|
patient_last_name = models.CharField(max_length=255, blank=True, null=True)
|
|
43
43
|
patient_dob = models.DateTimeField(
|
|
@@ -51,24 +51,24 @@ class SensitiveMeta(models.Model):
|
|
|
51
51
|
blank=True,
|
|
52
52
|
null=True,
|
|
53
53
|
help_text="FK to the pseudo-anonymized PatientExamination record."
|
|
54
|
-
)
|
|
54
|
+
)
|
|
55
55
|
patient_gender = models.ForeignKey(
|
|
56
56
|
"Gender",
|
|
57
57
|
on_delete=models.CASCADE,
|
|
58
58
|
blank=True,
|
|
59
59
|
null=True,
|
|
60
|
-
)
|
|
60
|
+
)
|
|
61
61
|
examiners = models.ManyToManyField(
|
|
62
62
|
"Examiner",
|
|
63
63
|
blank=True,
|
|
64
64
|
help_text="Pseudo-anonymized examiner(s) associated with the examination."
|
|
65
|
-
)
|
|
65
|
+
)
|
|
66
66
|
center = models.ForeignKey(
|
|
67
67
|
"Center",
|
|
68
68
|
on_delete=models.CASCADE,
|
|
69
69
|
blank=True, # Should ideally be False if always required before save
|
|
70
70
|
null=True, # Should ideally be False
|
|
71
|
-
)
|
|
71
|
+
)
|
|
72
72
|
|
|
73
73
|
# Raw examiner names stored temporarily until pseudo-examiner is created/linked
|
|
74
74
|
examiner_first_name = models.CharField(max_length=255, blank=True, null=True, editable=False)
|
|
@@ -258,7 +258,7 @@ class SensitiveMeta(models.Model):
|
|
|
258
258
|
|
|
259
259
|
# 4. Handle ManyToMany linking (examiners) *after* the instance has a PK.
|
|
260
260
|
if examiner_to_link and self.pk and not self.examiners.filter(pk=examiner_to_link.pk).exists():
|
|
261
|
-
self.examiners.add(examiner_to_link)
|
|
261
|
+
self.examiners.add(examiner_to_link)
|
|
262
262
|
# Adding to M2M handles its own DB interaction, no second super().save() needed.
|
|
263
263
|
|
|
264
264
|
def mark_dob_verified(self):
|