endoreg-db 0.8.3.4__py3-none-any.whl → 0.8.3.7__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.
@@ -50,17 +50,11 @@ class Command(BaseCommand):
50
50
 
51
51
  try:
52
52
  # Download the model weights
53
- weights_path = hf_hub_download(
54
- repo_id=model_id,
55
- filename="colo_segmentation_RegNetX800MF_base.ckpt",
56
- local_dir="/tmp",
57
- )
53
+ weights_path = hf_hub_download(repo_id=model_id, filename="pytorch_model.bin", local_dir="/tmp")
58
54
  self.stdout.write(f"Downloaded weights to: {weights_path}")
59
55
 
60
56
  # Get or create AI model
61
- ai_model, created = AiModel.objects.get_or_create(
62
- name=model_name, defaults={"description": f"Model from {model_id}"}
63
- )
57
+ ai_model, created = AiModel.objects.get_or_create(name=model_name, defaults={"description": f"Model from {model_id}"})
64
58
  if created:
65
59
  self.stdout.write(f"Created AI model: {ai_model.name}")
66
60
 
@@ -68,9 +62,7 @@ class Command(BaseCommand):
68
62
  try:
69
63
  labelset = LabelSet.objects.get(name=labelset_name)
70
64
  except LabelSet.DoesNotExist:
71
- self.stdout.write(
72
- self.style.ERROR(f"LabelSet '{labelset_name}' not found")
73
- )
65
+ self.stdout.write(self.style.ERROR(f"LabelSet '{labelset_name}' not found"))
74
66
  return
75
67
 
76
68
  # Create ModelMeta
@@ -94,20 +86,13 @@ class Command(BaseCommand):
94
86
 
95
87
  # Save the weights file to the model
96
88
  with open(weights_path, "rb") as f:
97
- model_meta.weights.save(
98
- f"{model_name}_v{version}_colo_segmentation_RegNetX800MF_base.ckpt",
99
- ContentFile(f.read()),
100
- )
89
+ model_meta.weights.save(f"{model_name}_v{version}_pytorch_model.bin", ContentFile(f.read()))
101
90
 
102
91
  # Set as active meta
103
92
  ai_model.active_meta = model_meta
104
93
  ai_model.save()
105
94
 
106
- self.stdout.write(
107
- self.style.SUCCESS(
108
- f"Successfully {'created' if created else 'updated'} ModelMeta: {model_meta}"
109
- )
110
- )
95
+ self.stdout.write(self.style.SUCCESS(f"Successfully {'created' if created else 'updated'} ModelMeta: {model_meta}"))
111
96
 
112
97
  except Exception as e:
113
98
  self.stdout.write(self.style.ERROR(f"Error creating ModelMeta: {e}"))
@@ -1,21 +1,21 @@
1
1
  import shutil
2
- from logging import getLogger
3
2
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Any, Optional, Type
5
-
6
- from django.db import transaction
3
+ from typing import Optional, TYPE_CHECKING, Any, Type
7
4
  from huggingface_hub import hf_hub_download
5
+ from django.db import transaction
8
6
 
9
7
  # Assuming ModelMeta, AiModel, LabelSet are importable from the correct locations
10
8
  # Adjust imports based on your project structure if necessary
11
9
  from ..administration.ai.ai_model import AiModel
12
10
  from ..label.label_set import LabelSet
13
- from ..utils import STORAGE_DIR, WEIGHTS_DIR
11
+ from ..utils import WEIGHTS_DIR, STORAGE_DIR
12
+
13
+ from logging import getLogger
14
14
 
15
15
  logger = getLogger("ai_model")
16
16
 
17
17
  if TYPE_CHECKING:
18
- from .model_meta import ModelMeta # Import ModelMeta for type hinting
18
+ from .model_meta import ModelMeta # Import ModelMeta for type hinting
19
19
 
20
20
 
