endoreg-db 0.8.3.3__py3-none-any.whl → 0.8.6.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of endoreg-db might be problematic. Click here for more details.

Files changed (41) hide show
  1. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +23 -1
  2. endoreg_db/data/setup_config.yaml +38 -0
  3. endoreg_db/management/commands/create_model_meta_from_huggingface.py +1 -2
  4. endoreg_db/management/commands/load_ai_model_data.py +18 -15
  5. endoreg_db/management/commands/setup_endoreg_db.py +218 -33
  6. endoreg_db/models/media/pdf/raw_pdf.py +241 -97
  7. endoreg_db/models/media/video/pipe_1.py +30 -33
  8. endoreg_db/models/media/video/video_file.py +300 -187
  9. endoreg_db/models/medical/hardware/endoscopy_processor.py +10 -1
  10. endoreg_db/models/metadata/model_meta_logic.py +34 -45
  11. endoreg_db/models/metadata/sensitive_meta_logic.py +555 -150
  12. endoreg_db/serializers/__init__.py +26 -55
  13. endoreg_db/serializers/misc/__init__.py +1 -1
  14. endoreg_db/serializers/misc/file_overview.py +65 -35
  15. endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
  16. endoreg_db/serializers/video_examination.py +198 -0
  17. endoreg_db/services/lookup_service.py +228 -58
  18. endoreg_db/services/lookup_store.py +174 -30
  19. endoreg_db/services/pdf_import.py +585 -282
  20. endoreg_db/services/video_import.py +493 -240
  21. endoreg_db/urls/__init__.py +36 -23
  22. endoreg_db/urls/label_video_segments.py +2 -0
  23. endoreg_db/urls/media.py +103 -66
  24. endoreg_db/utils/setup_config.py +177 -0
  25. endoreg_db/views/__init__.py +5 -3
  26. endoreg_db/views/media/pdf_media.py +3 -1
  27. endoreg_db/views/media/video_media.py +1 -1
  28. endoreg_db/views/media/video_segments.py +187 -259
  29. endoreg_db/views/pdf/__init__.py +5 -8
  30. endoreg_db/views/pdf/pdf_stream.py +186 -0
  31. endoreg_db/views/pdf/reimport.py +110 -94
  32. endoreg_db/views/requirement/lookup.py +171 -287
  33. endoreg_db/views/video/__init__.py +0 -2
  34. endoreg_db/views/video/video_examination_viewset.py +202 -289
  35. {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/METADATA +1 -2
  36. {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/RECORD +38 -37
  37. endoreg_db/views/pdf/pdf_media.py +0 -239
  38. endoreg_db/views/pdf/pdf_stream_views.py +0 -127
  39. endoreg_db/views/video/video_media.py +0 -158
  40. {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/WHEEL +0 -0
  41. {endoreg_db-0.8.3.3.dist-info → endoreg_db-0.8.6.5.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -165,17 +162,22 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
165
162
  if not center:
166
163
  raise ValueError("Center is required to calculate patient hash.")
167
164
 
165
+ assert first_name is not None, "First name is required to calculate patient hash."
166
+ assert last_name is not None, "Last name is required to calculate patient hash."
167
+
168
168
  hash_str = get_patient_hash(
169
169
  first_name=first_name,
170
170
  last_name=last_name,
171
171
  dob=dob,
172
- center=center.name, # Use center name
172
+ center=center.name, # Use center name
173
173
  salt=salt,
174
174
  )
175
175
  return sha256(hash_str.encode()).hexdigest()
176
176
 
177
177
 
178
- def calculate_examination_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -> str:
178
+ def calculate_examination_hash(
179
+ instance: "SensitiveMeta", salt: str = SECRET_SALT
180
+ ) -> str:
179
181
  """Calculates the examination hash for the instance."""
180
182
  dob = instance.patient_dob
181
183
  first_name = instance.patient_first_name
@@ -190,13 +192,12 @@ def calculate_examination_hash(instance: "SensitiveMeta", salt: str = SECRET_SAL
190
192
  if not center:
191
193
  raise ValueError("Center is required to calculate examination hash.")
192
194
 
193
-
194
195
  hash_str = get_patient_examination_hash(
195
196
  first_name=first_name,
196
197
  last_name=last_name,
197
198
  dob=dob,
198
199
  examination_date=examination_date,
199
- center=center.name, # Use center name
200
+ center=center.name, # Use center name
200
201
  salt=salt,
201
202
  )
202
203
  return sha256(hash_str.encode()).hexdigest()
@@ -206,15 +207,19 @@ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
206
207
  """Creates or retrieves the pseudo examiner based on instance data."""
207
208
  first_name = instance.examiner_first_name
208
209
  last_name = instance.examiner_last_name
209
- center = instance.center # Should be set before calling save
210
+ center = instance.center # Should be set before calling save
210
211
 
211
212
  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.")
213
+ logger.warning(
214
+ f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner."
215
+ )
213
216
  # Ensure default center exists or handle appropriately
214
217
  try:
215
- default_center = Center.objects.get_by_natural_key("endoreg_db_demo")
218
+ default_center = Center.objects.get(name="endoreg_db_demo")
216
219
  except Center.DoesNotExist:
217
- logger.error("Default center 'endoreg_db_demo' not found. Cannot create default examiner.")
220
+ logger.error(
221
+ "Default center 'endoreg_db_demo' not found. Cannot create default examiner."
222
+ )
218
223
  raise ValueError("Default center 'endoreg_db_demo' not found.")
219
224
 
220
225
  examiner, _created = Examiner.custom_get_or_create(
@@ -232,7 +237,7 @@ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
232
237
  """Gets or creates the pseudo patient based on instance data."""
233
238
  # Ensure necessary fields are set
234
239
  if not instance.patient_hash:
235
- instance.patient_hash = calculate_patient_hash(instance)
240
+ instance.patient_hash = calculate_patient_hash(instance)
236
241
  if not instance.center:
237
242
  raise ValueError("Center must be set before creating pseudo patient.")
238
243
  if not instance.patient_gender:
@@ -254,61 +259,168 @@ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
254
259
  return patient
255
260
 
256
261
 
257
- def get_or_create_pseudo_patient_examination_logic(instance: "SensitiveMeta") -> "PatientExamination":
262
+ def get_or_create_pseudo_patient_examination_logic(
263
+ instance: "SensitiveMeta",
264
+ ) -> "PatientExamination":
258
265
  """Gets or creates the pseudo patient examination based on instance data."""
259
266
  # Ensure necessary fields are set
260
267
  if not instance.patient_hash:
261
- instance.patient_hash = calculate_patient_hash(instance)
268
+ instance.patient_hash = calculate_patient_hash(instance)
262
269
  if not instance.examination_hash:
263
- instance.examination_hash = calculate_examination_hash(instance)
270
+ instance.examination_hash = calculate_examination_hash(instance)
264
271
 
265
272
  # Ensure the pseudo patient exists first, as PatientExamination might depend on it
266
273
  if not instance.pseudo_patient_id:
267
274
  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
275
+ instance.pseudo_patient_id = pseudo_patient.pk # Assign FK directly
276
+
277
+ patient_examination, _created = (
278
+ PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
279
+ patient_hash=instance.patient_hash,
280
+ examination_hash=instance.examination_hash,
281
+ # Optionally pass pseudo_patient if the method requires it
282
+ # pseudo_patient=instance.pseudo_patient
283
+ )
275
284
  )
276
285
  return patient_examination
277
286
 
278
287
 
279
- @transaction.atomic # Ensure all operations within save succeed or fail together
288
+ @transaction.atomic # Ensure all operations within save succeed or fail together
280
289
  def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
281
290
  """
282
291
  Contains the core logic for preparing a SensitiveMeta instance for saving.
283
292
  Handles data generation (dates), hash calculation, and linking pseudo-entities.
284
- Returns the Examiner instance to be linked via M2M after the main save.
293
+
294
+ This function is called on every save() operation and implements a two-phase approach:
295
+
296
+ **Phase 1: Initial Creation (with defaults)**
297
+ - When a SensitiveMeta is first created (e.g., via get_or_create_sensitive_meta()),
298
+ it may have missing patient data (names, DOB, etc.)
299
+ - Default values are set to prevent hash calculation errors:
300
+ * patient_first_name: "unknown"
301
+ * patient_last_name: "unknown"
302
+ * patient_dob: random date (1920-2000)
303
+ - A temporary hash is calculated using these defaults
304
+ - Temporary pseudo-entities (Patient, Examination) are created
305
+
306
+ **Phase 2: Update (with extracted data)**
307
+ - When real patient data is extracted (e.g., from video OCR via lx_anonymizer),
308
+ update_from_dict() is called with actual values
309
+ - The instance fields are updated with real data (names, DOB, etc.)
310
+ - save() is called again, triggering this function
311
+ - Default-setting logic is skipped (fields are no longer empty)
312
+ - Hash is RECALCULATED with real data
313
+ - New pseudo-entities are created/retrieved based on new hash
314
+
315
+ **Example Flow:**
316
+ ```
317
+ # Initial creation
318
+ sm = SensitiveMeta.create_from_dict({"center": center})
319
+ # → patient_first_name = "unknown", patient_last_name = "unknown"
320
+ # → hash = sha256("unknown unknown 1990-01-01 ...")
321
+ # → pseudo_patient_temp created
322
+
323
+ # Later update with extracted data
324
+ sm.update_from_dict({"patient_first_name": "Max", "patient_last_name": "Mustermann"})
325
+ # → patient_first_name = "Max", patient_last_name = "Mustermann" (overwrites)
326
+ # → save() triggered → perform_save_logic() called again
327
+ # → Default-setting skipped (names already exist)
328
+ # → hash = sha256("Max Mustermann 1985-03-15 ...") (RECALCULATED)
329
+ # → pseudo_patient_real created/retrieved with new hash
330
+ ```
331
+
332
+ Args:
333
+ instance: The SensitiveMeta instance being saved
334
+
335
+ Returns:
336
+ Examiner: The pseudo examiner instance to be linked via M2M after save
337
+
338
+ Raises:
339
+ ValueError: If required fields (center, gender) cannot be determined
285
340
  """
286
341
 
287
342
  # --- Pre-Save Checks and Data Generation ---
288
343
 
289
344
  # 1. Ensure DOB and Examination Date exist
290
345
  if not instance.patient_dob:
291
- logger.debug(f"SensitiveMeta (pk={instance.pk}): Patient DOB missing, generating random.")
346
+ logger.debug(
347
+ f"SensitiveMeta (pk={instance.pk or 'new'}): Patient DOB missing, generating random."
348
+ )
292
349
  instance.patient_dob = generate_random_dob()
293
350
  if not instance.examination_date:
294
- logger.debug(f"SensitiveMeta (pk={instance.pk}): Examination date missing, generating random.")
351
+ logger.debug(
352
+ f"SensitiveMeta (pk={instance.pk or 'new'}): Examination date missing, generating random."
353
+ )
295
354
  instance.examination_date = generate_random_examination_date()
296
355
 
297
356
  # 2. Ensure Center exists (should be set before calling save)
298
357
  if not instance.center:
299
358
  raise ValueError("Center must be set before saving SensitiveMeta.")
300
359
 
360
+ # 2.5 CRITICAL: Set default patient names BEFORE hash calculation
361
+ #
362
+ # **Why this is necessary:**
363
+ # Hash calculation (step 4) requires first_name and last_name to be non-None.
364
+ # However, on initial creation (e.g., via get_or_create_sensitive_meta()), these
365
+ # fields may be empty because real patient data hasn't been extracted yet.
366
+ #
367
+ # **Two-phase approach:**
368
+ # - Phase 1 (Initial): Set defaults if names are missing
369
+ # → Allows hash calculation to succeed without errors
370
+ # → Creates temporary pseudo-entities with default hash
371
+ #
372
+ # - Phase 2 (Update): Real data extraction (OCR, manual input)
373
+ # → update_from_dict() sets real names ("Max", "Mustermann")
374
+ # → save() is called again
375
+ # → This block is SKIPPED (names already exist)
376
+ # → Hash is recalculated with real data (step 4)
377
+ # → New pseudo-entities created with correct hash
378
+ #
379
+ # **Example:**
380
+ # Initial: patient_first_name = "unknown" → hash = sha256("unknown unknown...")
381
+ # Updated: patient_first_name = "Max" → hash = sha256("Max Mustermann...")
382
+ #
383
+ if not instance.patient_first_name:
384
+ instance.patient_first_name = DEFAULT_UNKNOWN_NAME
385
+ logger.debug(
386
+ "SensitiveMeta (pk=%s): Patient first name missing, set to default '%s'.",
387
+ instance.pk or "new",
388
+ DEFAULT_UNKNOWN_NAME,
389
+ )
390
+
391
+ if not instance.patient_last_name:
392
+ instance.patient_last_name = DEFAULT_UNKNOWN_NAME
393
+ logger.debug(
394
+ "SensitiveMeta (pk=%s): Patient last name missing, set to default '%s'.",
395
+ instance.pk or "new",
396
+ DEFAULT_UNKNOWN_NAME,
397
+ )
398
+
301
399
  # 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
302
400
  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
-
401
+ # Use the now-guaranteed first_name for gender guessing
402
+ first_name = instance.patient_first_name
403
+ gender_str = guess_name_gender(first_name)
404
+ if not gender_str:
405
+ raise ValueError(
406
+ "Patient gender could not be determined and must be set before saving."
407
+ )
408
+ # Convert string to Gender object
409
+ try:
410
+ gender_obj = Gender.objects.get(name=gender_str)
411
+ instance.patient_gender = gender_obj
412
+ except Gender.DoesNotExist:
413
+ raise ValueError(f"Gender '{gender_str}' not found in database.")
310
414
 
311
415
  # 4. Calculate Hashes (depends on DOB, Exam Date, Center, Names)
416
+ #
417
+ # **IMPORTANT: Hashes are RECALCULATED on every save!**
418
+ # This enables the two-phase update pattern:
419
+ # - Initial save: Hash based on default "unknown unknown" names
420
+ # - Updated save: Hash based on real extracted names ("Max Mustermann")
421
+ #
422
+ # The new hash will link to different pseudo-entities, ensuring proper
423
+ # anonymization while maintaining referential integrity.
312
424
  instance.patient_hash = calculate_patient_hash(instance)
313
425
  instance.examination_hash = calculate_examination_hash(instance)
314
426
 
@@ -333,10 +445,59 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
333
445
  return examiner_instance
334
446
 
335
447
 
336
- def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str, Any]) -> "SensitiveMeta":
337
- """Logic to create a SensitiveMeta instance from a dictionary."""
448
+ def create_sensitive_meta_from_dict(
449
+ cls: Type["SensitiveMeta"], data: Dict[str, Any]
450
+ ) -> "SensitiveMeta":
451
+ """
452
+ Create a SensitiveMeta instance from a dictionary.
453
+
454
+ **Center handling:**
455
+ This function accepts TWO ways to specify the center:
456
+ 1. `center` (Center object) - Directly pass a Center instance
457
+ 2. `center_name` (string) - Pass the center name as a string (will be resolved to Center object)
458
+
459
+ At least ONE of these must be provided.
460
+
461
+ **Example usage:**
462
+ ```python
463
+ # Option 1: With Center object
464
+ data = {
465
+ "patient_first_name": "Patient",
466
+ "patient_last_name": "Unknown",
467
+ "patient_dob": date(1990, 1, 1),
468
+ "examination_date": date.today(),
469
+ "center": center_obj, # ← Center object
470
+ }
471
+ sm = SensitiveMeta.create_from_dict(data)
472
+
473
+ # Option 2: With center name string
474
+ data = {
475
+ "patient_first_name": "Patient",
476
+ "patient_last_name": "Unknown",
477
+ "patient_dob": date(1990, 1, 1),
478
+ "examination_date": date.today(),
479
+ "center_name": "university_hospital_wuerzburg", # ← String
480
+ }
481
+ sm = SensitiveMeta.create_from_dict(data)
482
+ ```
483
+
484
+ Args:
485
+ cls: The SensitiveMeta class
486
+ data: Dictionary containing field values
487
+
488
+ Returns:
489
+ SensitiveMeta: The created instance
338
490
 
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)}
491
+ Raises:
492
+ ValueError: If neither center nor center_name is provided
493
+ ValueError: If center_name does not match any Center in database
494
+ """
495
+
496
+ field_names = {
497
+ f.name
498
+ for f in cls._meta.get_fields()
499
+ if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
500
+ }
340
501
  selected_data = {k: v for k, v in data.items() if k in field_names}
341
502
 
342
503
  # --- Convert patient_dob if it's a date object ---
@@ -348,80 +509,153 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
348
509
  logger.debug("Converted patient_dob from date to aware datetime: %s", aware_dob)
349
510
  elif isinstance(dob, str):
350
511
  # 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)
512
+ if dob == "patient_dob" or dob in [
513
+ "patient_first_name",
514
+ "patient_last_name",
515
+ "examination_date",
516
+ ]:
517
+ logger.warning(
518
+ "Skipping invalid patient_dob value '%s' - appears to be field name",
519
+ dob,
520
+ )
353
521
  selected_data.pop("patient_dob", None) # Remove invalid value
354
522
  else:
355
523
  # Try to parse as date string
356
524
  try:
357
525
  import dateparser
358
- parsed_dob = dateparser.parse(dob, languages=['de'], settings={'DATE_ORDER': 'DMY'})
526
+
527
+ parsed_dob = dateparser.parse(
528
+ dob, languages=["de"], settings={"DATE_ORDER": "DMY"}
529
+ )
359
530
  if parsed_dob:
360
- aware_dob = timezone.make_aware(parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0))
531
+ aware_dob = timezone.make_aware(
532
+ parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0)
533
+ )
361
534
  selected_data["patient_dob"] = aware_dob
362
- logger.debug("Parsed string patient_dob '%s' to aware datetime: %s", dob, aware_dob)
535
+ logger.debug(
536
+ "Parsed string patient_dob '%s' to aware datetime: %s",
537
+ dob,
538
+ aware_dob,
539
+ )
363
540
  else:
364
- logger.warning("Could not parse patient_dob string '%s', removing from data", dob)
541
+ logger.warning(
542
+ "Could not parse patient_dob string '%s', removing from data",
543
+ dob,
544
+ )
365
545
  selected_data.pop("patient_dob", None)
366
546
  except Exception as e:
367
- logger.warning("Error parsing patient_dob string '%s': %s, removing from data", dob, e)
547
+ logger.warning(
548
+ "Error parsing patient_dob string '%s': %s, removing from data",
549
+ dob,
550
+ e,
551
+ )
368
552
  selected_data.pop("patient_dob", None)
369
553
  # --- End Conversion ---
370
-
554
+
371
555
  # Similar validation for examination_date
372
556
  exam_date = selected_data.get("examination_date")
373
557
  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)
