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

@@ -1,4 +1,5 @@
1
1
  import random
2
+ from typing import Optional
2
3
  from endoreg_db.models import (
3
4
  Center,
4
5
  Gender,
@@ -17,6 +18,7 @@ import shutil
17
18
  from pathlib import Path
18
19
  from django.conf import settings # Import settings
19
20
  from django.core.files.storage import default_storage # Import default storage
21
+ from django.db.models.fields.files import FieldFile
20
22
 
21
23
  from endoreg_db.utils import (
22
24
  create_mock_patient_name,
@@ -44,6 +46,10 @@ DEFAULT_INDICATIONS = [
44
46
  DEFAULT_SEGMENTATION_MODEL_NAME = "image_multilabel_classification_colonoscopy_default"
45
47
 
46
48
  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)
47
53
 
48
54
  def get_information_source_prediction():
49
55
  """
@@ -170,33 +176,41 @@ def get_default_center() -> Center:
170
176
  return center
171
177
 
172
178
  def generate_patient(**kwargs) -> Patient:
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):
179
+ """Create a Patient with deterministic defaults unless ``randomize=True`` is supplied."""
180
+
181
+ randomize = kwargs.pop("randomize", False)
182
+
183
+ gender = kwargs.get("gender")
184
+ if gender is None:
185
+ if randomize:
186
+ gender = get_random_gender()
187
+ else:
188
+ gender = Gender.objects.get(name=DEFAULT_PATIENT_GENDER_NAME)
189
+ elif not isinstance(gender, Gender):
187
190
  gender = Gender.objects.get(name=gender)
188
191
 
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)
192
+ first_name = kwargs.get("first_name")
193
+ last_name = kwargs.get("last_name")
194
+ if first_name is None or last_name is None:
195
+ if randomize:
196
+ generated_first, generated_last = create_mock_patient_name(gender=gender.name)
197
+ else:
198
+ generated_first, generated_last = DEFAULT_PATIENT_FIRST_NAME, DEFAULT_PATIENT_LAST_NAME
199
+ first_name = first_name or generated_first
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")
197
211
  if center is None:
198
212
  center = get_default_center()
199
- else:
213
+ elif not isinstance(center, Center):
200
214
  center = Center.objects.get(name=center)
201
215
 
202
216
  patient = Patient(
@@ -261,6 +275,7 @@ def get_default_egd_pdf():
261
275
  shutil.copy(egd_path, temp_file_path)
262
276
 
263
277
  pdf_file = None
278
+ file_field: Optional[FieldFile] = None
264
279
  try:
265
280
  # Create the PDF record using the temporary file.
266
281
  # delete_source=True will ensure temp_file_path is deleted by create_from_file
@@ -275,8 +290,11 @@ def get_default_egd_pdf():
275
290
  raise RuntimeError("Failed to create PDF file object")
276
291
 
277
292
  # Use storage API to check existence
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}")
293
+ file_field = pdf_file.file
294
+ if not isinstance(file_field, FieldFile):
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}")
280
298
 
281
299
  # Check that the source temp file was deleted
282
300
  if temp_file_path.exists():
@@ -308,10 +326,11 @@ def get_default_egd_pdf():
308
326
 
309
327
  # pdf_file.file.path might fail if storage doesn't support direct paths (like S3)
310
328
  # Prefer using storage API for checks. Logging path if available.
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)")
329
+ if file_field is not None:
330
+ try:
331
+ logger.info(f"PDF file created: {file_field.name}, Path: {file_field.path}")
332
+ except NotImplementedError:
333
+ logger.info(f"PDF file created: {file_field.name}, Path: (Not available from storage)")
315
334
 
316
335
 
317
336
  return pdf_file
@@ -294,10 +294,12 @@ 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
- Path(video_file_obj.raw_file.path),
297
+ video_path=Path(video_file_obj.raw_file.path),
298
298
  video_file_obj=video_file_obj, # Pass VideoFile object to store metadata
299
- report_reader=report_reader,
300
- device_name=processor_name
299
+ device_name=processor_name,
300
+ endoscope_roi=video_file_obj.processor.get_roi_endoscope_image if video_file_obj.processor else None,
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.
301
303
  )
302
304
 
303
305
  # Save the cleaned video using Django's FileField
@@ -7,6 +7,7 @@ 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
10
11
  from django.core.validators import FileExtensionValidator
11
12
  from django.db.models import F
12
13
  from endoreg_db.utils.calc_duration_seconds import _calc_duration_vf
@@ -127,36 +128,36 @@ class VideoFile(models.Model):
127
128
  sensitive_meta = models.OneToOneField(
128
129
  "SensitiveMeta", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
129
130
  ) # type: ignore
130
- center = models.ForeignKey("Center", on_delete=models.PROTECT)
131
+ center = models.ForeignKey("Center", on_delete=models.PROTECT) # type: ignore
131
132
  processor = models.ForeignKey(
132
133
  "EndoscopyProcessor", on_delete=models.PROTECT, blank=True, null=True
133
- )
134
+ ) # type: ignore
134
135
  video_meta = models.OneToOneField(
135
136
  "VideoMeta", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
136
- )
137
+ ) # type: ignore
137
138
  examination = models.ForeignKey(
138
139
  "PatientExamination",
139
140
  on_delete=models.SET_NULL,
140
141
  blank=True,
141
142
  null=True,
142
143
  related_name="video_files",
143
- )
144
+ ) # type: ignore
144
145
  patient = models.ForeignKey(
145
146
  "Patient",
146
147
  on_delete=models.SET_NULL,
147
148
  blank=True,
148
149
  null=True,
149
150
  related_name="video_files",
150
- )
151
+ ) # type: ignore
151
152
  ai_model_meta = models.ForeignKey(
152
153
  "ModelMeta", on_delete=models.SET_NULL, blank=True, null=True
153
- )
154
+ ) # type: ignore
154
155
  state = models.OneToOneField(
155
156
  "VideoState", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
156
- )
157
+ ) # type: ignore
157
158
  import_meta = models.OneToOneField(
158
159
  "VideoImportMeta", on_delete=models.CASCADE, blank=True, null=True
159
- )
160
+ ) # type: ignore
160
161
 
161
162
  original_file_name = models.CharField(max_length=255, blank=True, null=True)
162
163
  uploaded_at = models.DateTimeField(auto_now_add=True)
@@ -197,7 +198,9 @@ class VideoFile(models.Model):
197
198
  """
198
199
  from endoreg_db.models import FFMpegMeta
199
200
  if self.video_meta is not None:
200
- return self.video_meta.ffmpeg_meta
201
+ if self.video_meta.ffmpeg_meta is not None:
202
+ return self.video_meta.ffmpeg_meta
203
+ raise AssertionError("Expected FFMpegMeta instance.")
201
204
  else:
202
205
  self.initialize_video_specs()
203
206
  ffmpeg_meta = self.video_meta.ffmpeg_meta if self.video_meta else None
@@ -216,20 +219,19 @@ class VideoFile(models.Model):
216
219
  Raises:
217
220
  Value Error if no active VideoFile is available.
218
221
  """
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
222
+ active = self.active_file
223
+ if not isinstance(active, FieldFile):
224
+ raise ValueError("Active file is not a stored FieldFile instance.")
225
+ if not active.name:
226
+ raise ValueError("Active file has no associated name.")
227
+ return active.url
226
228
 
227
229
  @property
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")
230
+ def active_raw_file(self) -> FieldFile:
231
+ raw = self.raw_file
232
+ if isinstance(raw, FieldFile) and raw.name:
233
+ return raw
234
+ raise ValueError("No raw file available for this video")
233
235
 
234
236
  @property
235
237
  def active_raw_file_url(self)-> str:
@@ -241,12 +243,10 @@ class VideoFile(models.Model):
241
243
 
242
244
  Returns:
243
245
  """
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
246
+ raw = self.active_raw_file
247
+ if not raw.name:
248
+ raise ValueError("Active raw file has no associated name.")
249
+ return raw.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) -> File:
366
+ def active_file(self) -> FieldFile:
367
367
  """
368
368
  Return the active video file, preferring the processed file if available.
369
369
 
@@ -373,12 +373,15 @@ class VideoFile(models.Model):
373
373
  Raises:
374
374
  ValueError: If neither a processed nor a raw file is available.
375
375
  """
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.")
376
+ processed = self.processed_file
377
+ if isinstance(processed, FieldFile) and processed.name:
378
+ return processed
379
+
380
+ raw = self.raw_file
381
+ if isinstance(raw, FieldFile) and raw.name:
382
+ return raw
383
+
384
+ raise ValueError("No active file available. VideoFile has neither raw nor processed file.")
382
385
 
383
386
 
384
387
  @property
@@ -393,13 +396,17 @@ class VideoFile(models.Model):
393
396
  ValueError: If neither a processed nor raw file is present.
394
397
  """
395
398
  active = self.active_file
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)
399
+ if active is self.processed_file:
400
+ path = _get_processed_file_path(self)
401
+ elif active is self.raw_file:
402
+ path = _get_raw_file_path(self)
400
403
  else:
401
404
  raise ValueError("No active file path available. VideoFile has neither raw nor processed file.")
402
405
 
406
+ if path is None:
407
+ raise ValueError("Active file path could not be resolved.")
408
+ return path
409
+
403
410
 
404
411
  @classmethod
405
412
  def create_from_file(cls, file_path: Union[str, Path], center_name: str, **kwargs) -> Optional["VideoFile"]:
@@ -448,13 +455,14 @@ class VideoFile(models.Model):
448
455
  """
449
456
  # Ensure frames are deleted before the main instance
450
457
  _delete_frames(self)
451
-
458
+
452
459
  # Call the original delete method to remove the instance from the database
453
- logger.info(f"Deleting VideoFile: {self.uuid} - {self.active_file_path}")
454
-
460
+ active_path = self.active_file_path
461
+ logger.info(f"Deleting VideoFile: {self.uuid} - {active_path}")
462
+
455
463
  # Delete associated files if they exist
456
- if self.active_file_path.exists():
457
- self.active_file_path.unlink(missing_ok=True)
464
+ if active_path.exists():
465
+ active_path.unlink(missing_ok=True)
458
466
 
459
467
  # Delete file storage
460
468
  if self.raw_file and self.raw_file.storage.exists(self.raw_file.name):
@@ -479,8 +487,9 @@ class VideoFile(models.Model):
479
487
 
480
488
  try:
481
489
  # Call parent delete with proper parameters
482
- super().delete(using=using, keep_parents=keep_parents)
490
+ result = super().delete(using=using, keep_parents=keep_parents)
483
491
  logger.info(f"VideoFile {self.uuid} deleted successfully.")
492
+ return result
484
493
  except Exception as e:
485
494
  logger.error(f"Error deleting VideoFile {self.uuid}: {e}")
486
495
  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
+ ) # type: ignore
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
+ ) # type: ignore
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
+ ) # type: ignore
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
+ ) # type: ignore
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
+ ) # type: ignore
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) # type: ignore
262
262
  # Adding to M2M handles its own DB interaction, no second super().save() needed.
263
263
 
264
264
  def mark_dob_verified(self):
@@ -56,7 +56,7 @@ def generate_patient_pseudonym(patient: Patient) -> Tuple[str, bool]:
56
56
  patient.patient_hash = patient_hash
57
57
  patient.save(update_fields=['patient_hash'])
58
58
 
59
- logger.info(f"Generated and persisted pseudonym for patient {patient.id}: {patient_hash[:8]}...")
59
+ logger.info(f"Generated and persisted pseudonym for patient {patient.id}")
60
60
 
61
61
  return patient_hash, True
62
62