21
21
  def get_latest_version_number_logic(
@@ -29,13 +29,13 @@ def get_latest_version_number_logic(
29
29
  """
30
30
  versions_qs = cls.objects.filter(
31
31
  name=meta_name, model__name=model_name
32
- ).values_list("version", flat=True)
32
+ ).values_list('version', flat=True)
33
33
 
34
34
  max_v = 0
35
35
  found_numeric_version = False
36
36
 
37
37
  for v_str in versions_qs:
38
- if v_str is None: # Skip None versions
38
+ if v_str is None: # Skip None versions
39
39
  continue
40
40
  try:
41
41
  v_int = int(v_str)
@@ -47,13 +47,13 @@ def get_latest_version_number_logic(
47
47
  f"Warning: Could not parse version string '{v_str}' as an integer for "
48
48
  f"meta_name='{meta_name}', model_name='{model_name}' while determining the max version."
49
49
  )
50
-
50
+
51
51
  return max_v if found_numeric_version else 0
52
52
 
53
53
 
54
54
  @transaction.atomic
55
55
  def create_from_file_logic(
56
- cls: Type["ModelMeta"], # cls is ModelMeta
56
+ cls: Type["ModelMeta"], # cls is ModelMeta
57
57
  meta_name: str,
58
58
  model_name: str,
59
59
  labelset_name: str,
@@ -94,14 +94,11 @@ def create_from_file_logic(
94
94
  )
95
95
  elif existing and bump_if_exists:
96
96
  target_version = str(latest_version_num + 1)
97
- logger.info(
98
- f"Bumping version for {meta_name}/{model_name} to {target_version}"
99
- )
97
+ logger.info(f"Bumping version for {meta_name}/{model_name} to {target_version}")
100
98
  else:
101
99
  target_version = str(latest_version_num + 1)
102
- logger.info(
103
- f"Setting next version for {meta_name}/{model_name} to {target_version}"
104
- )
100
+ logger.info(f"Setting next version for {meta_name}/{model_name} to {target_version}")
101
+
105
102
 
106
103
  # --- Prepare Weights File ---
107
104
  source_weights_path = Path(weights_file).resolve()
@@ -111,10 +108,7 @@ def create_from_file_logic(
111
108
  # Construct destination path within MEDIA_ROOT/WEIGHTS_DIR
112
109
  weights_filename = source_weights_path.name
113
110
  # Relative path for the FileField upload_to
114
- relative_dest_path = (
115
- Path(WEIGHTS_DIR.relative_to(STORAGE_DIR))
116
- / f"{meta_name}_v{target_version}_{weights_filename}"
117
- )
111
+ relative_dest_path = Path(WEIGHTS_DIR.relative_to(STORAGE_DIR)) / f"{meta_name}_v{target_version}_{weights_filename}"
118
112
  # Full path for shutil.copy
119
113
  full_dest_path = STORAGE_DIR / relative_dest_path
120
114
 
@@ -131,8 +125,8 @@ def create_from_file_logic(
131
125
  # --- Create/Update ModelMeta Instance ---
132
126
  defaults = {
133
127
  "labelset": label_set,
134
- "weights": relative_dest_path.as_posix(), # Store relative path for FileField
135
- **kwargs, # Pass through other fields like activation, mean, std, etc.
128
+ "weights": relative_dest_path.as_posix(), # Store relative path for FileField
129
+ **kwargs, # Pass through other fields like activation, mean, std, etc.
136
130
  }
137
131
 
138
132
  # Remove None values from defaults to avoid overriding model defaults unnecessarily
@@ -158,39 +152,35 @@ def create_from_file_logic(
158
152
 
159
153
  return model_meta
160
154
 
161
-
162
155
  # --- Add other logic functions referenced by ModelMeta here ---
163
156
  # (get_latest_version_number_logic, get_activation_function_logic, etc.)
164
157
  # Placeholder for get_activation_function_logic
165
158
  def get_activation_function_logic(activation_name: str):
166
- import torch.nn as nn # Import locally as it's specific to this function
167
-
159
+ import torch.nn as nn # Import locally as it's specific to this function
168
160
  if activation_name.lower() == "sigmoid":
169
161
  return nn.Sigmoid()
170
162
  elif activation_name.lower() == "softmax":
171
163
  # Note: Softmax usually requires specifying the dimension
172
- return nn.Softmax(dim=1) # Assuming dim=1 (channels) is common
164
+ return nn.Softmax(dim=1) # Assuming dim=1 (channels) is common
173
165
  elif activation_name.lower() == "none":
174
166
  return nn.Identity()
175
167
  else:
176
168
  # Consider adding more activations or raising an error
177
169
  raise ValueError(f"Unsupported activation function: {activation_name}")
178
170
 
179
-
180
171
  # Placeholder for get_inference_dataset_config_logic
181
172
  def get_inference_dataset_config_logic(model_meta: "ModelMeta") -> dict:
182
173
  # This would typically extract relevant fields from model_meta
183
174
  # for configuring a dataset during inference
184
175
  return {
185
- "mean": [float(x) for x in model_meta.mean.split(",")],
186
- "std": [float(x) for x in model_meta.std.split(",")],
187
- "size_y": model_meta.size_y, # Add size_y key
188
- "size_x": model_meta.size_x, # Add size_x key
189
- "axes": [int(x) for x in model_meta.axes.split(",")],
176
+ "mean": [float(x) for x in model_meta.mean.split(',')],
177
+ "std": [float(x) for x in model_meta.std.split(',')],
178
+ "size_y": model_meta.size_y, # Add size_y key
179
+ "size_x": model_meta.size_x, # Add size_x key
180
+ "axes": [int(x) for x in model_meta.axes.split(',')],
190
181
  # Add other relevant config like normalization type, etc.
191
182
  }
192
183
 
193
-
194
184
  # Placeholder for get_config_dict_logic
195
185
  def get_config_dict_logic(model_meta: "ModelMeta") -> dict:
196
186
  # Returns a dictionary representation of the model's configuration
@@ -212,7 +202,6 @@ def get_config_dict_logic(model_meta: "ModelMeta") -> dict:
212
202
  # Add any other relevant fields
213
203
  }
214
204
 
215
-
216
205
  # Placeholder for get_model_meta_by_name_version_logic
217
206
  def get_model_meta_by_name_version_logic(
218
207
  cls: Type["ModelMeta"],
@@ -238,23 +227,16 @@ def get_model_meta_by_name_version_logic(
238
227
  ) from exc
239
228
  else:
240
229
  # Get latest version
241
- latest = (
242
- cls.objects.filter(name=meta_name, model=ai_model)
243
- .order_by("-date_created")
244
- .first()
245
- )
230
+ latest = cls.objects.filter(name=meta_name, model=ai_model).order_by("-date_created").first()
246
231
  if latest:
247
232
  return latest
248
233
  else:
249
234
  raise cls.DoesNotExist(
250
235
  f"No ModelMeta found for '{meta_name}' and model '{model_name}'."
251
236
  )
252
-
253
-
254
- import re
255
-
237
+
256
238
  from huggingface_hub import model_info
257
-
239
+ import re
258
240
 
259
241
  def infer_default_model_meta_from_hf(model_id: str) -> dict[str, Any]:
260
242
  """
@@ -266,9 +248,7 @@ def infer_default_model_meta_from_hf(model_id: str) -> dict[str, Any]:
266
248
  """
267
249
 
268
250
  if not (info := model_info(model_id)):
269
- logger.info(
270
- f"Could not retrieve model info for {model_id}, using ColoReg segmentation defaults."
271
- )
251
+ logger.info(f"Could not retrieve model info for {model_id}, using ColoReg segmentation defaults.")
272
252
  return {
273
253
  "name": "wg-lux/colo_segmentation_RegNetX800MF_base",
274
254
  "activation": "sigmoid",
@@ -315,29 +295,18 @@ def infer_default_model_meta_from_hf(model_id: str) -> dict[str, Any]:
315
295
  "size_y": size_y,
316
296
  "description": f"Inferred defaults for {model_id}",
317
297
  }
318
-
319
-
320
- def setup_default_from_huggingface_logic(
321
- cls, model_id: str, labelset_name: str | None = None
322
- ):
298
+
299
+ def setup_default_from_huggingface_logic(cls, model_id: str, labelset_name: str | None = None):
323
300
  """
324
301
  Downloads model weights from Hugging Face and auto-fills ModelMeta fields.
325
302
  """
326
303
  meta = infer_default_model_meta_from_hf(model_id)
327
304
 
328
305
  # Download weights
329
- weights_path = hf_hub_download(
330
- repo_id=model_id,
331
- filename="colo_segmentation_RegNetX800MF_base.ckpt",
332
- local_dir=WEIGHTS_DIR,
333
- )
306
+ weights_path = hf_hub_download(repo_id=model_id, filename="pytorch_model.bin", local_dir=WEIGHTS_DIR)
334
307
 
335
308
  ai_model, _ = AiModel.objects.get_or_create(name=meta["name"])
336
- labelset = (
337
- LabelSet.objects.first()
338
- if not labelset_name
339
- else LabelSet.objects.get(name=labelset_name)
340
- )
309
+ labelset = LabelSet.objects.first() if not labelset_name else LabelSet.objects.get(name=labelset_name)
341
310
 
342
311
  return create_from_file_logic(
343
312
  cls,
@@ -2,26 +2,26 @@ import logging
2
2
  import os
3
3
  import random
4
4
  import re # Neu hinzugefügt für Regex-Pattern
5
+ from datetime import date, datetime, timedelta
5
6
  from hashlib import sha256
6
- from datetime import datetime, timedelta, date
7
- from typing import TYPE_CHECKING, Dict, Any, Optional, Type
7
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Type
8
8
 
9
- from django.utils import timezone
10
9
  from django.db import transaction
10
+ from django.utils import timezone
11
11
 
12
- # Assuming these utils are correctly located
13
- from endoreg_db.utils.hashs import get_patient_hash, get_patient_examination_hash
14
12
  from endoreg_db.utils import guess_name_gender
15
13
 
14
+ # Assuming these utils are correctly located
15
+ from endoreg_db.utils.hashs import get_patient_examination_hash, get_patient_hash
16
+
16
17
  # Import models needed for logic, use local imports inside functions if needed to break cycles
17
- from ..administration import Center, Examiner, Patient, FirstName, LastName
18
- from ..other import Gender
18
+ from ..administration import Center, Examiner, FirstName, LastName, Patient
19
19
  from ..medical import PatientExamination
20
+ from ..other import Gender
20
21
  from ..state import SensitiveMetaState
21
22
 
22
-
23
23
  if TYPE_CHECKING:
24
- from .sensitive_meta import SensitiveMeta # Import model for type hinting
24
+ from .sensitive_meta import SensitiveMeta # Import model for type hinting
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
27
  SECRET_SALT = os.getenv("DJANGO_SALT", "default_salt")
@@ -35,23 +35,23 @@ DE_RX = re.compile(r"^\d{2}\.\d{2}\.\d{4}$")
35
35
  def parse_any_date(s: str) -> Optional[date]:
36
36
  """
37
37
  Parst Datumsstring mit Priorität auf deutsches Format (DD.MM.YYYY).
38
-
38
+
39
39
  Unterstützte Formate:
40
40
  1. DD.MM.YYYY (Priorität) - deutsches Format
41
41
  2. YYYY-MM-DD (Fallback) - ISO-Format
42
42
  3. Erweiterte Fallbacks über dateparser
43
-
43
+
44
44
  Args:
45
45
  s: Datumsstring zum Parsen
46
-
46
+
47
47
  Returns:
48
48
  date-Objekt oder None bei ungültigem/fehlendem Input
49
49
  """
50
50
  if not s:
51
51
  return None
52
-
52
+
53
53
  s = s.strip()
54
-
54
+
55
55
  # 1. German dd.mm.yyyy (PRIORITÄT)
56
56
  if DE_RX.match(s):
57
57
  try:
@@ -60,7 +60,7 @@ def parse_any_date(s: str) -> Optional[date]:
60
60
  except ValueError as e:
61
61
  logger.warning(f"Invalid German date format '{s}': {e}")
62
62
  return None
63
-
63
+
64
64
  # 2. ISO yyyy-mm-dd (Fallback für Rückwärtskompatibilität)
65
65
  if ISO_RX.match(s):
66
66
  try:
@@ -68,23 +68,20 @@ def parse_any_date(s: str) -> Optional[date]:
68
68
  except ValueError as e:
69
69
  logger.warning(f"Invalid ISO date format '{s}': {e}")
70
70
  return None
71
-
71
+
72
72
  # 3. Extended fallbacks
73
73
  try:
74
74
  # Try standard datetime parsing
75
75
  return datetime.fromisoformat(s).date()
76
76
  except Exception:
77
77
  pass
78
-
78
+
79
79
  try:
80
80
  # Try dateparser with German locale preference
81
81
  import dateparser
82
+
82
83
  dt = dateparser.parse(
83
- s,
84
- settings={
85
- "DATE_ORDER": "DMY",
86
- "PREFER_DAY_OF_MONTH": "first"
87
- }
84
+ s, settings={"DATE_ORDER": "DMY", "PREFER_DAY_OF_MONTH": "first"}
88
85
  )
89
86
  return dt.date() if dt else None
90
87
  except Exception as e:
@@ -95,10 +92,10 @@ def parse_any_date(s: str) -> Optional[date]:
95
92
  def format_date_german(d: Optional[date]) -> str:
96
93
  """
97
94
  Formatiert date-Objekt als deutsches Datumsformat (DD.MM.YYYY).
98
-
95
+
99
96
  Args:
100
97
  d: date-Objekt oder None
101
-
98
+
102
99
  Returns:
103
100
  Formatiertes Datum als String oder leerer String bei None
104
101
  """
@@ -110,10 +107,10 @@ def format_date_german(d: Optional[date]) -> str:
110
107
  def format_date_iso(d: Optional[date]) -> str:
111
108
  """
112
109
  Formatiert date-Objekt als ISO-Format (YYYY-MM-DD).
113
-
110
+
114
111
  Args:
115
112
  d: date-Objekt oder None
116
-
113
+
117
114
  Returns:
118
115
  Formatiertes Datum als String oder leerer String bei None
119
116
  """
@@ -137,7 +134,7 @@ def generate_random_dob() -> datetime:
137
134
  def generate_random_examination_date() -> date:
138
135
  """Generates a random date within the last 20 years."""
139
136
  today = date.today()
140
- start_date = today - timedelta(days=20 * 365) # Approximate 20 years back
137
+ start_date = today - timedelta(days=20 * 365) # Approximate 20 years back
141
138
  time_between_dates = today - start_date
142
139
  days_between_dates = time_between_dates.days
143
140
  random_number_of_days = random.randrange(days_between_dates)
@@ -169,13 +166,15 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
169
166
  first_name=first_name,
170
167
  last_name=last_name,
171
168
  dob=dob,
172
- center=center.name, # Use center name
169
+ center=center.name, # Use center name
173
170
  salt=salt,
174
171
  )
175
172
  return sha256(hash_str.encode()).hexdigest()
176
173
 
177
174
 
178
- def calculate_examination_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -> str:
175
+ def calculate_examination_hash(
176
+ instance: "SensitiveMeta", salt: str = SECRET_SALT
177
+ ) -> str:
179
178
  """Calculates the examination hash for the instance."""
180
179
  dob = instance.patient_dob
181
180
  first_name = instance.patient_first_name
@@ -190,13 +189,12 @@ def calculate_examination_hash(instance: "SensitiveMeta", salt: str = SECRET_SAL
190
189
  if not center:
191
190
  raise ValueError("Center is required to calculate examination hash.")
192
191
 
193
-
194
192
  hash_str = get_patient_examination_hash(
195
193
  first_name=first_name,
196
194
  last_name=last_name,
197
195
  dob=dob,
198
196
  examination_date=examination_date,
199
- center=center.name, # Use center name
197
+ center=center.name, # Use center name
200
198
  salt=salt,
201
199
  )
202
200
  return sha256(hash_str.encode()).hexdigest()
@@ -206,15 +204,19 @@ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
206
204
  """Creates or retrieves the pseudo examiner based on instance data."""
207
205
  first_name = instance.examiner_first_name
208
206
  last_name = instance.examiner_last_name
209
- center = instance.center # Should be set before calling save
207
+ center = instance.center # Should be set before calling save
210
208
 
211
209
  if not first_name or not last_name or not center:
212
- logger.warning(f"Incomplete examiner info for SensitiveMeta (pk={instance.pk}). Using default examiner.")
210
+ logger.warning(
211
+ f"Incomplete examiner info for SensitiveMeta (pk={instance.pk}). Using default examiner."
212
+ )
213
213
  # Ensure default center exists or handle appropriately
214
214
  try:
215
215
  default_center = Center.objects.get_by_natural_key("endoreg_db_demo")
216
216
  except Center.DoesNotExist:
217
- logger.error("Default center 'endoreg_db_demo' not found. Cannot create default examiner.")
217
+ logger.error(
218
+ "Default center 'endoreg_db_demo' not found. Cannot create default examiner."
219
+ )
218
220
  raise ValueError("Default center 'endoreg_db_demo' not found.")
219
221
 
220
222
  examiner, _created = Examiner.custom_get_or_create(
@@ -232,7 +234,7 @@ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
232
234
  """Gets or creates the pseudo patient based on instance data."""
233
235
  # Ensure necessary fields are set
234
236
  if not instance.patient_hash:
235
- instance.patient_hash = calculate_patient_hash(instance)
237
+ instance.patient_hash = calculate_patient_hash(instance)
236
238
  if not instance.center:
237
239
  raise ValueError("Center must be set before creating pseudo patient.")
238
240
  if not instance.patient_gender:
@@ -254,29 +256,33 @@ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
254
256
  return patient
255
257
 
256
258
 
257
- def get_or_create_pseudo_patient_examination_logic(instance: "SensitiveMeta") -> "PatientExamination":
259
+ def get_or_create_pseudo_patient_examination_logic(
260
+ instance: "SensitiveMeta",
261
+ ) -> "PatientExamination":
258
262
  """Gets or creates the pseudo patient examination based on instance data."""
259
263
  # Ensure necessary fields are set
260
264
  if not instance.patient_hash:
261
- instance.patient_hash = calculate_patient_hash(instance)
265
+ instance.patient_hash = calculate_patient_hash(instance)
262
266
  if not instance.examination_hash:
263
- instance.examination_hash = calculate_examination_hash(instance)
267
+ instance.examination_hash = calculate_examination_hash(instance)
264
268
 
265
269
  # Ensure the pseudo patient exists first, as PatientExamination might depend on it
266
270
  if not instance.pseudo_patient_id:
267
271
  pseudo_patient = get_or_create_pseudo_patient_logic(instance)
268
- instance.pseudo_patient_id = pseudo_patient.pk # Assign FK directly
269
-
270
- patient_examination, _created = PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
271
- patient_hash=instance.patient_hash,
272
- examination_hash=instance.examination_hash,
273
- # Optionally pass pseudo_patient if the method requires it
274
- # pseudo_patient=instance.pseudo_patient
272
+ instance.pseudo_patient_id = pseudo_patient.pk # Assign FK directly
273
+
274
+ patient_examination, _created = (
275
+ PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
276
+ patient_hash=instance.patient_hash,
277
+ examination_hash=instance.examination_hash,
278
+ # Optionally pass pseudo_patient if the method requires it
279
+ # pseudo_patient=instance.pseudo_patient
280
+ )
275
281
  )
276
282
  return patient_examination
277
283
 
278
284
 
279
- @transaction.atomic # Ensure all operations within save succeed or fail together
285
+ @transaction.atomic # Ensure all operations within save succeed or fail together
280
286
  def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
281
287
  """
282
288
  Contains the core logic for preparing a SensitiveMeta instance for saving.
@@ -288,10 +294,14 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
288
294
 
289
295
  # 1. Ensure DOB and Examination Date exist
290
296
  if not instance.patient_dob:
291
- logger.debug(f"SensitiveMeta (pk={instance.pk}): Patient DOB missing, generating random.")
297
+ logger.debug(
298
+ f"SensitiveMeta (pk={instance.pk}): Patient DOB missing, generating random."
299
+ )
292
300
  instance.patient_dob = generate_random_dob()
293
301
  if not instance.examination_date:
294
- logger.debug(f"SensitiveMeta (pk={instance.pk}): Examination date missing, generating random.")
302
+ logger.debug(
303
+ f"SensitiveMeta (pk={instance.pk}): Examination date missing, generating random."
304
+ )
295
305
  instance.examination_date = generate_random_examination_date()
296
306
 
297
307
  # 2. Ensure Center exists (should be set before calling save)
@@ -300,13 +310,14 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
300
310
 
301
311
  # 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
302
312
  if not instance.patient_gender:
303
- # Attempt to guess if names are available
304
- first_name = instance.patient_first_name or DEFAULT_UNKNOWN_NAME
305
- gender = guess_name_gender(first_name)
306
- if not gender:
307
- raise ValueError("Patient gender could not be determined and must be set before saving.")
308
- instance.patient_gender = gender
309
-
313
+ # Attempt to guess if names are available
314
+ first_name = instance.patient_first_name or DEFAULT_UNKNOWN_NAME
315
+ gender = guess_name_gender(first_name)
316
+ if not gender:
317
+ raise ValueError(
318
+ "Patient gender could not be determined and must be set before saving."
319
+ )
320
+ instance.patient_gender = gender
310
321
 
311
322
  # 4. Calculate Hashes (depends on DOB, Exam Date, Center, Names)
312
323
  instance.patient_hash = calculate_patient_hash(instance)
@@ -333,10 +344,16 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
333
344
  return examiner_instance
334
345
 
335
346
 
336
- def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str, Any]) -> "SensitiveMeta":
347
+ def create_sensitive_meta_from_dict(
348
+ cls: Type["SensitiveMeta"], data: Dict[str, Any]
349
+ ) -> "SensitiveMeta":
337
350
  """Logic to create a SensitiveMeta instance from a dictionary."""