558
+ if exam_date == "examination_date" or exam_date in [
559
+ "patient_first_name",
560
+ "patient_last_name",
561
+ "patient_dob",
562
+ ]:
563
+ logger.warning(
564
+ "Skipping invalid examination_date value '%s' - appears to be field name",
565
+ exam_date,
566
+ )
376
567
  selected_data.pop("examination_date", None)
377
568
  else:
378
569
  # Try to parse as date string
379
570
  try:
380
571
  # First try simple ISO format for YYYY-MM-DD
381
- if len(exam_date) == 10 and exam_date.count('-') == 2:
572
+ if len(exam_date) == 10 and exam_date.count("-") == 2:
382
573
  try:
383
574
  from datetime import datetime as dt
384
- parsed_date = dt.strptime(exam_date, '%Y-%m-%d').date()
575
+
576
+ parsed_date = dt.strptime(exam_date, "%Y-%m-%d").date()
385
577
  selected_data["examination_date"] = parsed_date
386
- logger.debug("Parsed ISO examination_date '%s' to date: %s", exam_date, parsed_date)
578
+ logger.debug(
579
+ "Parsed ISO examination_date '%s' to date: %s",
580
+ exam_date,
581
+ parsed_date,
582
+ )
387
583
  except ValueError:
388
584
  # Fall back to dateparser for complex formats
