endoreg-db 0.8.2.2__py3-none-any.whl → 0.8.2.4__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,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
- """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):
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
- 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")
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
- elif not isinstance(center, Center):
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
- 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}")
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
- 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)")
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
- video_path=Path(video_file_obj.raw_file.path),
297
+ Path(video_file_obj.raw_file.path),
298
298
  video_file_obj=video_file_obj, # Pass VideoFile object to store metadata
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.
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) # type: ignore
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
- ) # type: ignore
133
+ )
135
134
  video_meta = models.OneToOneField(
136
135
  "VideoMeta", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
137
- ) # type: ignore
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
- ) # type: ignore
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
- ) # type: ignore
150
+ )
152
151
  ai_model_meta = models.ForeignKey(
153
152
  "ModelMeta", on_delete=models.SET_NULL, blank=True, null=True
154
- ) # type: ignore
153
+ )
155
154
  state = models.OneToOneField(
156
155
  "VideoState", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
157
- ) # type: ignore
156
+ )
158
157
  import_meta = models.OneToOneField(
159
158
  "VideoImportMeta", on_delete=models.CASCADE, blank=True, null=True
160
- ) # type: ignore
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
- if self.video_meta.ffmpeg_meta is not None:
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
- 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
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) -> 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")
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
- raw = self.active_raw_file
247
- if not raw.name:
248
- raise ValueError("Active raw file has no associated name.")
249
- return raw.url
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) -> FieldFile:
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
- 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.")
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 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)
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
- active_path = self.active_file_path
461
- logger.info(f"Deleting VideoFile: {self.uuid} - {active_path}")
462
-
453
+ logger.info(f"Deleting VideoFile: {self.uuid} - {self.active_file_path}")
454
+
463
455
  # Delete associated files if they exist
464
- if active_path.exists():
465
- active_path.unlink(missing_ok=True)
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
- result = super().delete(using=using, keep_parents=keep_parents)
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
- ) # type: ignore
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
- ) # type: ignore
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
- ) # type: ignore
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
- ) # type: ignore
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
- ) # type: ignore
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) # type: ignore
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):