338
351
 
339
- field_names = {f.name for f in cls._meta.get_fields() if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)}
352
+ field_names = {
353
+ f.name
354
+ for f in cls._meta.get_fields()
355
+ if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
356
+ }
340
357
  selected_data = {k: v for k, v in data.items() if k in field_names}
341
358
 
342
359
  # --- Convert patient_dob if it's a date object ---
@@ -348,64 +365,123 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
348
365
  logger.debug("Converted patient_dob from date to aware datetime: %s", aware_dob)
349
366
  elif isinstance(dob, str):
350
367
  # Handle string DOB - check if it's a field name or actual date
351
- if dob == "patient_dob" or dob in ["patient_first_name", "patient_last_name", "examination_date"]:
352
- logger.warning("Skipping invalid patient_dob value '%s' - appears to be field name", dob)
368
+ if dob == "patient_dob" or dob in [
369
+ "patient_first_name",
370
+ "patient_last_name",
371
+ "examination_date",
372
+ ]:
373
+ logger.warning(
374
+ "Skipping invalid patient_dob value '%s' - appears to be field name",
375
+ dob,
376
+ )
353
377
  selected_data.pop("patient_dob", None) # Remove invalid value
354
378
  else:
355
379
  # Try to parse as date string
356
380
  try:
357
381
  import dateparser