389
585
  import dateparser
390
- parsed_date = dateparser.parse(exam_date, languages=['de'], settings={'DATE_ORDER': 'DMY'})
586
+
587
+ parsed_date = dateparser.parse(
588
+ exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
589
+ )
391
590
  if parsed_date:
392
591
  selected_data["examination_date"] = parsed_date.date()
393
- logger.debug("Parsed string examination_date '%s' to date: %s", exam_date, parsed_date.date())
592
+ logger.debug(
593
+ "Parsed string examination_date '%s' to date: %s",
594
+ exam_date,
595
+ parsed_date.date(),
596
+ )
394
597
  else:
395
- logger.warning("Could not parse examination_date string '%s', removing from data", exam_date)
598
+ logger.warning(
599
+ "Could not parse examination_date string '%s', removing from data",
600
+ exam_date,
601
+ )
396
602
  selected_data.pop("examination_date", None)
397
603
  else:
398
604
  # Use dateparser for non-ISO formats
399
605
  import dateparser
400
- parsed_date = dateparser.parse(exam_date, languages=['de'], settings={'DATE_ORDER': 'DMY'})
606
+
607
+ parsed_date = dateparser.parse(
608
+ exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
609
+ )
401
610
  if parsed_date:
402
611
  selected_data["examination_date"] = parsed_date.date()
