endoreg-db 0.8.3.7__py3-none-any.whl → 0.8.6.3__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.
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 +19 -5
  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 +63 -43
  11. endoreg_db/models/metadata/sensitive_meta_logic.py +251 -25
  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 +485 -242
  21. endoreg_db/urls/__init__.py +36 -23
  22. endoreg_db/urls/label_video_segments.py +2 -0
  23. endoreg_db/urls/media.py +3 -2
  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 +187 -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.7.dist-info → endoreg_db-0.8.6.3.dist-info}/METADATA +1 -2
  36. {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.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.7.dist-info → endoreg_db-0.8.6.3.dist-info}/WHEEL +0 -0
  41. {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,329 +1,242 @@
1
- # endoreg_db/views/examination_views.py
2
- from rest_framework.viewsets import ModelViewSet
3
- from rest_framework import status
4
- from rest_framework.decorators import api_view
5
- from rest_framework.response import Response
1
+ # endoreg_db/views/video/video_examination_viewset.py
2
+ """
3
+ Video Examination ViewSet
4
+
5
+ Provides REST API endpoints for managing video-based patient examinations.
6
+ Handles CRUD operations for PatientExamination records linked to VideoFile instances.
7
+
8
+ **API Endpoints:**
9
+ - GET /api/video-examinations/ - List all video examinations
10
+ - GET /api/video-examinations/{id}/ - Get examination details
11
+ - POST /api/video-examinations/ - Create new examination
12
+ - PATCH /api/video-examinations/{id}/ - Update examination
13
+ - DELETE /api/video-examinations/{id}/ - Delete examination
14
+ - GET /api/video/{video_id}/examinations/ - List examinations for specific video
15
+
16
+ **Frontend Integration:**
17
+ Used by VideoExaminationAnnotation.vue for annotation workflow.
18
+ """
19
+
20
+ import logging
21
+
22
+ from django.db import transaction
6
23
  from django.shortcuts import get_object_or_404
7
- from rest_framework import viewsets
8
-
9
- from endoreg_db.models import (
10
- Examination,
11
- VideoFile,
12
- FindingClassification,
13
- FindingClassificationChoice,
14
- FindingIntervention,
15
- Finding
16
- )
17
- from ...serializers.examination import (
18
- ExaminationSerializer as ExaminationSerializer,
19
- # FindingSerializer as FindingSerializer,
20
- )
24
+ from rest_framework import status, viewsets
25
+ from rest_framework.decorators import action
26
+ from rest_framework.response import Response
27
+
28
+ from endoreg_db.models import PatientExamination, VideoFile
21
29
 
30
+ from ...serializers.video_examination import (
31
+ VideoExaminationCreateSerializer,
32
+ VideoExaminationSerializer,
33
+ VideoExaminationUpdateSerializer,
34
+ )
22
35
 
23
- class ExaminationViewSet(ModelViewSet):
24
- queryset = Examination.objects.all()
25
- serializer_class = ExaminationSerializer
36
+ logger = logging.getLogger(__name__)
26
37
 
27
38
 
28
- # NEW: Video Examination CRUD ViewSet
29
39
  class VideoExaminationViewSet(viewsets.ModelViewSet):
30
40
  """
31
- ViewSet for Video Examination CRUD operations
32
- Handles POST and PATCH for video examinations at timestamps
41
+ ViewSet for Video Examination CRUD operations.
42
+
43
+ Provides comprehensive API for managing patient examinations within
44
+ the video annotation workflow. Supports filtering by video, patient,
45
+ and examination type.
46
+
47
+ **Usage Example:**
48
+ ```python
49
+ # Frontend (JavaScript)
50
+ // Get examinations for video 123
51
+ const response = await api.get('/api/video/123/examinations/');
52
+
53
+ // Create new examination
54
+ await api.post('/api/video-examinations/', {
55
+ video_id: 123,
56
+ examination_id: 5,
57
+ date_start: '2024-01-15'
58
+ });
59
+ ```
33
60
  """
34
-
61
+
62
+ queryset = PatientExamination.objects.select_related(
63
+ "patient", "examination", "video"
64
+ ).prefetch_related("patient_findings")
65
+ serializer_class = VideoExaminationSerializer
66
+
67
+ def get_serializer_class(self):
68
+ """
69
+ Return appropriate serializer based on action.
70
+
71
+ - create: VideoExaminationCreateSerializer (handles complex creation logic)
72
+ - update/partial_update: VideoExaminationUpdateSerializer
73
+ - list/retrieve: VideoExaminationSerializer (read-only with nested data)
74
+ """
75
+ if self.action == "create":
76
+ return VideoExaminationCreateSerializer
77
+ elif self.action in ["update", "partial_update"]:
78
+ return VideoExaminationUpdateSerializer
79
+ return VideoExaminationSerializer
80
+
35
81
  def get_queryset(self):
36
- # Return empty queryset as we handle retrieval manually
37
- return []
38
-
82
+ """
83
+ Filter examinations based on query parameters.
84
+
85
+ **Supported filters:**
86
+ - ?video_id=123 - Get examinations for specific video
87
+ - ?patient_id=456 - Get examinations for specific patient
88
+ - ?examination_id=789 - Get examinations of specific type
89
+ """
90
+ queryset = super().get_queryset()
91
+
92
+ # Filter by video if provided
93
+ video_id = self.request.query_params.get("video_id")
94
+ if video_id:
95
+ queryset = queryset.filter(video_id=video_id)
96
+
97
+ # Filter by patient if provided
98
+ patient_id = self.request.query_params.get("patient_id")
99
+ if patient_id:
100
+ queryset = queryset.filter(patient_id=patient_id)
101
+
102
+ # Filter by examination type if provided
103
+ examination_id = self.request.query_params.get("examination_id")
104
+ if examination_id:
105
+ queryset = queryset.filter(examination_id=examination_id)
106
+
107
+ return queryset
108
+
109
+ @action(detail=False, methods=["get"], url_path="video/(?P<video_id>[^/.]+)")
110
+ def by_video(self, request, video_id=None):
111
+ """
112
+ Get all examinations for a specific video.
113
+
114
+ **Endpoint:** GET /api/video-examinations/video/{video_id}/
115
+ **Alternative:** GET /api/video/{video_id}/examinations/
116
+
117
+ Args:
118
+ video_id: ID of the video
119
+
120
+ Returns:
121
+ 200: List of examinations for the video
122
+ 404: Video not found
123
+ """
124
+ # Validate video exists
125
+ video = get_object_or_404(VideoFile, id=video_id)
126
+
127
+ # Get examinations for this video
128
+ examinations = self.queryset.filter(video=video)
129
+
130
+ serializer = self.get_serializer(examinations, many=True)
131
+ return Response(serializer.data)
132
+
39
133
  def create(self, request, *args, **kwargs):
40
134
  """
41
- Create a new video examination
42
- POST /api/examinations/
43
-
44
- Expected payload:
135
+ Create a new video examination.
136
+
137
+ **Endpoint:** POST /api/video-examinations/
138
+
139
+ **Payload:**
140
+ ```json
45
141
  {
46
- "videoId": 123,
47
- "timestamp": 45.5,
48
- "examinationTypeId": 1,
49
- "findingId": 2,
50
- "locationClassificationId": 3,
51
- "locationChoiceId": 4,
52
- "morphologyClassificationId": 5,
53
- "morphologyChoiceId": 6,
54
- "interventionIds": [7, 8],
55
- "notes": "Sample notes"
142
+ "video_id": 123,
143
+ "examination_id": 5,
144
+ "date_start": "2024-01-15",
145
+ "date_end": "2024-01-15"
56
146
  }
147
+ ```
148
+
149
+ Returns:
150
+ 201: Examination created successfully
151
+ 400: Invalid data (missing required fields, validation errors)
152
+ 404: Video or examination type not found
57
153
  """
58
- from django.db import transaction
59
- import logging
60
-
61
- logger = logging.getLogger(__name__)
62
-
154
+ serializer = self.get_serializer(data=request.data)
155
+ serializer.is_valid(raise_exception=True)
156
+
63
157
  try:
64
- data = request.data
65
-
66
- # Validate required fields
67
- required_fields = ['videoId', 'timestamp', 'examinationTypeId', 'findingId']
68
- for field in required_fields:
69
- if field not in data or data[field] is None:
70
- return Response(
71
- {'error': f'Missing or null required field: {field}'},
72
- status=status.HTTP_400_BAD_REQUEST
73
- )
74
-
75
- # Validate data types
76
- try:
77
- video_id = int(data['videoId'])
78
- timestamp = float(data['timestamp'])
79
- examination_type_id = int(data['examinationTypeId'])
80
- finding_id = int(data['findingId'])
81
- except (ValueError, TypeError) as e:
82
- return Response(
83
- {'error': f'Invalid data type in request: {str(e)}'},
84
- status=status.HTTP_400_BAD_REQUEST
158
+ with transaction.atomic():
159
+ patient_exam = serializer.save()
160
+
161
+ # Return created examination with full serialization
162
+ response_serializer = VideoExaminationSerializer(patient_exam)
163
+ logger.info(
164
+ f"Created video examination: video={request.data.get('video_id')}, "
165
+ f"exam={request.data.get('examination_id')}"
85
166
  )
86
-
87
- # Validate timestamp is not negative
88
- if timestamp < 0:
89
167
  return Response(
90
- {'error': 'Timestamp cannot be negative'},
91
- status=status.HTTP_400_BAD_REQUEST
168
+ response_serializer.data, status=status.HTTP_201_CREATED
92
169
  )
93
-
94
- with transaction.atomic():
95
- # Get video
96
- try:
97
- video = VideoFile.objects.get(id=video_id)
98
- except VideoFile.DoesNotExist:
99
- return Response(
100
- {'error': 'Video not found'},
101
- status=status.HTTP_404_NOT_FOUND
102
- )
103
-
104
- # Get examination type
105
- try:
106
- examination = Examination.objects.get(id=examination_type_id)
107
- except Examination.DoesNotExist:
108
- return Response(
109
- {'error': 'Examination type not found'},
110
- status=status.HTTP_404_NOT_FOUND
111
- )
112
-
113
- # Get finding
114
- try:
115
- finding = Finding.objects.get(id=finding_id)
116
- except Finding.DoesNotExist:
117
- return Response(
118
- {'error': 'Finding not found'},
119
- status=status.HTTP_404_NOT_FOUND
120
- )
121
-
122
- # Validate optional foreign keys if provided
123
- if data.get('locationClassificationId'):
124
- try:
125
- FindingClassification.objects.get(id=data['locationClassificationId'], classification_types__name__iexact="location")
126
- except FindingClassification.DoesNotExist:
127
- return Response(
128
- {'error': 'Location classification not found'},
129
- status=status.HTTP_404_NOT_FOUND
130
- )
131
-
132
- if data.get('morphologyClassificationId'):
133
- try:
134
- FindingClassification.objects.get(
135
- id=data['morphologyClassificationId'],
136
- classification_types__name__iexact="morphology"
137
- )
138
- except FindingClassification.DoesNotExist:
139
- return Response(
140
- {'error': 'Morphology classification not found'},
141
- status=status.HTTP_404_NOT_FOUND
142
- )
143
-
144
- # Create examination record
145
- examination_data = {
146
- 'id': f"exam_{video.id}_{timestamp}_{examination.id}",
147
- 'video_id': video_id,
148
- 'timestamp': timestamp,
149
- 'examination_type': examination.name,
150
- 'finding': finding.name,
151
- 'location_classification': data.get('locationClassificationId'),
152
- 'location_choice': data.get('locationChoiceId'),
153
- 'morphology_classification': data.get('morphologyClassificationId'),
154
- 'morphology_choice': data.get('morphologyChoiceId'),
155
- 'interventions': data.get('interventionIds', []),
156
- 'notes': data.get('notes', ''),
157
- 'created_at': '2024-01-01T00:00:00Z' # Placeholder timestamp
158
- }
159
-
160
- logger.info(f"Created video examination for video {video_id} at timestamp {timestamp}")
161
- return Response(examination_data, status=status.HTTP_201_CREATED)
162
-
163
170
  except Exception as e:
164
- logger.error(f"Unexpected error creating examination: {str(e)}")
171
+ logger.error(f"Error creating video examination: {str(e)}")
165
172
  return Response(
166
- {'error': 'Internal server error while creating examination'},
167
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
173
+ {"error": "Internal server error while creating examination"},
174
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
168
175
  )
169
-
176
+
170
177
  def update(self, request, *args, **kwargs):
171
178
  """
172
- Update an existing video examination
173
- PATCH /api/examinations/{id}/
179
+ Update an existing video examination.
180
+
181
+ **Endpoint:** PATCH /api/video-examinations/{id}/
182
+
183
+ **Payload:**
184
+ ```json
185
+ {
186
+ "examination_id": 6,
187
+ "date_start": "2024-01-16"
188
+ }
189
+ ```
190
+
191
+ Returns:
192
+ 200: Examination updated successfully
193
+ 400: Invalid data
194
+ 404: Examination not found
174
195
  """
175
- from django.db import transaction
176
- import logging
177
-
178
- logger = logging.getLogger(__name__)
179
-
196
+ partial = kwargs.pop("partial", False)
197
+ instance = self.get_object()
198
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
199
+ serializer.is_valid(raise_exception=True)
200
+
180
201
  try:
181
- examination_id = kwargs.get('pk')
182
- if not examination_id:
183
- return Response(
184
- {'error': 'Examination ID is required'},
185
- status=status.HTTP_400_BAD_REQUEST
186
- )
187
-
188
- data = request.data
189
-
190
- # Validate data types for provided fields
191
- if 'videoId' in data:
192
- try:
193
- data['videoId'] = int(data['videoId'])
194
- except (ValueError, TypeError):
195
- return Response(
196
- {'error': 'Invalid videoId format'},
197
- status=status.HTTP_400_BAD_REQUEST
198
- )
199
-
200
- if 'timestamp' in data:
201
- try:
202
- timestamp = float(data['timestamp'])
203
- if timestamp < 0:
204
- return Response(
205
- {'error': 'Timestamp cannot be negative'},
206
- status=status.HTTP_400_BAD_REQUEST
207
- )
208
- data['timestamp'] = timestamp
209
- except (ValueError, TypeError):
210
- return Response(
211
- {'error': 'Invalid timestamp format'},
212
- status=status.HTTP_400_BAD_REQUEST
213
- )
214
-
215
202
  with transaction.atomic():
216
- # Validate foreign keys if provided
217
- if 'videoId' in data:
218
- try:
219
- VideoFile.objects.get(id=data['videoId'])
220
- except VideoFile.DoesNotExist:
221
- return Response(
222
- {'error': 'Video not found'},
223
- status=status.HTTP_404_NOT_FOUND
224
- )
225
-
226
- if 'examinationTypeId' in data:
227
- try:
228
- examination_type_id = int(data['examinationTypeId'])
229
- Examination.objects.get(id=examination_type_id)
230
- except (ValueError, TypeError):
231
- return Response(
232
- {'error': 'Invalid examination type ID format'},
233
- status=status.HTTP_400_BAD_REQUEST
234
- )
235
- except Examination.DoesNotExist:
236
- return Response(
237
- {'error': 'Examination type not found'},
238
- status=status.HTTP_404_NOT_FOUND
239
- )
240
-
241
- if 'findingId' in data:
242
- try:
243
- finding_id = int(data['findingId'])
244
- Finding.objects.get(id=finding_id)
245
- except (ValueError, TypeError):
246
- return Response(
247
- {'error': 'Invalid finding ID format'},
248
- status=status.HTTP_400_BAD_REQUEST
249
- )
250
- except Finding.DoesNotExist:
251
- return Response(
252
- {'error': 'Finding not found'},
253
- status=status.HTTP_404_NOT_FOUND
254
- )
255
-
256
- # Return updated examination data
257
- examination_data = {
258
- 'id': examination_id,
259
- 'video_id': data.get('videoId'),
260
- 'timestamp': data.get('timestamp'),
261
- 'examination_type': data.get('examinationTypeId'),
262
- 'finding': data.get('findingId'),
263
- 'location_classification': data.get('locationClassificationId'),
264
- 'location_choice': data.get('locationChoiceId'),
265
- 'morphology_classification': data.get('morphologyClassificationId'),
266
- 'morphology_choice': data.get('morphologyChoiceId'),
267
- 'interventions': data.get('interventionIds', []),
268
- 'notes': data.get('notes', ''),
269
- 'updated_at': '2024-01-01T00:00:00Z' # Placeholder timestamp
270
- }
271
-
272
- logger.info(f"Updated video examination {examination_id}")
273
- return Response(examination_data, status=status.HTTP_200_OK)
274
-
203
+ patient_exam = serializer.save()
204
+
205
+ # Return updated examination
206
+ response_serializer = VideoExaminationSerializer(patient_exam)
207
+ logger.info(f"Updated video examination {instance.id}")
208
+ return Response(response_serializer.data)
275
209
  except Exception as e:
276
- logger.error(f"Unexpected error updating examination {examination_id}: {str(e)}")
210
+ logger.error(f"Error updating video examination {instance.id}: {str(e)}")
277
211
  return Response(
278
- {'error': 'Internal server error while updating examination'},
279
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
212
+ {"error": "Internal server error while updating examination"},
213
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
280
214
  )
281
-
215
+
282
216
  def destroy(self, request, *args, **kwargs):
283
217
  """
284
- Delete a video examination
285
- DELETE /api/examinations/{id}/
218
+ Delete a video examination.
219
+
220
+ **Endpoint:** DELETE /api/video-examinations/{id}/
221
+
222
+ Returns:
223
+ 204: Examination deleted successfully
224
+ 404: Examination not found
286
225
  """
287
- from django.db import transaction
288
- import logging
289
-
290
- logger = logging.getLogger(__name__)
291
-
226
+ instance = self.get_object()
227
+ examination_id = instance.id
228
+
292
229
  try:
293
- examination_id = kwargs.get('pk')
294
- if not examination_id:
295
- return Response(
296
- {'error': 'Examination ID is required'},
297
- status=status.HTTP_400_BAD_REQUEST
298
- )
299
-
300
- # Validate examination_id format
301
- try:
302
- # For now, we're using string IDs, so just validate it's not empty
303
- if not str(examination_id).strip():
304
- return Response(
305
- {'error': 'Invalid examination ID format'},
306
- status=status.HTTP_400_BAD_REQUEST
307
- )
308
- except (ValueError, TypeError):
309
- return Response(
310
- {'error': 'Invalid examination ID format'},
311
- status=status.HTTP_400_BAD_REQUEST
312
- )
313
-
314
230
  with transaction.atomic():
315
- # For now, we simulate successful deletion
316
- # TODO: Implement actual examination record deletion when persistence is added
317
-
231
+ instance.delete()
318
232
  logger.info(f"Deleted video examination {examination_id}")
319
233
  return Response(
320
- {'message': f'Examination {examination_id} deleted successfully'},
321
- status=status.HTTP_204_NO_CONTENT
234
+ {"message": f"Examination {examination_id} deleted successfully"},
235
+ status=status.HTTP_204_NO_CONTENT,
322
236
  )
323
-
324
237
  except Exception as e:
325
- logger.error(f"Unexpected error deleting examination {examination_id}: {str(e)}")
238
+ logger.error(f"Error deleting examination {examination_id}: {str(e)}")
326
239
  return Response(
327
- {'error': 'Internal server error while deleting examination'},
328
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
240
+ {"error": "Internal server error while deleting examination"},
241
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
329
242
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: endoreg-db
3
- Version: 0.8.3.7
3
+ Version: 0.8.6.3
4
4
  Summary: EndoReg Db Django App
5
5
  Project-URL: Homepage, https://info.coloreg.de
6
6
  Project-URL: Repository, https://github.com/wg-lux/endoreg-db
@@ -29,7 +29,6 @@ Requires-Dist: dotenv>=0.9.9
29
29
  Requires-Dist: faker>=37.6.0
30
30
  Requires-Dist: flake8>=7.3.0
31
31
  Requires-Dist: gunicorn>=23.0.0
32
- Requires-Dist: huggingface-hub>=0.35.3
33
32
  Requires-Dist: icecream>=2.1.4
34
33
  Requires-Dist: librosa==0.11.0
35
34
  Requires-Dist: llvmlite>=0.44.0