358
- parsed_dob = dateparser.parse(dob, languages=['de'], settings={'DATE_ORDER': 'DMY'})
382
+
383
+ parsed_dob = dateparser.parse(
384
+ dob, languages=["de"], settings={"DATE_ORDER": "DMY"}
385
+ )
359
386
  if parsed_dob:
360
- aware_dob = timezone.make_aware(parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0))
387
+ aware_dob = timezone.make_aware(
388
+ parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0)
389
+ )
361
390
  selected_data["patient_dob"] = aware_dob
362
- logger.debug("Parsed string patient_dob '%s' to aware datetime: %s", dob, aware_dob)
391
+ logger.debug(
392
+ "Parsed string patient_dob '%s' to aware datetime: %s",
393
+ dob,
394
+ aware_dob,
395
+ )
363
396
  else:
364
- logger.warning("Could not parse patient_dob string '%s', removing from data", dob)
397
+ logger.warning(
398
+ "Could not parse patient_dob string '%s', removing from data",
399
+ dob,
400
+ )
365
401
  selected_data.pop("patient_dob", None)
366
402
  except Exception as e:
367
- logger.warning("Error parsing patient_dob string '%s': %s, removing from data", dob, e)
403
+ logger.warning(
404
+ "Error parsing patient_dob string '%s': %s, removing from data",
405
+ dob,
406
+ e,
407
+ )
368
408
  selected_data.pop("patient_dob", None)
369
409
  # --- End Conversion ---
370
-
410
+
371
411
  # Similar validation for examination_date
372
412
  exam_date = selected_data.get("examination_date")
373
413
  if isinstance(exam_date, str):
374
- if exam_date == "examination_date" or exam_date in ["patient_first_name", "patient_last_name", "patient_dob"]:
375
- logger.warning("Skipping invalid examination_date value '%s' - appears to be field name", exam_date)
414
+ if exam_date == "examination_date" or exam_date in [
415
+ "patient_first_name",
416
+ "patient_last_name",
417
+ "patient_dob",
418
+ ]:
419
+ logger.warning(
420
+ "Skipping invalid examination_date value '%s' - appears to be field name",
421
+ exam_date,
422
+ )
376
423
  selected_data.pop("examination_date", None)
377
424
  else:
378
425
  # Try to parse as date string
379
426
  try:
380
427
  # First try simple ISO format for YYYY-MM-DD
381
- if len(exam_date) == 10 and exam_date.count('-') == 2:
428
+ if len(exam_date) == 10 and exam_date.count("-") == 2:
382
429
  try:
383
430
  from datetime import datetime as dt
384
- parsed_date = dt.strptime(exam_date, '%Y-%m-%d').date()
431
+
432
+ parsed_date = dt.strptime(exam_date, "%Y-%m-%d").date()
385
433
  selected_data["examination_date"] = parsed_date
