endoreg-db 0.8.3.4__py3-none-any.whl → 0.8.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +5 -20
- endoreg_db/management/commands/setup_endoreg_db.py +14 -1
- endoreg_db/models/metadata/model_meta_logic.py +31 -62
- endoreg_db/models/metadata/sensitive_meta_logic.py +314 -135
- endoreg_db/services/video_import.py +32 -12
- {endoreg_db-0.8.3.4.dist-info → endoreg_db-0.8.3.8.dist-info}/METADATA +1 -1
- {endoreg_db-0.8.3.4.dist-info → endoreg_db-0.8.3.8.dist-info}/RECORD +9 -9
- {endoreg_db-0.8.3.4.dist-info → endoreg_db-0.8.3.8.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.3.4.dist-info → endoreg_db-0.8.3.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -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}"))
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
from django.core.management import call_command
|
|
10
10
|
from django.core.management.base import BaseCommand
|
|
11
|
-
|
|
11
|
+
from endoreg_db.models import ModelMeta
|
|
12
12
|
|
|
13
13
|
class Command(BaseCommand):
|
|
14
14
|
help = """
|
|
@@ -137,6 +137,14 @@ class Command(BaseCommand):
|
|
|
137
137
|
def _find_model_weights_file(self):
|
|
138
138
|
"""Find the model weights file in various possible locations."""
|
|
139
139
|
# Check common locations for model weights
|
|
140
|
+
|
|
141
|
+
if not ModelMeta.objects.exists():
|
|
142
|
+
print("📦 No model metadata found — creating from Hugging Face...")
|
|
143
|
+
ModelMeta.setup_default_from_huggingface(
|
|
144
|
+
"wg-lux/colo_segmentation_RegNetX800MF_base",
|
|
145
|
+
labelset_name="multilabel_classification_colonoscopy_default"
|
|
146
|
+
)
|
|
147
|
+
print("✅ Default ModelMeta created.")
|
|
140
148
|
possible_paths = [
|
|
141
149
|
# Test assets (for development)
|
|
142
150
|
Path("tests/assets/colo_segmentation_RegNetX800MF_6.ckpt"),
|
|
@@ -154,7 +162,10 @@ class Command(BaseCommand):
|
|
|
154
162
|
return path
|
|
155
163
|
|
|
156
164
|
self.stdout.write("Model weights file not found in standard locations")
|
|
165
|
+
|
|
157
166
|
return None
|
|
167
|
+
|
|
168
|
+
|
|
158
169
|
|
|
159
170
|
def _verify_setup(self):
|
|
160
171
|
"""Verify that the setup was successful."""
|
|
@@ -194,3 +205,5 @@ class Command(BaseCommand):
|
|
|
194
205
|
self.stdout.write(f"Found {meta_count} model metadata record(s)")
|
|
195
206
|
|
|
196
207
|
self.stdout.write("Setup verification passed")
|
|
208
|
+
|
|
209
|
+
|
|
@@ -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,
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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"],
|
|
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
|
-
|
|
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(),
|
|
135
|
-
**kwargs,
|
|
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
|
|
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)
|
|
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,
|
|
188
|
-
"size_x": model_meta.size_x,
|
|
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
|
|
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,
|
|
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
|
|
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)
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
265
|
+
instance.patient_hash = calculate_patient_hash(instance)
|
|
262
266
|
if not instance.examination_hash:
|
|
263
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
patient_examination, _created =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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(
|
|
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 = {
|
|
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 [
|
|
352
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 [
|
|
375
|
-
|
|
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(
|
|
428
|
+
if len(exam_date) == 10 and exam_date.count("-") == 2:
|
|
382
429
|
try:
|
|
383
430
|
from datetime import datetime as dt
|
|
384
|
-
|
|
431
|
+
|
|
432
|
+
parsed_date = dt.strptime(exam_date, "%Y-%m-%d").date()
|
|
385
433
|
selected_data["examination_date"] = parsed_date
|
|
386
|
-
logger.debug(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
511
|
+
selected_data["patient_gender"] = Gender.objects.get(
|
|
512
|
+
name=patient_gender_input
|
|
513
|
+
)
|
|
436
514
|
except Gender.DoesNotExist:
|
|
437
|
-
logger.warning(
|
|
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
|
|
440
|
-
|
|
441
|
-
if not isinstance(
|
|
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(
|
|
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(
|
|
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(
|
|
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()
|
|
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(
|
|
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 = {
|
|
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 = {
|
|
469
|
-
selected_data = {
|
|
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
|
|
572
|
+
instance.center = center # Update center directly
|
|
477
573
|
except Center.DoesNotExist as exc:
|
|
478
|
-
logger.warning(
|
|
479
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
611
|
+
logger.info(
|
|
612
|
+
f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
|
|
613
|
+
)
|
|
510
614
|
else:
|
|
511
|
-
logger.warning(
|
|
512
|
-
|
|
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(
|
|
623
|
+
logger.warning(
|
|
624
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
|
|
625
|
+
)
|
|
516
626
|
else:
|
|
517
|
-
logger.error(
|
|
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(
|
|
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(
|
|
638
|
+
logger.warning(
|
|
639
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
|
|
640
|
+
)
|
|
525
641
|
else:
|
|
526
|
-
logger.error(
|
|
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(
|
|
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(
|
|
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
|
|
540
|
-
|
|
541
|
-
|
|
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(
|
|
671
|
+
aware_dob = timezone.make_aware(
|
|
672
|
+
datetime.combine(v, datetime.min.time())
|
|
673
|
+
)
|
|
549
674
|
value_to_set = aware_dob
|
|
550
|
-
logger.debug(
|
|
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 [
|
|
554
|
-
|
|
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
|
-
|
|
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(
|
|
563
|
-
|
|
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(
|
|
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(
|
|
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 [
|
|
572
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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)
|
|
595
|
-
|
|
770
|
+
|
|
771
|
+
setattr(instance, k, value_to_set) # Use value_to_set
|
|
772
|
+
|
|
596
773
|
except Exception as e:
|
|
597
|
-
logger.error(
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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[
|
|
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,
|
|
836
|
+
is_available, frame_cleaner = self._ensure_frame_cleaning_available()
|
|
822
837
|
|
|
823
|
-
if not is_available
|
|
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
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
|
|
840
859
|
# Clean video with ROI masking (heavy I/O operation)
|
|
841
|
-
actual_cleaned_path, extracted_metadata =
|
|
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
|
|
|
@@ -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=
|
|
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
|
|
@@ -290,7 +290,7 @@ endoreg_db/management/commands/load_unit_data.py,sha256=tcux-iL-ByT2ApgmHEkLllZS
|
|
|
290
290
|
endoreg_db/management/commands/load_user_groups.py,sha256=D7SK2FvZEHoE4TIXNGCjDw5_12MH9bpGZvoS7eEv0Os,1031
|
|
291
291
|
endoreg_db/management/commands/register_ai_model.py,sha256=KixTfuQR6TUfRmzB5GOos16BFOz7NL4TzLzBkgtPPgE,2510
|
|
292
292
|
endoreg_db/management/commands/reset_celery_schedule.py,sha256=U-m_FNRTw6LAwJoT9RUE4qrhmQXm7AyFToPcHYyJpIE,386
|
|
293
|
-
endoreg_db/management/commands/setup_endoreg_db.py,sha256=
|
|
293
|
+
endoreg_db/management/commands/setup_endoreg_db.py,sha256=efOXE6IQs4ey84tIncf6zXI2VVVRd7CYXbeuACFgkgI,9095
|
|
294
294
|
endoreg_db/management/commands/start_filewatcher.py,sha256=3jESBqRiYPa9f35--zd70qQaYnyT0tzRO_b_HJuyteQ,4093
|
|
295
295
|
endoreg_db/management/commands/storage_management.py,sha256=NpToX59ndwTFNmnSoeppmiPdMvpjSHH7mAdIe4SvUoI,22396
|
|
296
296
|
endoreg_db/management/commands/summarize_db_content.py,sha256=pOIz3qbY4Ktmh0zV_DKFx971VD0pPx027gCD7a47EL0,10766
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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.
|
|
790
|
-
endoreg_db-0.8.3.
|
|
791
|
-
endoreg_db-0.8.3.
|
|
792
|
-
endoreg_db-0.8.3.
|
|
789
|
+
endoreg_db-0.8.3.8.dist-info/METADATA,sha256=4evIJXXr-HVMvN07sn2wZ0MXR3LujkKoOPF7os_3Bbg,14758
|
|
790
|
+
endoreg_db-0.8.3.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
791
|
+
endoreg_db-0.8.3.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
792
|
+
endoreg_db-0.8.3.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|