403
- logger.debug("Parsed string examination_date '%s' to date: %s", exam_date, parsed_date.date())
612
+ logger.debug(
613
+ "Parsed string examination_date '%s' to date: %s",
614
+ exam_date,
615
+ parsed_date.date(),
616
+ )
404
617
  else:
405
- logger.warning("Could not parse examination_date string '%s', removing from data", exam_date)
618
+ logger.warning(
619
+ "Could not parse examination_date string '%s', removing from data",
620
+ exam_date,
621
+ )
406
622
  selected_data.pop("examination_date", None)
407
623
  except Exception as e:
408
- logger.warning("Error parsing examination_date string '%s': %s, removing from data", exam_date, e)
624
+ logger.warning(
625
+ "Error parsing examination_date string '%s': %s, removing from data",
626
+ exam_date,
627
+ e,
628
+ )
409
629
  selected_data.pop("examination_date", None)
410
630
 
411
- # Handle Center
631
+ # Handle Center - accept both center_name (string) and center (object)
632
+ from ..administration import Center
633
+
634
+ center = data.get("center") # First try direct Center object
412
635
  center_name = data.get("center_name")
413
- if not center_name:
414
- raise ValueError("center_name is required in data dictionary.")
415
- try:
416
- center = Center.objects.get_by_natural_key(center_name)
636
+
637
+ if center is not None:
638
+ # Center object provided directly - validate it's a Center instance
639
+ if not isinstance(center, Center):
640
+ raise ValueError(f"'center' must be a Center instance, got {type(center)}")
417
641
  selected_data["center"] = center