386
- logger.debug("Parsed ISO examination_date '%s' to date: %s", exam_date, parsed_date)
434
+ logger.debug(
435
+ "Parsed ISO examination_date '%s' to date: %s",
436
+ exam_date,
437
+ parsed_date,
438
+ )
387
439
  except ValueError:
388
440
  # Fall back to dateparser for complex formats
389
441
  import dateparser
390
- parsed_date = dateparser.parse(exam_date, languages=['de'], settings={'DATE_ORDER': 'DMY'})
442
+
443
+ parsed_date = dateparser.parse(
444
+ exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
445
+ )
391
446
  if parsed_date:
392
447
  selected_data["examination_date"] = parsed_date.date()
393
- logger.debug("Parsed string examination_date '%s' to date: %s", exam_date, parsed_date.date())
448
+ logger.debug(
449
+ "Parsed string examination_date '%s' to date: %s",
450
+ exam_date,
451
+ parsed_date.date(),
452
+ )
394
453
  else:
395
- logger.warning("Could not parse examination_date string '%s', removing from data", exam_date)
454
+ logger.warning(
455
+ "Could not parse examination_date string '%s', removing from data",
456
+ exam_date,
457
+ )
396
458
  selected_data.pop("examination_date", None)
397
459
  else:
398
460
  # Use dateparser for non-ISO formats
399
461
  import dateparser
400
- parsed_date = dateparser.parse(exam_date, languages=['de'], settings={'DATE_ORDER': 'DMY'})
462
+
463
+ parsed_date = dateparser.parse(
464
+ exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
465
+ )
401
466
  if parsed_date:
402
467
  selected_data["examination_date"] = parsed_date.date()
403
- logger.debug("Parsed string examination_date '%s' to date: %s", exam_date, parsed_date.date())
468
+ logger.debug(
469
+ "Parsed string examination_date '%s' to date: %s",
470
+ exam_date,
471
+ parsed_date.date(),
472
+ )
404
473
  else:
405
- logger.warning("Could not parse examination_date string '%s', removing from data", exam_date)
474
+ logger.warning(
475
+ "Could not parse examination_date string '%s', removing from data",
476
+ exam_date,
477
+ )
406
478
  selected_data.pop("examination_date", None)
407
479
  except Exception as e:
408
- logger.warning("Error parsing examination_date string '%s': %s, removing from data", exam_date, e)
480
+ logger.warning(
481
+ "Error parsing examination_date string '%s': %s, removing from data",
482
+ exam_date,
483
+ e,
484
+ )
409
485
  selected_data.pop("examination_date", None)
410
486
 
411
487
  # Handle Center
@@ -421,7 +497,7 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
421
497
  # Handle Names and Gender
422
498
  first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN_NAME
423
499
  last_name = selected_data.get("patient_last_name") or DEFAULT_UNKNOWN_NAME
424
- selected_data["patient_first_name"] = first_name # Ensure defaults are set
500
+ selected_data["patient_first_name"] = first_name # Ensure defaults are set
425
501
  selected_data["patient_last_name"] = last_name
426
502
 
427
503
  patient_gender_input = selected_data.get("patient_gender")
@@ -432,22 +508,34 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
432
508
  elif isinstance(patient_gender_input, str):
433
509
  # Input is a string (gender name)
434
510
  try:
435
- selected_data["patient_gender"] = Gender.objects.get(name=patient_gender_input)
511
+ selected_data["patient_gender"] = Gender.objects.get(
512
+ name=patient_gender_input
513
+ )
436
514
  except Gender.DoesNotExist:
437
- logger.warning(f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default.")
515
+ logger.warning(
516
+ f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default."
517
+ )
438
518
  # Fall through to guessing logic if provided string name is invalid
439
- patient_gender_input = None # Reset to trigger guessing
440
-
441
- if not isinstance(selected_data.get("patient_gender"), Gender): # If not already a Gender object (e.g. was None, or string lookup failed)
519
+ patient_gender_input = None # Reset to trigger guessing
520
+
521
+ if not isinstance(
522
+ selected_data.get("patient_gender"), Gender
523
+ ): # If not already a Gender object (e.g. was None, or string lookup failed)
442
524
  gender_name_to_use = guess_name_gender(first_name)
443
525
  if not gender_name_to_use:
444
- logger.warning(f"Could not guess gender for name '{first_name}'. Setting Gender to unknown.")
526
+ logger.warning(
527
+ f"Could not guess gender for name '{first_name}'. Setting Gender to unknown."
528
+ )
445
529
  gender_name_to_use = "unknown"
446
530
  try:
447
- selected_data["patient_gender"] = Gender.objects.get(name=gender_name_to_use)
531
+ selected_data["patient_gender"] = Gender.objects.get(
532
+ name=gender_name_to_use
533
+ )
448
534
  except Gender.DoesNotExist:
449
535
  # This should ideally not happen if "unknown" gender is guaranteed to exist
450
- raise ValueError(f"Default or guessed gender '{gender_name_to_use}' does not exist in Gender table.")
536
+ raise ValueError(
537
+ f"Default or guessed gender '{gender_name_to_use}' does not exist in Gender table."
538
+ )
451
539
 
452
540
  # Update name DB
453
541
  update_name_db(first_name, last_name)
@@ -456,32 +544,42 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
456
544
  sensitive_meta = cls(**selected_data)
457
545
 
458
546
  # Call save once at the end. This triggers the custom save logic.
459
- sensitive_meta.save() # This will call perform_save_logic internally
547
+ sensitive_meta.save() # This will call perform_save_logic internally
460
548
 
461
549
  return sensitive_meta
462
550
 
463
551
 
464
- def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, Any]) -> "SensitiveMeta":
552
+ def update_sensitive_meta_from_dict(
553
+ instance: "SensitiveMeta", data: Dict[str, Any]
554
+ ) -> "SensitiveMeta":
465
555
  """Logic to update a SensitiveMeta instance from a dictionary."""
466
- field_names = {f.name for f in instance._meta.get_fields() if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)}
556
+ field_names = {
557
+ f.name
558
+ for f in instance._meta.get_fields()
559
+ if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
560
+ }
467
561
  # Exclude FKs that should not be updated directly from dict keys (handled separately or via save logic)
468
- excluded_fields = {'pseudo_patient', 'pseudo_examination'}
469
- selected_data = {k: v for k, v in data.items() if k in field_names and k not in excluded_fields}
562
+ excluded_fields = {"pseudo_patient", "pseudo_examination"}
563
+ selected_data = {
564
+ k: v for k, v in data.items() if k in field_names and k not in excluded_fields
565
+ }
470
566
 
471
567
  # Handle potential Center update
472
568
  center_name = data.get("center_name")
473
569
  if center_name:
474
570
  try:
475
571
  center = Center.objects.get_by_natural_key(center_name)
476
- instance.center = center # Update center directly
572
+ instance.center = center # Update center directly
477
573
  except Center.DoesNotExist as exc:
478
- logger.warning(f"Center '{center_name}' not found during update. Keeping existing center.")
479
- selected_data.pop('center', None) # Remove from dict if not found
574
+ logger.warning(
575
+ f"Center '{center_name}' not found during update. Keeping existing center."
576
+ )
577
+ selected_data.pop("center", None) # Remove from dict if not found
480
578
 
481
579
  # Set examiner names if provided, before calling save
482
580
  examiner_first_name = data.get("examiner_first_name")
483
581
  examiner_last_name = data.get("examiner_last_name")
484
- if examiner_first_name is not None: # Allow setting empty strings
582
+ if examiner_first_name is not None: # Allow setting empty strings
485
583
  instance.examiner_first_name = examiner_first_name
