endoreg-db 0.8.8.9__py3-none-any.whl → 0.8.9.10__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 (453) hide show
  1. endoreg_db/admin.py +10 -5
  2. endoreg_db/apps.py +4 -7
  3. endoreg_db/authz/auth.py +1 -0
  4. endoreg_db/authz/backends.py +1 -1
  5. endoreg_db/authz/management/commands/list_routes.py +2 -0
  6. endoreg_db/authz/middleware.py +8 -7
  7. endoreg_db/authz/permissions.py +21 -10
  8. endoreg_db/authz/policy.py +14 -19
  9. endoreg_db/authz/views_auth.py +14 -10
  10. endoreg_db/codemods/rename_datetime_fields.py +8 -1
  11. endoreg_db/exceptions.py +5 -2
  12. endoreg_db/forms/__init__.py +0 -1
  13. endoreg_db/forms/examination_form.py +4 -3
  14. endoreg_db/forms/patient_finding_intervention_form.py +30 -8
  15. endoreg_db/forms/patient_form.py +9 -13
  16. endoreg_db/forms/questionnaires/__init__.py +1 -1
  17. endoreg_db/forms/settings/__init__.py +4 -1
  18. endoreg_db/forms/unit.py +2 -1
  19. endoreg_db/helpers/count_db.py +17 -14
  20. endoreg_db/helpers/default_objects.py +2 -1
  21. endoreg_db/helpers/download_segmentation_model.py +4 -3
  22. endoreg_db/helpers/interact.py +0 -5
  23. endoreg_db/helpers/test_video_helper.py +33 -25
  24. endoreg_db/import_files/__init__.py +1 -1
  25. endoreg_db/import_files/context/__init__.py +1 -1
  26. endoreg_db/import_files/context/default_sensitive_meta.py +11 -9
  27. endoreg_db/import_files/context/ensure_center.py +4 -4
  28. endoreg_db/import_files/context/file_lock.py +3 -3
  29. endoreg_db/import_files/context/import_context.py +11 -12
  30. endoreg_db/import_files/context/validate_directories.py +1 -0
  31. endoreg_db/import_files/file_storage/create_report_file.py +57 -34
  32. endoreg_db/import_files/file_storage/create_video_file.py +64 -35
  33. endoreg_db/import_files/file_storage/sensitive_meta_storage.py +5 -2
  34. endoreg_db/import_files/file_storage/state_management.py +146 -83
  35. endoreg_db/import_files/file_storage/storage.py +5 -1
  36. endoreg_db/import_files/processing/report_processing/report_anonymization.py +24 -19
  37. endoreg_db/import_files/processing/sensitive_meta_adapter.py +3 -3
  38. endoreg_db/import_files/processing/video_processing/video_anonymization.py +18 -18
  39. endoreg_db/import_files/pseudonymization/k_anonymity.py +8 -9
  40. endoreg_db/import_files/pseudonymization/k_pseudonymity.py +16 -5
  41. endoreg_db/import_files/report_import_service.py +36 -30
  42. endoreg_db/import_files/video_import_service.py +27 -23
  43. endoreg_db/logger_conf.py +56 -40
  44. endoreg_db/management/__init__.py +1 -1
  45. endoreg_db/management/commands/__init__.py +1 -1
  46. endoreg_db/management/commands/check_auth.py +45 -38
  47. endoreg_db/management/commands/create_model_meta_from_huggingface.py +53 -2
  48. endoreg_db/management/commands/create_multilabel_model_meta.py +54 -19
  49. endoreg_db/management/commands/fix_missing_patient_data.py +105 -71
  50. endoreg_db/management/commands/fix_video_paths.py +75 -54
  51. endoreg_db/management/commands/import_report.py +1 -3
  52. endoreg_db/management/commands/list_routes.py +2 -0
  53. endoreg_db/management/commands/load_ai_model_data.py +8 -2
  54. endoreg_db/management/commands/load_ai_model_label_data.py +0 -1
  55. endoreg_db/management/commands/load_center_data.py +3 -3
  56. endoreg_db/management/commands/load_distribution_data.py +35 -38
  57. endoreg_db/management/commands/load_endoscope_data.py +0 -3
  58. endoreg_db/management/commands/load_examination_data.py +20 -4
  59. endoreg_db/management/commands/load_finding_data.py +18 -3
  60. endoreg_db/management/commands/load_gender_data.py +17 -24
  61. endoreg_db/management/commands/load_green_endoscopy_wuerzburg_data.py +95 -85
  62. endoreg_db/management/commands/load_information_source.py +0 -3
  63. endoreg_db/management/commands/load_lab_value_data.py +14 -3
  64. endoreg_db/management/commands/load_legacy_data.py +303 -0
  65. endoreg_db/management/commands/load_name_data.py +1 -2
  66. endoreg_db/management/commands/load_pdf_type_data.py +4 -8
  67. endoreg_db/management/commands/load_profession_data.py +0 -1
  68. endoreg_db/management/commands/load_report_reader_flag_data.py +0 -4
  69. endoreg_db/management/commands/load_requirement_data.py +6 -2
  70. endoreg_db/management/commands/load_unit_data.py +0 -4
  71. endoreg_db/management/commands/load_user_groups.py +5 -7
  72. endoreg_db/management/commands/model_input.py +169 -0
  73. endoreg_db/management/commands/register_ai_model.py +22 -16
  74. endoreg_db/management/commands/setup_endoreg_db.py +110 -32
  75. endoreg_db/management/commands/storage_management.py +14 -8
  76. endoreg_db/management/commands/summarize_db_content.py +154 -63
  77. endoreg_db/management/commands/train_image_multilabel_model.py +144 -0
  78. endoreg_db/management/commands/validate_video_files.py +82 -50
  79. endoreg_db/management/commands/video_validation.py +4 -6
  80. endoreg_db/migrations/0001_initial.py +112 -63
  81. endoreg_db/migrations/__init__.py +0 -0
  82. endoreg_db/models/__init__.py +8 -0
  83. endoreg_db/models/administration/ai/active_model.py +5 -5
  84. endoreg_db/models/administration/ai/ai_model.py +41 -18
  85. endoreg_db/models/administration/ai/model_type.py +1 -0
  86. endoreg_db/models/administration/case/case.py +22 -22
  87. endoreg_db/models/administration/center/__init__.py +5 -5
  88. endoreg_db/models/administration/center/center.py +6 -2
  89. endoreg_db/models/administration/center/center_resource.py +18 -4
  90. endoreg_db/models/administration/center/center_shift.py +3 -1
  91. endoreg_db/models/administration/center/center_waste.py +6 -2
  92. endoreg_db/models/administration/person/__init__.py +1 -1
  93. endoreg_db/models/administration/person/employee/__init__.py +1 -1
  94. endoreg_db/models/administration/person/employee/employee_type.py +3 -1
  95. endoreg_db/models/administration/person/examiner/__init__.py +1 -1
  96. endoreg_db/models/administration/person/examiner/examiner.py +10 -2
  97. endoreg_db/models/administration/person/names/first_name.py +6 -4
  98. endoreg_db/models/administration/person/names/last_name.py +4 -3
  99. endoreg_db/models/administration/person/patient/__init__.py +1 -1
  100. endoreg_db/models/administration/person/patient/patient.py +0 -1
  101. endoreg_db/models/administration/person/patient/patient_external_id.py +0 -1
  102. endoreg_db/models/administration/person/person.py +1 -1
  103. endoreg_db/models/administration/product/__init__.py +7 -6
  104. endoreg_db/models/administration/product/product.py +6 -2
  105. endoreg_db/models/administration/product/product_group.py +9 -7
  106. endoreg_db/models/administration/product/product_material.py +9 -2
  107. endoreg_db/models/administration/product/reference_product.py +64 -15
  108. endoreg_db/models/administration/qualification/qualification.py +3 -1
  109. endoreg_db/models/administration/shift/shift.py +3 -1
  110. endoreg_db/models/administration/shift/shift_type.py +12 -4
  111. endoreg_db/models/aidataset/__init__.py +5 -0
  112. endoreg_db/models/aidataset/aidataset.py +193 -0
  113. endoreg_db/models/label/__init__.py +1 -1
  114. endoreg_db/models/label/label.py +10 -2
  115. endoreg_db/models/label/label_set.py +3 -1
  116. endoreg_db/models/label/label_video_segment/_create_from_video.py +6 -2
  117. endoreg_db/models/label/label_video_segment/label_video_segment.py +148 -44
  118. endoreg_db/models/media/__init__.py +12 -5
  119. endoreg_db/models/media/frame/__init__.py +1 -1
  120. endoreg_db/models/media/frame/frame.py +34 -8
  121. endoreg_db/models/media/pdf/__init__.py +2 -1
  122. endoreg_db/models/media/pdf/raw_pdf.py +11 -4
  123. endoreg_db/models/media/pdf/report_file.py +6 -2
  124. endoreg_db/models/media/pdf/report_reader/__init__.py +3 -3
  125. endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +15 -5
  126. endoreg_db/models/media/video/create_from_file.py +20 -41
  127. endoreg_db/models/media/video/pipe_1.py +75 -30
  128. endoreg_db/models/media/video/pipe_2.py +37 -12
  129. endoreg_db/models/media/video/video_file.py +36 -24
  130. endoreg_db/models/media/video/video_file_ai.py +235 -70
  131. endoreg_db/models/media/video/video_file_anonymize.py +240 -65
  132. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -1
  133. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +3 -1
  134. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +30 -9
  135. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +95 -29
  136. endoreg_db/models/media/video/video_file_frames/_get_frame.py +13 -3
  137. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -1
  138. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +15 -3
  139. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +15 -3
  140. endoreg_db/models/media/video/video_file_frames/_get_frames.py +7 -2
  141. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +109 -23
  142. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +111 -27
  143. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +46 -13
  144. endoreg_db/models/media/video/video_file_io.py +85 -33
  145. endoreg_db/models/media/video/video_file_meta/__init__.py +6 -6
  146. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +17 -4
  147. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +28 -7
  148. endoreg_db/models/media/video/video_file_meta/get_fps.py +46 -13
  149. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +81 -20
  150. endoreg_db/models/media/video/video_file_meta/text_meta.py +61 -20
  151. endoreg_db/models/media/video/video_file_meta/video_meta.py +40 -12
  152. endoreg_db/models/media/video/video_file_segments.py +118 -27
  153. endoreg_db/models/media/video/video_metadata.py +25 -6
  154. endoreg_db/models/media/video/video_processing.py +54 -15
  155. endoreg_db/models/medical/__init__.py +3 -13
  156. endoreg_db/models/medical/contraindication/__init__.py +3 -1
  157. endoreg_db/models/medical/disease.py +18 -6
  158. endoreg_db/models/medical/event.py +6 -2
  159. endoreg_db/models/medical/examination/__init__.py +5 -1
  160. endoreg_db/models/medical/examination/examination.py +22 -6
  161. endoreg_db/models/medical/examination/examination_indication.py +23 -7
  162. endoreg_db/models/medical/examination/examination_time.py +6 -2
  163. endoreg_db/models/medical/finding/__init__.py +3 -1
  164. endoreg_db/models/medical/finding/finding.py +37 -12
  165. endoreg_db/models/medical/finding/finding_classification.py +27 -8
  166. endoreg_db/models/medical/finding/finding_intervention.py +19 -6
  167. endoreg_db/models/medical/finding/finding_type.py +3 -1
  168. endoreg_db/models/medical/hardware/__init__.py +1 -1
  169. endoreg_db/models/medical/hardware/endoscope.py +14 -2
  170. endoreg_db/models/medical/laboratory/__init__.py +1 -1
  171. endoreg_db/models/medical/laboratory/lab_value.py +139 -39
  172. endoreg_db/models/medical/medication/__init__.py +7 -3
  173. endoreg_db/models/medical/medication/medication.py +3 -1
  174. endoreg_db/models/medical/medication/medication_indication.py +3 -1
  175. endoreg_db/models/medical/medication/medication_indication_type.py +11 -3
  176. endoreg_db/models/medical/medication/medication_intake_time.py +3 -1
  177. endoreg_db/models/medical/medication/medication_schedule.py +3 -1
  178. endoreg_db/models/medical/patient/__init__.py +2 -10
  179. endoreg_db/models/medical/patient/medication_examples.py +3 -14
  180. endoreg_db/models/medical/patient/patient_disease.py +17 -5
  181. endoreg_db/models/medical/patient/patient_event.py +12 -4
  182. endoreg_db/models/medical/patient/patient_examination.py +52 -15
  183. endoreg_db/models/medical/patient/patient_examination_indication.py +15 -4
  184. endoreg_db/models/medical/patient/patient_finding.py +105 -29
  185. endoreg_db/models/medical/patient/patient_finding_classification.py +41 -12
  186. endoreg_db/models/medical/patient/patient_finding_intervention.py +11 -3
  187. endoreg_db/models/medical/patient/patient_lab_sample.py +6 -2
  188. endoreg_db/models/medical/patient/patient_lab_value.py +42 -10
  189. endoreg_db/models/medical/patient/patient_medication.py +25 -7
  190. endoreg_db/models/medical/patient/patient_medication_schedule.py +34 -10
  191. endoreg_db/models/metadata/model_meta.py +40 -12
  192. endoreg_db/models/metadata/model_meta_logic.py +51 -16
  193. endoreg_db/models/metadata/sensitive_meta.py +65 -28
  194. endoreg_db/models/metadata/sensitive_meta_logic.py +28 -26
  195. endoreg_db/models/metadata/video_meta.py +146 -39
  196. endoreg_db/models/metadata/video_prediction_logic.py +70 -21
  197. endoreg_db/models/metadata/video_prediction_meta.py +80 -27
  198. endoreg_db/models/operation_log.py +63 -0
  199. endoreg_db/models/other/__init__.py +10 -10
  200. endoreg_db/models/other/distribution/__init__.py +9 -7
  201. endoreg_db/models/other/distribution/base_value_distribution.py +3 -1
  202. endoreg_db/models/other/distribution/date_value_distribution.py +19 -5
  203. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +3 -1
  204. endoreg_db/models/other/distribution/numeric_value_distribution.py +34 -9
  205. endoreg_db/models/other/emission/__init__.py +1 -1
  206. endoreg_db/models/other/emission/emission_factor.py +9 -3
  207. endoreg_db/models/other/information_source.py +15 -5
  208. endoreg_db/models/other/material.py +3 -1
  209. endoreg_db/models/other/transport_route.py +3 -1
  210. endoreg_db/models/other/unit.py +6 -2
  211. endoreg_db/models/report/report.py +0 -1
  212. endoreg_db/models/requirement/requirement.py +84 -27
  213. endoreg_db/models/requirement/requirement_error.py +5 -6
  214. endoreg_db/models/requirement/requirement_evaluation/__init__.py +1 -1
  215. endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +8 -8
  216. endoreg_db/models/requirement/requirement_evaluation/get_values.py +3 -3
  217. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +24 -8
  218. endoreg_db/models/requirement/requirement_operator.py +28 -8
  219. endoreg_db/models/requirement/requirement_set.py +34 -11
  220. endoreg_db/models/state/__init__.py +1 -0
  221. endoreg_db/models/state/audit_ledger.py +9 -2
  222. endoreg_db/models/{media → state}/processing_history/__init__.py +1 -3
  223. endoreg_db/models/state/processing_history/processing_history.py +136 -0
  224. endoreg_db/models/state/raw_pdf.py +0 -1
  225. endoreg_db/models/state/video.py +2 -3
  226. endoreg_db/models/utils.py +4 -2
  227. endoreg_db/queries/__init__.py +2 -6
  228. endoreg_db/queries/annotations/__init__.py +1 -3
  229. endoreg_db/queries/annotations/legacy.py +37 -26
  230. endoreg_db/root_urls.py +3 -4
  231. endoreg_db/schemas/examination_evaluation.py +3 -0
  232. endoreg_db/serializers/Frames_NICE_and_PARIS_classifications.py +249 -163
  233. endoreg_db/serializers/__init__.py +2 -8
  234. endoreg_db/serializers/administration/__init__.py +1 -2
  235. endoreg_db/serializers/administration/ai/__init__.py +0 -1
  236. endoreg_db/serializers/administration/ai/active_model.py +3 -1
  237. endoreg_db/serializers/administration/ai/ai_model.py +5 -3
  238. endoreg_db/serializers/administration/ai/model_type.py +3 -1
  239. endoreg_db/serializers/administration/center.py +7 -2
  240. endoreg_db/serializers/administration/gender.py +4 -2
  241. endoreg_db/serializers/anonymization.py +13 -13
  242. endoreg_db/serializers/evaluation/examination_evaluation.py +0 -1
  243. endoreg_db/serializers/examination/__init__.py +1 -1
  244. endoreg_db/serializers/examination/base.py +12 -13
  245. endoreg_db/serializers/examination/dropdown.py +6 -7
  246. endoreg_db/serializers/examination_serializer.py +3 -6
  247. endoreg_db/serializers/finding/__init__.py +1 -1
  248. endoreg_db/serializers/finding/finding.py +14 -7
  249. endoreg_db/serializers/finding_classification/__init__.py +3 -3
  250. endoreg_db/serializers/finding_classification/choice.py +3 -3
  251. endoreg_db/serializers/finding_classification/classification.py +2 -4
  252. endoreg_db/serializers/label_video_segment/__init__.py +5 -3
  253. endoreg_db/serializers/{label → label_video_segment}/image_classification_annotation.py +5 -5
  254. endoreg_db/serializers/label_video_segment/label/__init__.py +6 -0
  255. endoreg_db/serializers/{label → label_video_segment/label}/label.py +1 -1
  256. endoreg_db/serializers/label_video_segment/label_video_segment.py +338 -228
  257. endoreg_db/serializers/meta/__init__.py +1 -2
  258. endoreg_db/serializers/meta/sensitive_meta_detail.py +28 -13
  259. endoreg_db/serializers/meta/sensitive_meta_update.py +51 -46
  260. endoreg_db/serializers/meta/sensitive_meta_verification.py +19 -16
  261. endoreg_db/serializers/misc/__init__.py +2 -2
  262. endoreg_db/serializers/misc/file_overview.py +11 -7
  263. endoreg_db/serializers/misc/stats.py +10 -8
  264. endoreg_db/serializers/misc/translatable_field_mix_in.py +6 -6
  265. endoreg_db/serializers/misc/upload_job.py +32 -29
  266. endoreg_db/serializers/patient/__init__.py +2 -1
  267. endoreg_db/serializers/patient/patient.py +32 -15
  268. endoreg_db/serializers/patient/patient_dropdown.py +11 -3
  269. endoreg_db/serializers/patient_examination/__init__.py +1 -1
  270. endoreg_db/serializers/patient_examination/patient_examination.py +67 -40
  271. endoreg_db/serializers/patient_finding/__init__.py +1 -1
  272. endoreg_db/serializers/patient_finding/patient_finding.py +2 -1
  273. endoreg_db/serializers/patient_finding/patient_finding_classification.py +17 -9
  274. endoreg_db/serializers/patient_finding/patient_finding_detail.py +26 -17
  275. endoreg_db/serializers/patient_finding/patient_finding_intervention.py +7 -5
  276. endoreg_db/serializers/patient_finding/patient_finding_list.py +10 -11
  277. endoreg_db/serializers/patient_finding/patient_finding_write.py +36 -27
  278. endoreg_db/serializers/pdf/__init__.py +1 -3
  279. endoreg_db/serializers/requirements/requirement_schema.py +1 -6
  280. endoreg_db/serializers/sensitive_meta_serializer.py +100 -81
  281. endoreg_db/serializers/video/__init__.py +2 -2
  282. endoreg_db/serializers/video/{segmentation.py → video_file.py} +66 -47
  283. endoreg_db/serializers/video/video_file_brief.py +6 -2
  284. endoreg_db/serializers/video/video_file_detail.py +36 -23
  285. endoreg_db/serializers/video/video_file_list.py +4 -2
  286. endoreg_db/serializers/video/video_processing_history.py +54 -50
  287. endoreg_db/services/__init__.py +1 -1
  288. endoreg_db/services/anonymization.py +2 -2
  289. endoreg_db/services/examination_evaluation.py +40 -17
  290. endoreg_db/services/model_meta_from_hf.py +76 -0
  291. endoreg_db/services/polling_coordinator.py +101 -70
  292. endoreg_db/services/pseudonym_service.py +27 -22
  293. endoreg_db/services/report_import.py +6 -3
  294. endoreg_db/services/segment_sync.py +75 -59
  295. endoreg_db/services/video_import.py +6 -7
  296. endoreg_db/urls/__init__.py +2 -2
  297. endoreg_db/urls/ai.py +7 -25
  298. endoreg_db/urls/anonymization.py +61 -15
  299. endoreg_db/urls/auth.py +4 -4
  300. endoreg_db/urls/classification.py +4 -9
  301. endoreg_db/urls/examination.py +27 -18
  302. endoreg_db/urls/media.py +27 -34
  303. endoreg_db/urls/patient.py +11 -7
  304. endoreg_db/urls/requirements.py +3 -1
  305. endoreg_db/urls/root_urls.py +2 -3
  306. endoreg_db/urls/stats.py +24 -16
  307. endoreg_db/urls/upload.py +3 -11
  308. endoreg_db/utils/__init__.py +14 -15
  309. endoreg_db/utils/ai/__init__.py +1 -1
  310. endoreg_db/utils/ai/data_loader_for_model_input.py +262 -0
  311. endoreg_db/utils/ai/data_loader_for_model_training.py +262 -0
  312. endoreg_db/utils/ai/get.py +2 -1
  313. endoreg_db/utils/ai/inference_dataset.py +14 -15
  314. endoreg_db/utils/ai/model_training/config.py +117 -0
  315. endoreg_db/utils/ai/model_training/dataset.py +74 -0
  316. endoreg_db/utils/ai/model_training/losses.py +68 -0
  317. endoreg_db/utils/ai/model_training/metrics.py +78 -0
  318. endoreg_db/utils/ai/model_training/model_backbones.py +155 -0
  319. endoreg_db/utils/ai/model_training/model_gastronet_resnet.py +118 -0
  320. endoreg_db/utils/ai/model_training/trainer_gastronet_multilabel.py +771 -0
  321. endoreg_db/utils/ai/multilabel_classification_net.py +21 -6
  322. endoreg_db/utils/ai/predict.py +4 -4
  323. endoreg_db/utils/ai/preprocess.py +19 -11
  324. endoreg_db/utils/calc_duration_seconds.py +4 -4
  325. endoreg_db/utils/case_generator/lab_sample_factory.py +3 -4
  326. endoreg_db/utils/check_video_files.py +74 -47
  327. endoreg_db/utils/cropping.py +10 -9
  328. endoreg_db/utils/dataloader.py +11 -3
  329. endoreg_db/utils/dates.py +3 -4
  330. endoreg_db/utils/defaults/set_default_center.py +7 -6
  331. endoreg_db/utils/env.py +6 -2
  332. endoreg_db/utils/extract_specific_frames.py +24 -9
  333. endoreg_db/utils/file_operations.py +30 -18
  334. endoreg_db/utils/fix_video_path_direct.py +57 -41
  335. endoreg_db/utils/frame_anonymization_utils.py +157 -157
  336. endoreg_db/utils/hashs.py +3 -18
  337. endoreg_db/utils/links/requirement_link.py +96 -52
  338. endoreg_db/utils/ocr.py +30 -25
  339. endoreg_db/utils/operation_log.py +61 -0
  340. endoreg_db/utils/parse_and_generate_yaml.py +12 -13
  341. endoreg_db/utils/paths.py +6 -6
  342. endoreg_db/utils/permissions.py +40 -24
  343. endoreg_db/utils/pipelines/process_video_dir.py +50 -26
  344. endoreg_db/utils/product/sum_emissions.py +5 -3
  345. endoreg_db/utils/product/sum_weights.py +4 -2
  346. endoreg_db/utils/pydantic_models/__init__.py +3 -4
  347. endoreg_db/utils/requirement_operator_logic/_old/lab_value_operators.py +207 -107
  348. endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py +252 -65
  349. endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +27 -10
  350. endoreg_db/utils/setup_config.py +21 -5
  351. endoreg_db/utils/storage.py +3 -1
  352. endoreg_db/utils/translation.py +19 -15
  353. endoreg_db/utils/uuid.py +1 -0
  354. endoreg_db/utils/validate_endo_roi.py +12 -4
  355. endoreg_db/utils/validate_subcategory_dict.py +26 -24
  356. endoreg_db/utils/validate_video_detailed.py +207 -149
  357. endoreg_db/utils/video/__init__.py +7 -3
  358. endoreg_db/utils/video/extract_frames.py +30 -18
  359. endoreg_db/utils/video/ffmpeg_wrapper.py +217 -52
  360. endoreg_db/utils/video/names.py +11 -6
  361. endoreg_db/utils/video/streaming_processor.py +175 -101
  362. endoreg_db/utils/video/video_splitter.py +30 -19
  363. endoreg_db/views/Frames_NICE_and_PARIS_classifications_views.py +59 -50
  364. endoreg_db/views/__init__.py +0 -20
  365. endoreg_db/views/anonymization/__init__.py +6 -2
  366. endoreg_db/views/anonymization/media_management.py +2 -6
  367. endoreg_db/views/anonymization/overview.py +34 -1
  368. endoreg_db/views/anonymization/validate.py +79 -18
  369. endoreg_db/views/auth/__init__.py +1 -1
  370. endoreg_db/views/auth/keycloak.py +16 -14
  371. endoreg_db/views/examination/__init__.py +12 -15
  372. endoreg_db/views/examination/examination.py +5 -5
  373. endoreg_db/views/examination/examination_manifest_cache.py +5 -5
  374. endoreg_db/views/examination/get_finding_classification_choices.py +8 -5
  375. endoreg_db/views/examination/get_finding_classifications.py +9 -7
  376. endoreg_db/views/examination/get_findings.py +8 -10
  377. endoreg_db/views/examination/get_instruments.py +3 -2
  378. endoreg_db/views/examination/get_interventions.py +1 -1
  379. endoreg_db/views/finding/__init__.py +2 -2
  380. endoreg_db/views/finding/finding.py +58 -54
  381. endoreg_db/views/finding/get_classifications.py +1 -1
  382. endoreg_db/views/finding/get_interventions.py +1 -1
  383. endoreg_db/views/finding_classification/__init__.py +5 -5
  384. endoreg_db/views/finding_classification/finding_classification.py +5 -6
  385. endoreg_db/views/finding_classification/get_classification_choices.py +3 -4
  386. endoreg_db/views/media/__init__.py +13 -13
  387. endoreg_db/views/media/pdf_media.py +9 -9
  388. endoreg_db/views/media/sensitive_metadata.py +10 -7
  389. endoreg_db/views/media/video_media.py +4 -4
  390. endoreg_db/views/meta/__init__.py +1 -1
  391. endoreg_db/views/meta/sensitive_meta_list.py +20 -22
  392. endoreg_db/views/meta/sensitive_meta_verification.py +14 -11
  393. endoreg_db/views/misc/__init__.py +6 -34
  394. endoreg_db/views/misc/center.py +2 -1
  395. endoreg_db/views/misc/csrf.py +2 -1
  396. endoreg_db/views/misc/gender.py +2 -1
  397. endoreg_db/views/misc/stats.py +141 -106
  398. endoreg_db/views/patient/__init__.py +1 -3
  399. endoreg_db/views/patient/patient.py +141 -99
  400. endoreg_db/views/patient_examination/__init__.py +5 -5
  401. endoreg_db/views/patient_examination/patient_examination.py +43 -42
  402. endoreg_db/views/patient_examination/patient_examination_create.py +10 -15
  403. endoreg_db/views/patient_examination/patient_examination_detail.py +12 -15
  404. endoreg_db/views/patient_examination/patient_examination_list.py +21 -17
  405. endoreg_db/views/patient_examination/video.py +114 -80
  406. endoreg_db/views/patient_finding/__init__.py +1 -1
  407. endoreg_db/views/patient_finding/patient_finding.py +17 -10
  408. endoreg_db/views/patient_finding/patient_finding_optimized.py +127 -95
  409. endoreg_db/views/patient_finding_classification/__init__.py +1 -1
  410. endoreg_db/views/patient_finding_classification/pfc_create.py +35 -27
  411. endoreg_db/views/report/reimport.py +1 -1
  412. endoreg_db/views/report/report_stream.py +5 -8
  413. endoreg_db/views/requirement/__init__.py +2 -1
  414. endoreg_db/views/requirement/evaluate.py +7 -9
  415. endoreg_db/views/requirement/lookup.py +2 -3
  416. endoreg_db/views/requirement/lookup_store.py +0 -1
  417. endoreg_db/views/requirement/requirement_utils.py +2 -4
  418. endoreg_db/views/stats/__init__.py +4 -4
  419. endoreg_db/views/stats/stats_views.py +152 -115
  420. endoreg_db/views/video/__init__.py +18 -27
  421. endoreg_db/views/{ai → video/ai}/__init__.py +2 -2
  422. endoreg_db/views/{ai → video/ai}/label.py +20 -16
  423. endoreg_db/views/video/correction.py +5 -6
  424. endoreg_db/views/video/reimport.py +134 -99
  425. endoreg_db/views/video/segments_crud.py +134 -44
  426. endoreg_db/views/video/video_apply_mask.py +13 -12
  427. endoreg_db/views/video/video_correction.py +2 -1
  428. endoreg_db/views/video/video_download_processed.py +15 -15
  429. endoreg_db/views/video/video_meta_stats.py +7 -6
  430. endoreg_db/views/video/video_processing_history.py +3 -2
  431. endoreg_db/views/video/video_remove_frames.py +13 -12
  432. endoreg_db/views/video/video_stream.py +110 -82
  433. {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.10.dist-info}/METADATA +9 -3
  434. {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.10.dist-info}/RECORD +436 -433
  435. endoreg_db/import_files/processing/video_processing/video_cleanup_on_error.py +0 -119
  436. endoreg_db/management/commands/import_fallback_video.py +0 -203
  437. endoreg_db/management/commands/import_video.py +0 -422
  438. endoreg_db/management/commands/import_video_with_classification.py +0 -367
  439. endoreg_db/models/media/processing_history/processing_history.py +0 -96
  440. endoreg_db/serializers/label/__init__.py +0 -7
  441. endoreg_db/serializers/label_video_segment/_lvs_create.py +0 -149
  442. endoreg_db/serializers/label_video_segment/_lvs_update.py +0 -138
  443. endoreg_db/serializers/label_video_segment/_lvs_validate.py +0 -149
  444. endoreg_db/serializers/label_video_segment/label_video_segment_annotation.py +0 -99
  445. endoreg_db/serializers/label_video_segment/label_video_segment_update.py +0 -163
  446. endoreg_db/services/__old/pdf_import.py +0 -1487
  447. endoreg_db/services/__old/video_import.py +0 -1306
  448. endoreg_db/tasks/upload_tasks.py +0 -216
  449. endoreg_db/tasks/video_ingest.py +0 -161
  450. endoreg_db/tasks/video_processing_tasks.py +0 -327
  451. endoreg_db/views/misc/translation.py +0 -182
  452. {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.10.dist-info}/WHEEL +0 -0
  453. {endoreg_db-0.8.8.9.dist-info → endoreg_db-0.8.9.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,422 +0,0 @@
1
- # See Pipe 1 video file function.
2
- #
3
-
4
- """
5
- Management command to import a video file into the database.
6
- This command is designed to be run from the command line and takes various arguments
7
- to specify the video file, center name, and other options.
8
- """
9
-
10
- from django.core.management import BaseCommand
11
- from django.core.files.base import ContentFile
12
- from django.db import connection
13
- from pathlib import Path
14
- from endoreg_db.models import VideoFile
15
- from endoreg_db.models.administration.center import Center
16
- from endoreg_db.models.medical.hardware import EndoscopyProcessor
17
- # #FIXME
18
- # from endoreg_db.management.commands import validate_video
19
-
20
- from endoreg_db.utils.video.ffmpeg_wrapper import check_ffmpeg_availability # ADDED
21
-
22
- import logging
23
- logger = logging.getLogger(__name__)
24
-
25
- # Import frame cleaning functionality - simplified approach
26
- FRAME_CLEANING_AVAILABLE = False
27
-
28
- # Try to import lx_anonymizer using the existing working import method from create_from_file
29
- try:
30
- # Check if we can find the lx-anonymizer directory
31
- current_file = Path(__file__)
32
- endoreg_db_root = current_file.parent.parent.parent.parent
33
- lx_anonymizer_path = endoreg_db_root / "lx-anonymizer"
34
-
35
- if lx_anonymizer_path.exists():
36
- # Add to Python path temporarily
37
- import sys
38
- if str(lx_anonymizer_path) not in sys.path:
39
- sys.path.insert(0, str(lx_anonymizer_path))
40
-
41
- # Try simple import
42
- from lx_anonymizer import FrameCleaner, ReportReader
43
-
44
- FRAME_CLEANING_AVAILABLE = True
45
- logger.debug("Successfully imported lx_anonymizer modules")
46
-
47
- # Remove from path to avoid conflicts
48
- if str(lx_anonymizer_path) in sys.path:
49
- sys.path.remove(str(lx_anonymizer_path))
50
-
51
- except Exception as e:
52
- logger.debug(f"Frame cleaning not available: {e}")
53
- FRAME_CLEANING_AVAILABLE = False
54
-
55
- IMPORT_MODELS = [
56
- VideoFile.__name__,
57
- ]
58
-
59
- IMPORT_METADATA = {
60
- VideoFile.__name__: {
61
- "uuid": VideoFile.uuid,
62
- "raw_file": VideoFile.raw_file,
63
- "processed_file": VideoFile.processed_file,
64
- "foreign_keys": [],
65
- "foreign_key_models": [],
66
- },
67
- }
68
-
69
- class Command(BaseCommand):
70
- help = """
71
- Creates a new VideoFile object in the database.
72
- 1. Validates the existence of the specified center and processor
73
- 2. Checks that the video file is saved and anonymized
74
- 3. Creates or updates a ModelMeta entry with the specified parameters
75
- """
76
-
77
- def add_arguments(self, parser):
78
-
79
- """
80
- Adds command-line arguments for the video import management command.
81
-
82
- Defines options for specifying the video file path, associated center and processor names, directory roots for frames and videos, deletion and saving behavior, model path, segmentation usage, and verbosity.
83
- """
84
- parser.add_argument(
85
- "--verbose",
86
- action="store_true",
87
- help="Display verbose output",
88
- )
89
- parser.add_argument(
90
- "--",
91
- type=str,
92
- default="university_hospital_wuerzburg",
93
- help="Name of the center to associate with video",
94
- )
95
-
96
- # add the path to the video file
97
- parser.add_argument(
98
- "video_file",
99
- type=Path,
100
- help="Path to the video file to import",
101
- )
102
-
103
- # frame dir parent
104
- parser.add_argument(
105
- "--frame_dir_root",
106
- type=str,
107
- default="~/test-data/raw_frame_dir",
108
- help="Path to the frame directory",
109
- )
110
-
111
- # video dir
112
- parser.add_argument(
113
- "--video_dir_root",
114
- type=str,
115
- default="~/test-data/raw_video_dir",
116
- help="Path to the video directory",
117
- )
118
-
119
- # delete source
120
- parser.add_argument(
121
- "--delete_source",
122
- action="store_true",
123
- default=False,
124
- help="Delete the source video file after importing",
125
- )
126
-
127
- # save video file
128
- parser.add_argument(
129
- "--save_video_file",
130
- action="store_true",
131
- default=True,
132
- help="Save the video file to the video directory",
133
- )
134
-
135
- # model_path
136
- parser.add_argument(
137
- "--model_name",
138
- type=str,
139
- default="image_multilabel_classification_colonoscopy_default",
140
- help="AiModel Name",
141
- )
142
-
143
- #
144
- parser.add_argument(
145
- "--segmentation",
146
- action="store_true",
147
- help="Whether to use segmentation",
148
- )
149
-
150
- parser.add_argument(
151
- "--processor_name",
152
- type=str,
153
- default="olympus_cv_1500",
154
- help="Name of the processor to associate with video",
155
- )
156
-
157
- def handle(self, *args, **options):
158
-
159
- """
160
- Imports a video file into the database, associating it with a specified medical center and endoscopy processor, and optionally applies AI-based segmentation.
161
-
162
- Checks for required dependencies, loads reference data, validates the existence of the specified center and processor, and processes the video file. If segmentation is enabled, retrieves the latest segmentation model metadata. Handles interactive processor selection if multiple are available, creates a new `VideoFile` entry, and invokes the processing pipeline. Can optionally delete the source file or save the video to a specified directory. Reports the outcome of the import and processing steps.
163
- """
164
- try: # ADDED
165
- check_ffmpeg_availability() # ADDED
166
- self.stdout.write(self.style.SUCCESS("FFMPEG is available")) # ADDED
167
- except FileNotFoundError as e: # ADDED
168
- self.stderr.write(self.style.ERROR(str(e))) # ADDED
169
- # Decide if the command should exit or if FFMPEG is optional for some operations
170
- # For this command, it seems FFMPEG is critical for VideoFile.pipe_1 and VideoFile.create_from_file
171
- return # ADDED
172
-
173
- self.stdout.write(f"Current database: {connection.alias}")
174
- self.stdout.write(self.style.SUCCESS("Starting video import..."))
175
-
176
- # Should not be invoked here but in a previous db setup step
177
- # load_gender_data()
178
- # load_disease_data()
179
- # load_event_data()
180
- # load_information_source()
181
- # load_examination_data()
182
- # load_center_data()
183
- # load_endoscope_data()
184
-
185
- segmentation = options["segmentation"]
186
-
187
- verbose = options["verbose"]
188
- center_name = options["center_name"]
189
- video_file = options["video_file"]
190
- frame_dir_root = options["frame_dir_root"]
191
- delete_source = options["delete_source"]
192
- save_video_file = options["save_video_file"]
193
- model_name = options["model_name"]
194
- processor_name = options["processor_name"]
195
- video_file = Path(video_file).expanduser()
196
-
197
-
198
- assert isinstance(delete_source, bool), "delete_source must be a boolean"
199
- assert isinstance(save_video_file, bool), "save_video_file must be a boolean"
200
- assert isinstance(verbose, bool), "verbose must be a boolean"
201
- assert isinstance(center_name, str), "center_name must be a string"
202
- assert isinstance(video_file, Path), "video_file must be a Path"
203
- assert isinstance(frame_dir_root, str), "frame_dir_root must be a string"
204
- # Assert Center exists -> Does not exist methods are deprecated
205
- try:
206
- center = Center.objects.get(name=center_name)
207
- self.stdout.write(self.style.SUCCESS(f"Using center: {center.name}"))
208
- except Center.DoesNotExist:
209
- self.stdout.write(self.style.ERROR(f"Center not found: {center_name}"))
210
- return
211
-
212
- # Assert Processor Exists
213
- if processor_name is None:
214
- processors_qs = center.endoscopy_processors.all()
215
- proc_count = processors_qs.count()
216
- if proc_count == 0:
217
- raise AssertionError(
218
- f"No processors linked to '{center.name}' and no processor called '{processor_name}' exists. "
219
- "Fallback from default Processor applied."
220
- )
221
- elif proc_count == 1:
222
- processor = processors_qs.first()
223
- else:
224
-
225
- processor = self._choose_processor_interactively(processors_qs)
226
- self.stdout.write(self.style.SUCCESS(f"Using processor: {processor.name}"))
227
-
228
- else:
229
- processor = EndoscopyProcessor.objects.get(name=processor_name)
230
-
231
- cns = processor.centers.values_list("name", flat=True)
232
- if center_name not in cns:
233
- self.stdout.write(
234
- self.style.ERROR(
235
- f"Processor '{processor_name}' is not linked to center '{center_name}'."
236
- )
237
- )
238
- return
239
-
240
- if not Path(video_file).exists():
241
- self.stdout.write(self.style.ERROR(f"Video file not found: {video_file} saving unsuccessful."))
242
- return AssertionError(f"Video file not found: {video_file}")
243
-
244
- # Create VideoFile instance first
245
- video_file_obj = VideoFile.create_from_file_initialized(
246
- file_path=video_file,
247
- center_name=center_name,
248
- processor_name=processor_name,
249
- delete_source=delete_source,
250
- save_video_file=save_video_file, # Add this line
251
- )
252
-
253
- if not video_file_obj:
254
- self.stdout.write(self.style.ERROR("Failed to create VideoFile instance"))
255
- return
256
-
257
- self.stdout.write(self.style.SUCCESS(f"Created VideoFile with UUID: {video_file_obj.uuid}"))
258
-
259
- # Initialize video specs and frames
260
- video_file_obj.initialize_video_specs()
261
- video_file_obj.initialize_frames()
262
-
263
- # Run Pipe 1 for OCR and AI processing
264
- self.stdout.write(self.style.SUCCESS("Starting Pipe 1 processing (OCR + AI)..."))
265
- success = video_file_obj.pipe_1(
266
- model_name=model_name,
267
- delete_frames_after=True,
268
- ocr_frame_fraction=0.01,
269
- ocr_cap=5
270
- )
271
-
272
- if not success:
273
- self.stdout.write(self.style.ERROR("Pipe 1 processing failed"))
274
- return
275
-
276
- self.stdout.write(self.style.SUCCESS("Pipe 1 processing completed"))
277
-
278
- # Ensure minimum patient data is available
279
- self.stdout.write(self.style.SUCCESS("Ensuring minimum patient data..."))
280
- self._ensure_default_patient_data(video_file_obj)
281
-
282
- # Frame-level anonymization integration
283
- if FRAME_CLEANING_AVAILABLE and video_file_obj.raw_file:
284
- try:
285
- self.stdout.write(self.style.SUCCESS("Starting frame-level anonymization..."))
286
- # Properly instantiate FrameCleaner and ReportReader with correct arguments
287
- frame_cleaner = FrameCleaner()
288
- report_reader = ReportReader(
289
- report_root_path=str(video_file_obj.raw_file.path),
290
- locale="de_DE", # Default German locale for medical data
291
- text_date_format="%d.%m.%Y" # Common German date format
292
- )
293
-
294
- # Updated to handle new return signature (path, metadata)
295
- cleaned_video_path, extracted_metadata = frame_cleaner.clean_video(
296
- video_path=Path(video_file_obj.raw_file.path),
297
- endoscope_image_roi=video_file_obj.processor.get_roi_endoscope_image() if video_file_obj.processor else None,
298
- endoscope_data_roi_nested=video_file_obj.processor.get_rois() if video_file_obj.processor else None,
299
- output_path=video_file_obj.get_processed_file_path(),
300
- technique="mask_overlay" # Use mask overlay technique as default, if not set this will be inferred.
301
- )
302
-
303
- # Save the cleaned video using Django's FileField
304
- with open(cleaned_video_path, 'rb') as f:
305
- video_file_obj.raw_file.save(cleaned_video_path.name, ContentFile(f.read()))
306
- video_file_obj.save()
307
-
308
- self.stdout.write(self.style.SUCCESS(f"Frame cleaning completed: {cleaned_video_path.name}"))
309
- self.stdout.write(self.style.SUCCESS(f"Extracted metadata: {extracted_metadata}"))
310
-
311
- except Exception as e:
312
- self.stdout.write(self.style.WARNING(f"Frame cleaning failed, continuing with original video: {e}"))
313
- elif not FRAME_CLEANING_AVAILABLE:
314
- self.stdout.write(self.style.WARNING("Frame cleaning not available (lx_anonymizer not found)"))
315
-
316
- # Now call pipe_1 on the VideoFile instance
317
- if segmentation:
318
- success = video_file_obj.pipe_1(model_name=model_name, model_meta_version = None)
319
-
320
- if success:
321
- self.stdout.write(self.style.SUCCESS("Pipeline 1 completed successfully"))
322
- else:
323
- self.stdout.write(self.style.ERROR("Pipeline 1 failed"))
324
-
325
- def _ensure_default_patient_data(self, video_file):
326
- """
327
- Ensure video has minimum required patient data in SensitiveMeta.
328
- Creates default values if data is missing after OCR processing.
329
- """
330
- from endoreg_db.models import SensitiveMeta
331
- from datetime import date
332
-
333
- if not video_file.sensitive_meta:
334
- self.stdout.write(self.style.WARNING(f"No SensitiveMeta found for video {video_file.uuid}, creating default"))
335
-
336
- # Create default SensitiveMeta with placeholder data
337
- default_data = {
338
- "patient_first_name": None,
339
- "patient_last_name": None,
340
- "patient_dob": None,
341
- "examination_date": None,
342
- "center_name": video_file.center.name if video_file.center else "unknown",
343
- }
344
-
345
- try:
346
- sensitive_meta = SensitiveMeta.create_from_dict(default_data)
347
- video_file.sensitive_meta = sensitive_meta
348
- video_file.save(update_fields=['sensitive_meta'])
349
- self.stdout.write(self.style.SUCCESS(f"Created default SensitiveMeta for video {video_file.uuid}"))
350
- except Exception as e:
351
- self.stdout.write(self.style.ERROR(f"Failed to create default SensitiveMeta for video {video_file.uuid}: {e}"))
352
-
353
- else:
354
- # Update existing SensitiveMeta with missing fields
355
- update_needed = False
356
- update_data = {}
357
-
358
- if not video_file.sensitive_meta.patient_first_name:
359
- update_data["patient_first_name"] = "Patient"
360
- update_needed = True
361
-
362
- if not video_file.sensitive_meta.patient_last_name:
363
- update_data["patient_last_name"] = "Unknown"
364
- update_needed = True
365
-
366
- if not video_file.sensitive_meta.patient_dob:
367
- update_data["patient_dob"] = date(1990, 1, 1)
368
- update_needed = True
369
-
370
- if not video_file.sensitive_meta.examination_date:
371
- update_data["examination_date"] = date.today()
372
- update_needed = True
373
-
374
- if update_needed:
375
- try:
376
- video_file.sensitive_meta.update_from_dict(update_data)
377
- self.stdout.write(self.style.SUCCESS(f"Updated missing SensitiveMeta fields for video {video_file.uuid}: {list(update_data.keys())}"))
378
- except Exception as e:
379
- self.stdout.write(self.style.ERROR(f"Failed to update SensitiveMeta for video {video_file.uuid}: {e}"))
380
- else:
381
- self.stdout.write(self.style.SUCCESS(f"SensitiveMeta for video {video_file.uuid} already has all required fields"))
382
-
383
- def _choose_processor_interactively(
384
- self, processors_qs
385
- ) -> EndoscopyProcessor:
386
- """
387
- Interactively prompts the user to select an endoscopy processor from a list.
388
-
389
- Displays all available processors and requests user input until a valid selection is made. Aborts the process if the user interrupts input.
390
-
391
- Args:
392
- processors_qs: Queryset of EndoscopyProcessor objects to present for selection.
393
-
394
- Returns:
395
- The EndoscopyProcessor object chosen by the user.
396
- """
397
- # turn the QS into a concrete list so we can index it later
398
- processors = list(processors_qs) # -> [EndoscopyProcessor, …]
399
-
400
- self.stdout.write(self.style.ERROR(
401
- f"\nThe centre has {len(processors)} endoscopy processors.\n"
402
- "Choose one for this import:\n"
403
- ))
404
- for idx, proc in enumerate(processors, 1):
405
- self.stdout.write(f" [{idx}] {proc.name}")
406
-
407
- while True: # keep prompting until valid
408
- try:
409
- choice = input("Processor number › ").strip()
410
- except (EOFError, KeyboardInterrupt):
411
- self.stderr.write("\nAborted.")
412
- raise SystemExit(1)
413
-
414
- try:
415
- index = int(choice) - 1
416
- if not 0 <= index < len(processors):
417
- raise ValueError
418
- except ValueError:
419
- self.stderr.write("❌ Please enter a number in the list above.\n")
420
- continue
421
-
422
- return processors[index] # ← the chosen object