418
- except Center.DoesNotExist as exc:
419
- raise ValueError(f"Center with name '{center_name}' does not exist.") from exc
642
+ elif center_name:
643
+ # center_name string provided - resolve to Center object
644
+ try:
645
+ center = Center.objects.get(name=center_name)
646
+ selected_data["center"] = center
647
+ except Center.DoesNotExist:
648
+ raise ValueError(f"Center with name '{center_name}' does not exist.")
649
+ else:
650
+ # Neither center nor center_name provided
651
+ raise ValueError(
652
+ "Either 'center' (Center object) or 'center_name' (string) is required in data dictionary."
653
+ )
420
654
 
421
655
  # Handle Names and Gender
422
656
  first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN_NAME
423
657
  last_name = selected_data.get("patient_last_name") or DEFAULT_UNKNOWN_NAME
424
- selected_data["patient_first_name"] = first_name # Ensure defaults are set
658
+ selected_data["patient_first_name"] = first_name # Ensure defaults are set
425
659
  selected_data["patient_last_name"] = last_name
426
660
 
427
661
  patient_gender_input = selected_data.get("patient_gender")
@@ -432,22 +666,34 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
432
666
  elif isinstance(patient_gender_input, str):
433
667
  # Input is a string (gender name)
434
668
  try:
435
- selected_data["patient_gender"] = Gender.objects.get(name=patient_gender_input)
669
+ selected_data["patient_gender"] = Gender.objects.get(
670
+ name=patient_gender_input
671
+ )
436
672
  except Gender.DoesNotExist:
437
- logger.warning(f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default.")
673
+ logger.warning(
674
+ f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default."
675
+ )
438
676
  # 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)
677
+ patient_gender_input = None # Reset to trigger guessing
678
+
679
+ if not isinstance(
680
+ selected_data.get("patient_gender"), Gender
681
+ ): # If not already a Gender object (e.g. was None, or string lookup failed)
442
682
  gender_name_to_use = guess_name_gender(first_name)
443
683
  if not gender_name_to_use:
444
- logger.warning(f"Could not guess gender for name '{first_name}'. Setting Gender to unknown.")
684
+ logger.warning(
685
+ f"Could not guess gender for name '{first_name}'. Setting Gender to unknown."
686
+ )
445
687
  gender_name_to_use = "unknown"
446
688
  try:
447
- selected_data["patient_gender"] = Gender.objects.get(name=gender_name_to_use)
689
+ selected_data["patient_gender"] = Gender.objects.get(
690
+ name=gender_name_to_use
691
+ )
448
692
  except Gender.DoesNotExist:
449
693
  # 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.")
694
+ raise ValueError(
695
+ f"Default or guessed gender '{gender_name_to_use}' does not exist in Gender table."
696
+ )
451
697
 
452
698
  # Update name DB
453
699
  update_name_db(first_name, last_name)
@@ -456,32 +702,105 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
456
702
  sensitive_meta = cls(**selected_data)
457
703
 
458
704
  # Call save once at the end. This triggers the custom save logic.
459
- sensitive_meta.save() # This will call perform_save_logic internally
705
+ sensitive_meta.save() # This will call perform_save_logic internally
460
706
 
461
707
  return sensitive_meta
462
708
 
463
709
 
464
- def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, Any]) -> "SensitiveMeta":
465
- """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)}
710
+ def update_sensitive_meta_from_dict(
711
+ instance: "SensitiveMeta", data: Dict[str, Any]
712
+ ) -> "SensitiveMeta":
713
+ """
714
+ Updates a SensitiveMeta instance from a dictionary of new values.
715
+
716
+ **Integration with two-phase save pattern:**
717
+ This function is typically called after initial SensitiveMeta creation when real
718
+ patient data becomes available (e.g., extracted from video OCR, PDF parsing, or
719
+ manual annotation).
720
+
721
+ **Example workflow:**
722
+ ```python
723
+ # Phase 1: Initial creation with defaults
724
+ sm = SensitiveMeta.create_from_dict({"center": center})
725
+ # → patient_first_name = "unknown", hash = sha256("unknown...")
726
+
727
+ # Phase 2: Update with extracted data
728
+ extracted = {
729
+ "patient_first_name": "Max",
730
+ "patient_last_name": "Mustermann",
731
+ "patient_dob": date(1985, 3, 15)
732
+ }
733
+ update_sensitive_meta_from_dict(sm, extracted)
734
+ # → Sets: sm.patient_first_name = "Max", sm.patient_last_name = "Mustermann"
735
+ # → Calls: sm.save()
736
+ # → Triggers: perform_save_logic() again
737
+ # → Result: Hash recalculated with real data, new pseudo-entities created
738
+ ```
739
+
740
+ **Key behaviors:**
741
+ - Updates instance attributes from provided dictionary
742
+ - Handles type conversions (date strings → date objects, gender strings → Gender objects)
743
+ - Tracks patient name changes to update name database
744
+ - Calls save() at the end, triggering full save logic including hash recalculation
745
+ - Default-setting in perform_save_logic() is skipped (fields already populated)
746
+
747
+ Args:
748
+ instance: The existing SensitiveMeta instance to update
749
+ data: Dictionary of field names and new values
750
+
751
+ Returns:
752
+ The updated SensitiveMeta instance
753
+
754
+ Raises:
755
+ Exception: If save fails or required conversions fail
756
+ """
757
+ field_names = {
758
+ f.name
759
+ for f in instance._meta.get_fields()
760
+ if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
761
+ }
467
762
  # 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}
763
+ excluded_fields = {"pseudo_patient", "pseudo_examination"}
764
+ selected_data = {
765
+ k: v for k, v in data.items() if k in field_names and k not in excluded_fields
766
+ }
470
767
 
471
- # Handle potential Center update
768
+ # Handle potential Center update - accept both center_name (string) and center (object)
769
+ from ..administration import Center
770
+
771
+ center = data.get("center") # First try direct Center object
472
772
  center_name = data.get("center_name")
473
- if center_name:
773
+
774
+ if center is not None:
775
+ # Center object provided directly - validate and update
776
+ if isinstance(center, Center):
777
+ instance.center = center
778
+ logger.debug(f"Updated center from Center object: {center.name}")
779
+ else:
780
+ logger.warning(
781
+ f"Invalid center type {type(center)}, expected Center instance. Ignoring."
782
+ )
783
+ # Remove from selected_data to prevent override
784
+ selected_data.pop("center", None)
785
+ elif center_name:
786
+ # center_name string provided - resolve to Center object
474
787
  try:
475
- center = Center.objects.get_by_natural_key(center_name)
476
- instance.center = center # Update center directly
477
- 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
788
+ center_obj = Center.objects.get(name=center_name)
789
+ instance.center = center_obj
790
+ logger.debug(f"Updated center from center_name string: {center_name}")
791
+ except Center.DoesNotExist:
792
+ logger.warning(
793
+ f"Center '{center_name}' not found during update. Keeping existing center."
794
+ )
795
+ else:
796
+ # Both are None/missing - remove 'center' from selected_data to preserve existing value
797
+ selected_data.pop("center", None)
798
+ # If both are None/missing, keep existing center (no update needed)
480
799
 
481
800
  # Set examiner names if provided, before calling save
482
801
  examiner_first_name = data.get("examiner_first_name")
483
802
  examiner_last_name = data.get("examiner_last_name")
484
- if examiner_first_name is not None: # Allow setting empty strings
803
+ if examiner_first_name is not None: # Allow setting empty strings
485
804
  instance.examiner_first_name = examiner_first_name
486
805
  if examiner_last_name is not None:
487
806
  instance.examiner_last_name = examiner_last_name
@@ -495,10 +814,14 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
495
814
  elif isinstance(patient_gender_input, str):
496
815
  gender_input_clean = patient_gender_input.strip()
497
816
  # Try direct case-insensitive DB lookup first
498
- gender_obj = Gender.objects.filter(name__iexact=gender_input_clean).first()
817
+ gender_obj = Gender.objects.filter(
818
+ name__iexact=gender_input_clean
819
+ ).first()
499
820
  if gender_obj:
500
821
  selected_data["patient_gender"] = gender_obj
501
- logger.debug(f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup")
822
+ logger.debug(
823
+ f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup"
824
+ )
502
825
  else:
503
826
  # Use mapping helper for fallback
504
827
  mapped = _map_gender_string_to_standard(gender_input_clean)
@@ -506,95 +829,177 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
506
829
  gender_obj = Gender.objects.filter(name__iexact=mapped).first()
507
830
  if gender_obj:
508
831
  selected_data["patient_gender"] = gender_obj
509
- logger.info(f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping")
832
+ logger.info(
833
+ f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
834
+ )
510
835
  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()
836
+ logger.warning(
837
+ f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'."
838
+ )
839
+ unknown_gender = Gender.objects.filter(
840
+ name__iexact="unknown"
841
+ ).first()
513
842
  if unknown_gender:
514
843
  selected_data["patient_gender"] = unknown_gender
515
- logger.warning(f"Using 'unknown' gender as fallback for '{patient_gender_input}'")
844
+ logger.warning(
845
+ f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
846
+ )
516
847
  else:
517
- logger.error(f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update.")
848
+ logger.error(
849
+ f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
850
+ )
518
851
  selected_data.pop("patient_gender", None)
519
852
  else:
520
853
  # Last resort: try to get 'unknown' gender
521
- unknown_gender = Gender.objects.filter(name__iexact='unknown').first()
854
+ unknown_gender = Gender.objects.filter(
855
+ name__iexact="unknown"
856
+ ).first()
522
857
  if unknown_gender:
523
858
  selected_data["patient_gender"] = unknown_gender
524
- logger.warning(f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)")
859
+ logger.warning(
860
+ f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
861
+ )
525
862
  else:
526
- logger.error(f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update.")
863
+ logger.error(
864
+ f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
865
+ )
527
866
  selected_data.pop("patient_gender", None)
528
867
  else:
529
- logger.warning(f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update.")
868
+ logger.warning(
869
+ f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update."
870
+ )
530
871
  selected_data.pop("patient_gender", None)
531
872
  except Exception as e:
532
- logger.exception(f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update.")
873
+ logger.exception(
874
+ f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update."
875
+ )
533
876
  selected_data.pop("patient_gender", None)
534
877
 
535
878
  # Update other attributes from selected_data
536
879
  patient_name_changed = False
537
880
  for k, v in selected_data.items():
538
- # 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):
881
+ # Skip None values to avoid overwriting existing data
882
+ if v is None:
883
+ logger.debug(f"Skipping field '{k}' during update because value is None")
884
+ continue
542
885
 
886
+ # Avoid overwriting examiner names if they were just explicitly set
887
+ if (
888
+ k not in ["examiner_first_name", "examiner_last_name"]
889
+ or (k == "examiner_first_name" and examiner_first_name is None)
890
+ or (k == "examiner_last_name" and examiner_last_name is None)
891
+ ):
543
892
  try:
544
893
  # --- Convert patient_dob if it's a date object ---
545
894
  value_to_set = v
546
895
  if k == "patient_dob":
547
896
  if isinstance(v, date) and not isinstance(v, datetime):
548
- aware_dob = timezone.make_aware(datetime.combine(v, datetime.min.time()))
897
+ aware_dob = timezone.make_aware(
898
+ datetime.combine(v, datetime.min.time())
899
+ )
549
900
  value_to_set = aware_dob
550
- logger.debug("Converted patient_dob from date to aware datetime during update: %s", aware_dob)
901
+ logger.debug(
902
+ "Converted patient_dob from date to aware datetime during update: %s",
903
+ aware_dob,
904
+ )
551
905
  elif isinstance(v, str):
552
906
  # 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)
907
+ if v == "patient_dob" or v in [
908
+ "patient_first_name",
909
+ "patient_last_name",
910
+ "examination_date",
911
+ ]:
912
+ logger.warning(
913
+ "Skipping invalid patient_dob value '%s' during update - appears to be field name",
914
+ v,
915
+ )
555
916
  continue # Skip this field
556
917
  else:
557
918
  # Try to parse as date string
558
919
  try:
559
920
  import dateparser
560
- parsed_dob = dateparser.parse(v, languages=['de'], settings={'DATE_ORDER': 'DMY'})
921
+
922
+ parsed_dob = dateparser.parse(
923
+ v, languages=["de"], settings={"DATE_ORDER": "DMY"}
924
+ )
561
925
  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)
926
+ value_to_set = timezone.make_aware(
927
+ parsed_dob.replace(
928
+ hour=0, minute=0, second=0, microsecond=0
929
+ )
930
+ )
931
+ logger.debug(
932
+ "Parsed string patient_dob '%s' during update to aware datetime: %s",
933
+ v,
934
+ value_to_set,
935
+ )
564
936
  else:
565
- logger.warning("Could not parse patient_dob string '%s' during update, skipping", v)
937
+ logger.warning(
938
+ "Could not parse patient_dob string '%s' during update, skipping",
939
+ v,
940
+ )
566
941
  continue
567
942
  except Exception as e:
568
- logger.warning("Error parsing patient_dob string '%s' during update: %s, skipping", v, e)
943
+ logger.warning(
944
+ "Error parsing patient_dob string '%s' during update: %s, skipping",
945
+ v,
946
+ e,
947
+ )
569
948
  continue
570
949
  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)
950
+ if v == "examination_date" or v in [
951
+ "patient_first_name",
952
+ "patient_last_name",
953
+ "patient_dob",
954
+ ]:
955
+ logger.warning(
956
+ "Skipping invalid examination_date value '%s' during update - appears to be field name",
957
+ v,
958
+ )
573
959
  continue
574
960
  else:
575
961
  # Try to parse as date string
576
962
  try:
577
963
  import dateparser
578
- parsed_date = dateparser.parse(v, languages=['de'], settings={'DATE_ORDER': 'DMY'})
964
+
965
+ parsed_date = dateparser.parse(
966
+ v, languages=["de"], settings={"DATE_ORDER": "DMY"}
967
+ )
579
968
  if parsed_date:
580
969
  value_to_set = parsed_date.date()
581
- logger.debug("Parsed string examination_date '%s' during update to date: %s", v, value_to_set)
970
+ logger.debug(
971
+ "Parsed string examination_date '%s' during update to date: %s",
972
+ v,
973
+ value_to_set,
974
+ )
582
975
  else:
583
- logger.warning("Could not parse examination_date string '%s' during update, skipping", v)
976
+ logger.warning(
977
+ "Could not parse examination_date string '%s' during update, skipping",
978
+ v,
979
+ )
584
980
  continue
585
981
  except Exception as e:
586
- logger.warning("Error parsing examination_date string '%s' during update: %s, skipping", v, e)
982
+ logger.warning(
983
+ "Error parsing examination_date string '%s' during update: %s, skipping",
984
+ v,
985
+ e,
986
+ )
587
987
  continue
588
988
  # --- End Conversion ---
589
989
 
590
990
  # Check if patient name is changing
591
- if k in ["patient_first_name", "patient_last_name"] and getattr(instance, k) != value_to_set:
991
+ if (
992
+ k in ["patient_first_name", "patient_last_name"]
993
+ and getattr(instance, k) != value_to_set
994
+ ):
592
995
  patient_name_changed = True
593
-
594
- setattr(instance, k, value_to_set) # Use value_to_set
595
-
996
+
997
+ setattr(instance, k, value_to_set) # Use value_to_set
998
+
596
999
  except Exception as e:
597
- logger.error(f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field.")
1000
+ logger.error(
1001
+ f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field."
1002
+ )
598
1003
  continue
599
1004
 
600
1005
  # Update name DB if patient names changed
@@ -617,7 +1022,7 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
617
1022
  def update_or_create_sensitive_meta_from_dict(
618
1023
  cls: Type["SensitiveMeta"],
619
1024
  data: Dict[str, Any],
620
- instance: Optional["SensitiveMeta"] = None
1025
+ instance: Optional["SensitiveMeta"] = None,
621
1026
  ) -> "SensitiveMeta":
622
1027
  """Logic to update or create a SensitiveMeta instance from a dictionary."""
623
1028
  # Check if the instance already exists based on unique fields
@@ -632,9 +1037,9 @@ def update_or_create_sensitive_meta_from_dict(
632
1037
  def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
633
1038
  """Maps various gender string inputs to standard gender names used in the DB."""
634
1039
  mapping = {
635
- 'male': ['male', 'm', 'männlich', 'man'],
636
- 'female': ['female', 'f', 'weiblich', 'woman'],
637
- 'unknown': ['unknown', 'unbekannt', 'other', 'diverse', '']
1040
+ "male": ["male", "m", "männlich", "man"],
1041
+ "female": ["female", "f", "weiblich", "woman"],
1042
+ "unknown": ["unknown", "unbekannt", "other", "diverse", ""],
638
1043
  }
639
1044
  gender_lower = gender_str.strip().lower()
640
1045
  for standard, variants in mapping.items():