486
584
  if examiner_last_name is not None:
487
585
  instance.examiner_last_name = examiner_last_name
@@ -495,10 +593,14 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
495
593
  elif isinstance(patient_gender_input, str):
496
594
  gender_input_clean = patient_gender_input.strip()
497
595
  # Try direct case-insensitive DB lookup first
498
- gender_obj = Gender.objects.filter(name__iexact=gender_input_clean).first()
596
+ gender_obj = Gender.objects.filter(
597
+ name__iexact=gender_input_clean
598
+ ).first()
499
599
  if gender_obj:
500
600
  selected_data["patient_gender"] = gender_obj
501
- logger.debug(f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup")
601
+ logger.debug(
602
+ f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup"
603
+ )
502
604
  else:
503
605
  # Use mapping helper for fallback
504
606
  mapped = _map_gender_string_to_standard(gender_input_clean)
@@ -506,95 +608,172 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
506
608
  gender_obj = Gender.objects.filter(name__iexact=mapped).first()
507
609
  if gender_obj:
508
610
  selected_data["patient_gender"] = gender_obj
509
- logger.info(f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping")
611
+ logger.info(
612
+ f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
613
+ )
510
614
  else:
511
- logger.warning(f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'.")
512
- unknown_gender = Gender.objects.filter(name__iexact='unknown').first()
615
+ logger.warning(
616
+ f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'."
617
+ )
618
+ unknown_gender = Gender.objects.filter(
619
+ name__iexact="unknown"
620
+ ).first()
513
621
  if unknown_gender:
514
622
  selected_data["patient_gender"] = unknown_gender
515
- logger.warning(f"Using 'unknown' gender as fallback for '{patient_gender_input}'")
623
+ logger.warning(
624
+ f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
625
+ )
516
626
  else:
517
- logger.error(f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update.")
627
+ logger.error(
628
+ f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
629
+ )
518
630
  selected_data.pop("patient_gender", None)
519
631
  else:
520
632
  # Last resort: try to get 'unknown' gender
521
- unknown_gender = Gender.objects.filter(name__iexact='unknown').first()
633
+ unknown_gender = Gender.objects.filter(
634
+ name__iexact="unknown"
635
+ ).first()
522
636
  if unknown_gender:
523
637
  selected_data["patient_gender"] = unknown_gender
524
- logger.warning(f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)")
638
+ logger.warning(
639
+ f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
640
+ )
525
641
  else:
526
- logger.error(f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update.")
642
+ logger.error(
643
+ f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
644
+ )
527
645
  selected_data.pop("patient_gender", None)
528
646
  else:
529
- logger.warning(f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update.")
647
+ logger.warning(
648
+ f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update."
649
+ )
530
650
  selected_data.pop("patient_gender", None)
531
651
  except Exception as e:
532
- logger.exception(f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update.")
652
+ logger.exception(
653
+ f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update."
654
+ )
533
655
  selected_data.pop("patient_gender", None)
534
656
 
535
657
  # Update other attributes from selected_data
536
658
  patient_name_changed = False
537
659
  for k, v in selected_data.items():
538
660
  # Avoid overwriting examiner names if they were just explicitly set
539
- if k not in ["examiner_first_name", "examiner_last_name"] or \
540
- (k == "examiner_first_name" and examiner_first_name is None) or \
541
- (k == "examiner_last_name" and examiner_last_name is None):
542
-
661
+ if (
662
+ k not in ["examiner_first_name", "examiner_last_name"]
663
+ or (k == "examiner_first_name" and examiner_first_name is None)
664
+ or (k == "examiner_last_name" and examiner_last_name is None)
665
+ ):
543
666
  try:
544
667
  # --- Convert patient_dob if it's a date object ---
545
668
  value_to_set = v
546
669
  if k == "patient_dob":
547
670
  if isinstance(v, date) and not isinstance(v, datetime):
548
- aware_dob = timezone.make_aware(datetime.combine(v, datetime.min.time()))
671
+ aware_dob = timezone.make_aware(
672
+ datetime.combine(v, datetime.min.time())
673
+ )
549
674
  value_to_set = aware_dob
550
- logger.debug("Converted patient_dob from date to aware datetime during update: %s", aware_dob)
675
+ logger.debug(
676
+ "Converted patient_dob from date to aware datetime during update: %s",
677
+ aware_dob,
678
+ )
551
679
  elif isinstance(v, str):
552
680
  # Handle string DOB - check if it's a field name or actual date
553
- if v == "patient_dob" or v in ["patient_first_name", "patient_last_name", "examination_date"]:
554
- logger.warning("Skipping invalid patient_dob value '%s' during update - appears to be field name", v)
681
+ if v == "patient_dob" or v in [
682
+ "patient_first_name",
683
+ "patient_last_name",
684
+ "examination_date",
685
+ ]:
686
+ logger.warning(
687
+ "Skipping invalid patient_dob value '%s' during update - appears to be field name",
688
+ v,
689
+ )
555
690
  continue # Skip this field
556
691
  else:
557
692
  # Try to parse as date string
558
693
  try:
559
694
  import dateparser
560
- parsed_dob = dateparser.parse(v, languages=['de'], settings={'DATE_ORDER': 'DMY'})
695
+
696
+ parsed_dob = dateparser.parse(
697
+ v, languages=["de"], settings={"DATE_ORDER": "DMY"}
698
+ )
561
699
  if parsed_dob:
562
- value_to_set = timezone.make_aware(parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0))
563
- logger.debug("Parsed string patient_dob '%s' during update to aware datetime: %s", v, value_to_set)
700
+ value_to_set = timezone.make_aware(
701
+ parsed_dob.replace(
702
+ hour=0, minute=0, second=0, microsecond=0
703
+ )
704
+ )
705
+ logger.debug(
706
+ "Parsed string patient_dob '%s' during update to aware datetime: %s",
707
+ v,
708
+ value_to_set,
709
+ )
564
710
  else:
565
- logger.warning("Could not parse patient_dob string '%s' during update, skipping", v)
711
+ logger.warning(
712
+ "Could not parse patient_dob string '%s' during update, skipping",
713
+ v,
714
+ )
566
715
  continue
567
716
  except Exception as e:
568
- logger.warning("Error parsing patient_dob string '%s' during update: %s, skipping", v, e)
717
+ logger.warning(
718
+ "Error parsing patient_dob string '%s' during update: %s, skipping",
719
+ v,
720
+ e,
721
+ )
569
722
  continue
570
723
  elif k == "examination_date" and isinstance(v, str):
571
- if v == "examination_date" or v in ["patient_first_name", "patient_last_name", "patient_dob"]:
572
- logger.warning("Skipping invalid examination_date value '%s' during update - appears to be field name", v)
724
+ if v == "examination_date" or v in [
725
+ "patient_first_name",
726
+ "patient_last_name",
727
+ "patient_dob",
728
+ ]:
729
+ logger.warning(
730
+ "Skipping invalid examination_date value '%s' during update - appears to be field name",
731
+ v,
732
+ )
573
733
  continue
574
734
  else:
575
735
  # Try to parse as date string
576
736
  try:
577
737
  import dateparser
578
- parsed_date = dateparser.parse(v, languages=['de'], settings={'DATE_ORDER': 'DMY'})
738
+
739
+ parsed_date = dateparser.parse(
740
+ v, languages=["de"], settings={"DATE_ORDER": "DMY"}
741
+ )
579
742
  if parsed_date:
580
743
  value_to_set = parsed_date.date()
581
- logger.debug("Parsed string examination_date '%s' during update to date: %s", v, value_to_set)
744
+ logger.debug(
745
+ "Parsed string examination_date '%s' during update to date: %s",
746
+ v,
747
+ value_to_set,
748
+ )
582
749
  else:
583
- logger.warning("Could not parse examination_date string '%s' during update, skipping", v)
750
+ logger.warning(
751
+ "Could not parse examination_date string '%s' during update, skipping",
752
+ v,
753
+ )
584
754
  continue
585
755
  except Exception as e:
586
- logger.warning("Error parsing examination_date string '%s' during update: %s, skipping", v, e)
756
+ logger.warning(
757
+ "Error parsing examination_date string '%s' during update: %s, skipping",
758
+ v,
759
+ e,
760
+ )
587
761
  continue
588
762
  # --- End Conversion ---
589
763
 
590
764
  # Check if patient name is changing
591
- if k in ["patient_first_name", "patient_last_name"] and getattr(instance, k) != value_to_set:
765
+ if (
766
+ k in ["patient_first_name", "patient_last_name"]
767
+ and getattr(instance, k) != value_to_set
768
+ ):
592
769
  patient_name_changed = True
593
-
594
- setattr(instance, k, value_to_set) # Use value_to_set
595
-
770
+
771
+ setattr(instance, k, value_to_set) # Use value_to_set
772
+
596
773
  except Exception as e:
597
- logger.error(f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field.")
774
+ logger.error(
775
+ f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field."
776
+ )
598
777
  continue
599
778
 
600
779
  # Update name DB if patient names changed
@@ -617,7 +796,7 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
617
796
  def update_or_create_sensitive_meta_from_dict(
618
797
  cls: Type["SensitiveMeta"],
619
798
  data: Dict[str, Any],
620
- instance: Optional["SensitiveMeta"] = None
799
+ instance: Optional["SensitiveMeta"] = None,
621
800
  ) -> "SensitiveMeta":
622
801
  """Logic to update or create a SensitiveMeta instance from a dictionary."""
623
802
  # Check if the instance already exists based on unique fields
@@ -632,9 +811,9 @@ def update_or_create_sensitive_meta_from_dict(
632
811
  def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
633
812
  """Maps various gender string inputs to standard gender names used in the DB."""
634
813
  mapping = {
635
- 'male': ['male', 'm', 'männlich', 'man'],
636
- 'female': ['female', 'f', 'weiblich', 'woman'],
637
- 'unknown': ['unknown', 'unbekannt', 'other', 'diverse', '']
814
+ "male": ["male", "m", "männlich", "man"],
815
+ "female": ["female", "f", "weiblich", "woman"],
816
+ "unknown": ["unknown", "unbekannt", "other", "diverse", ""],
638
817
  }
639
818
  gender_lower = gender_str.strip().lower()
640
819
  for standard, variants in mapping.items():
@@ -18,6 +18,7 @@ from contextlib import contextmanager
18
18
  from pathlib import Path
19
19
  from typing import Union, Dict, Any, Optional, List, Tuple
20
20
  from django.db import transaction
21
+ from moviepy import video
21
22
  from endoreg_db.models import VideoFile, SensitiveMeta
22
23
  from endoreg_db.utils.paths import STORAGE_DIR, VIDEO_DIR, ANONYM_VIDEO_DIR
23
24
  import random
@@ -715,11 +716,15 @@ class VideoImportService():
715
716
  "Updated video.raw_file using fallback method: videos/sensitive/%s",
716
717
  target_file_path.name,
717
718
  )
719
+
720
+ self.processing_context["raw_video_path"] = target_file_path
721
+ self.processing_context["video_filename"] = target_file_path.name
722
+
718
723
 
719
724
  self.logger.info("Created sensitive file for %s at %s", video.uuid, target_file_path)
720
725
  return target_file_path
721
726
 
722
- def _get_processor_roi_info(self) -> Tuple[Optional[dict[str, dict[str, int | None] | None]], Optional[dict[str, int | None]] ]:
727
+ def _get_processor_roi_info(self) -> Tuple[Optional[List[List[Dict[str, Any]]]], Optional[Dict[str, Any]]]:
723
728
  """Get processor ROI information for masking."""
724
729
  endoscope_data_roi_nested = None
725
730
  endoscope_image_roi = None
@@ -742,6 +747,16 @@ class VideoImportService():
742
747
  except Exception as exc:
743
748
  self.logger.error("Failed to retrieve processor ROI information: %s", exc)
744
749
 
750
+ # Convert dict to nested list if necessary to match return type
751
+ if isinstance(endoscope_data_roi_nested, dict):
752
+ # Convert dict[str, dict[str, int | None] | None] to List[List[Dict[str, Any]]]
753
+ converted_roi = []
754
+ for key, value in endoscope_data_roi_nested.items():
755
+ if isinstance(value, dict):
756
+ converted_roi.append([value])
757
+ elif value is None:
758
+ converted_roi.append([])
759
+ endoscope_data_roi_nested = converted_roi
745
760
 
746
761
  return endoscope_data_roi_nested, endoscope_image_roi
747
762
 
@@ -818,42 +833,47 @@ class VideoImportService():
818
833
  def _perform_frame_cleaning(self, endoscope_data_roi_nested, endoscope_image_roi):
819
834
  """Perform frame cleaning and anonymization."""
820
835
  # Instantiate frame cleaner
821
- is_available, FrameCleaner = self._ensure_frame_cleaning_available()
836
+ is_available, frame_cleaner = self._ensure_frame_cleaning_available()
822
837
 
823
- if not is_available or FrameCleaner is None:
838
+ if not is_available:
824
839
  raise RuntimeError("Frame cleaning not available")
825
840
 
826
841
  # Prepare parameters for frame cleaning
827
842
  raw_video_path = self.processing_context.get('raw_video_path')
828
843
 
829
844
  if not raw_video_path or not Path(raw_video_path).exists():
830
- raise RuntimeError(f"Raw video path not found: {raw_video_path}")
845
+ try:
846
+ self.current_video = self._require_current_video()
847
+ raw_video_path = self.current_video.get_raw_file_path()
848
+ except Exception:
849
+ raise RuntimeError(f"Raw video path not found: {raw_video_path}")
831
850
 
851
+
832
852
  # Create temporary output path for cleaned video
833
853
  video_filename = self.processing_context.get('video_filename', Path(raw_video_path).name)
834
854
  cleaned_filename = f"cleaned_{video_filename}"
835
855
  cleaned_video_path = Path(raw_video_path).parent / cleaned_filename
836
-
837
- # Instantiate FrameCleaner object
838
- frame_cleaner_instance = FrameCleaner()
839
-
856
+
857
+
858
+
840
859
  # Clean video with ROI masking (heavy I/O operation)
841
- actual_cleaned_path, extracted_metadata = frame_cleaner_instance.clean_video(
860
+ actual_cleaned_path, extracted_metadata = frame_cleaner.clean_video(
842
861
  video_path=Path(raw_video_path),
843
862
  endoscope_image_roi=endoscope_image_roi,
844
863
  endoscope_data_roi_nested=endoscope_data_roi_nested,
845
864
  output_path=cleaned_video_path,
846
865
  technique="mask_overlay"
847
866
  )
848
-
867
+
868
+
849
869
  # Store cleaned video path for later use in _cleanup_and_archive
850
870
  self.processing_context['cleaned_video_path'] = actual_cleaned_path
851
871
  self.processing_context['extracted_metadata'] = extracted_metadata
852
-
872
+
853
873
  # Update sensitive metadata with extracted information
854
874
  self._update_sensitive_metadata(extracted_metadata)
855
875
  self.logger.info(f"Extracted metadata from frame cleaning: {extracted_metadata}")
856
-
876
+
857
877
  self.logger.info(f"Frame cleaning with ROI masking completed: {actual_cleaned_path}")
858
878
  self.logger.info("Cleaned video will be moved to anonym_videos during cleanup")
859
879
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: endoreg-db
3
- Version: 0.8.3.4
3
+ Version: 0.8.3.7
4
4
  Summary: EndoReg Db Django App
5
5
  Project-URL: Homepage, https://info.coloreg.de
6
6
  Project-URL: Repository, https://github.com/wg-lux/endoreg-db
@@ -248,7 +248,7 @@ endoreg_db/management/__init__.py,sha256=3dsK9Mizq1veuWTcvSOyWMFT9VI8wtyk-P2K9Ri
248
248
  endoreg_db/management/commands/__init__.py,sha256=Ch0jwQfNpOSr4O5KKMfYJ93dsesk1Afb-JtbRVyFXZs,21
249
249
  endoreg_db/management/commands/anonymize_video.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
250
250
  endoreg_db/management/commands/check_auth.py,sha256=TPiYeCZ5QcqIvR33xhbqXunO2nrcNAmHb_izoMTqgpg,5390
251
- endoreg_db/management/commands/create_model_meta_from_huggingface.py,sha256=RUuoBjTzdchuMY6qcwBENN7FTyTygPTZQBZYWwhugDc,3925
251
+ endoreg_db/management/commands/create_model_meta_from_huggingface.py,sha256=enZiNBi3wKLnOwdCp6nV3EDLrkK50KVEn6urgblNVjw,3621
252
252
  endoreg_db/management/commands/create_multilabel_model_meta.py,sha256=qeoyqcF2CWcnhniVRrlYbmJmwNwyZb-VQ0pjkr6arJU,7566
253
253
  endoreg_db/management/commands/fix_missing_patient_data.py,sha256=5TPUTOQwI2fVh3Zd88o4ne0R8N_V98k0GZsI1gW0kGM,7766
254
254
  endoreg_db/management/commands/fix_video_paths.py,sha256=7LLwc38oX3B_tYWbLJA43Li_KBO3m5Lyw0CF6YqN5rU,7145
@@ -464,10 +464,10 @@ endoreg_db/models/medical/risk/risk_type.py,sha256=kEugcaWSTEWH_Vxq4dcF80Iv1L4_K
464
464
  endoreg_db/models/metadata/__init__.py,sha256=8I6oLj3YTmeaPGJpL0AWG5gLwp38QzrEggxSkTisv7c,474
465
465
  endoreg_db/models/metadata/frame_ocr_result.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
466
466
  endoreg_db/models/metadata/model_meta.py,sha256=F_r-PTLeNi4J-4EaGCQkGIguhdl7Bwba7_i56ZAjc-4,7589
467
- endoreg_db/models/metadata/model_meta_logic.py,sha256=6w1YX8hVq40UXbVN1fvDO9OljwekBZaDVHEjVZecoV8,12252
467
+ endoreg_db/models/metadata/model_meta_logic.py,sha256=27mqScxUTJXNUVc6CqAs5dXjspEsh0TWPmlxdJVulGc,12015
468
468
  endoreg_db/models/metadata/pdf_meta.py,sha256=BTmpSgqxmPKi0apcNjyrZAS4AFKCPXVdBd6VBeyyv6E,3174
469
469
  endoreg_db/models/metadata/sensitive_meta.py,sha256=ekLHrW-b5uYcjfkRd0EW5ncx5ef8Bu-K6msDkpWCAbk,13034
470
- endoreg_db/models/metadata/sensitive_meta_logic.py,sha256=Oh7ssZQEPfKGfRMF5nXKJpOIxXx-Xibd3rpOu-bQilk,29988
470
+ endoreg_db/models/metadata/sensitive_meta_logic.py,sha256=by3eCW8CgglK1SHiDOepHhTOGaugswxJhkH0BZp7-gs,33909
471
471
  endoreg_db/models/metadata/video_meta.py,sha256=c6xWdLW3uNqJ5VPJXHCxXA3mbXw-b0uR54-TOS3qL2Q,14966
472
472
  endoreg_db/models/metadata/video_prediction_logic.py,sha256=j5N82mHtiomeeIaf1HA65kT5d0htQfJmbI2bJb8mpxQ,7677
473
473
  endoreg_db/models/metadata/video_prediction_meta.py,sha256=EyfctAAAVcW9L0gf76ZBc9-G8MLMcD-tc2kkjaaLH4w,10592
@@ -602,7 +602,7 @@ endoreg_db/services/pseudonym_service.py,sha256=CJhbtRa6K6SPbphgCZgEMi8AFQtB18CU
602
602
  endoreg_db/services/requirements_object.py,sha256=290zf8AEbVtCoHhW4Jr7_ud-RvrqYmb1Nz9UBHtTnc0,6164
603
603
  endoreg_db/services/segment_sync.py,sha256=YgHvIHkbW4mqCu0ACf3zjRSZnNfxWwt4gh5syUVXuE0,6400
604
604
  endoreg_db/services/storage_aware_video_processor.py,sha256=kKFK64vXLeBSVkp1YJonU3gFDTeXZ8C4qb9QZZB99SE,13420
605
- endoreg_db/services/video_import.py,sha256=RBk0n_oN3VXRBMHtUKsRVjVXYv9VnxlM6GsMx2yHL6s,45187
605
+ endoreg_db/services/video_import.py,sha256=PNAHHZHzge2TYDaZP63CL-sslj01CxFky6sEi6Twavg,46045
606
606
  endoreg_db/tasks/upload_tasks.py,sha256=OJq7DhNwcbWdXzHY8jz5c51BCVkPN5gSWOz-6Fx6W5M,7799
607
607
  endoreg_db/tasks/video_ingest.py,sha256=kxFuYkHijINV0VabQKCFVpJRv6eCAw07tviONurDgg8,5265
608
608
  endoreg_db/tasks/video_processing_tasks.py,sha256=rZ7Kr49bAR4Q-vALO2SURebrhcJ5hSFGwjF4aULrOao,14089
@@ -786,7 +786,7 @@ endoreg_db/views/video/video_meta.py,sha256=C1wBMTtQb_yzEUrhFGAy2UHEWMk_CbU75WXX
786
786
  endoreg_db/views/video/video_processing_history.py,sha256=mhFuS8RG5GV8E-lTtuD0qrq-bIpnUFp8vy9aERfC-J8,770
787
787
  endoreg_db/views/video/video_remove_frames.py,sha256=2FmvNrSPM0fUXiBxINN6vBUUDCqDlBkNcGR3WsLDgKo,1696
788
788
  endoreg_db/views/video/video_stream.py,sha256=kLyuf0ORTmsLeYUQkTQ6iRYqlIQozWhMMR3Lhfe_trk,12148
789
- endoreg_db-0.8.3.4.dist-info/METADATA,sha256=a9terVG2Fh1NBsonpVMM9l5O7PS8FSlAPOMdcg1vAOY,14758
790
- endoreg_db-0.8.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
791
- endoreg_db-0.8.3.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
792
- endoreg_db-0.8.3.4.dist-info/RECORD,,
789
+ endoreg_db-0.8.3.7.dist-info/METADATA,sha256=AnfCmoQPqWaQn-LvrsJB0xImN9vp12xyqd-jQLWCpzc,14758
790
+ endoreg_db-0.8.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
791
+ endoreg_db-0.8.3.7.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
792
+ endoreg_db-0.8.3.7.dist-info/RECORD,,