endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.8.0__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 (372) hide show
  1. endoreg_db/authz/auth.py +74 -0
  2. endoreg_db/authz/backends.py +168 -0
  3. endoreg_db/authz/management/commands/list_routes.py +18 -0
  4. endoreg_db/authz/middleware.py +83 -0
  5. endoreg_db/authz/permissions.py +127 -0
  6. endoreg_db/authz/policy.py +218 -0
  7. endoreg_db/authz/views_auth.py +66 -0
  8. endoreg_db/config/env.py +13 -8
  9. endoreg_db/data/__init__.py +8 -31
  10. endoreg_db/data/_examples/disease.yaml +55 -0
  11. endoreg_db/data/_examples/disease_classification.yaml +13 -0
  12. endoreg_db/data/_examples/disease_classification_choice.yaml +62 -0
  13. endoreg_db/data/_examples/event.yaml +64 -0
  14. endoreg_db/data/_examples/examination.yaml +72 -0
  15. endoreg_db/data/_examples/finding/anatomy_colon.yaml +128 -0
  16. endoreg_db/data/_examples/finding/colonoscopy.yaml +40 -0
  17. endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +56 -0
  18. endoreg_db/data/_examples/finding/complication.yaml +16 -0
  19. endoreg_db/data/_examples/finding/data.yaml +105 -0
  20. endoreg_db/data/_examples/finding/examination_setting.yaml +16 -0
  21. endoreg_db/data/_examples/finding/medication_related.yaml +18 -0
  22. endoreg_db/data/_examples/finding/outcome.yaml +12 -0
  23. endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +68 -0
  24. endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +22 -0
  25. endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +25 -0
  26. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
  27. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
  28. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +68 -0
  29. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
  30. endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +80 -0
  31. endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +21 -0
  32. endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +20 -0
  33. endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +26 -0
  34. endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +22 -0
  35. endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +53 -0
  36. endoreg_db/data/_examples/finding_classification/complication_generic.yaml +25 -0
  37. endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +40 -0
  38. endoreg_db/data/_examples/finding_classification/histology_colo.yaml +51 -0
  39. endoreg_db/data/_examples/finding_classification/intervention_required.yaml +26 -0
  40. endoreg_db/data/_examples/finding_classification/medication_related.yaml +23 -0
  41. endoreg_db/data/_examples/finding_classification/visualized.yaml +33 -0
  42. endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +78 -0
  43. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
  44. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
  45. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
  46. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +15 -0
  47. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +17 -0
  48. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_paris.yaml +57 -0
  49. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
  50. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +14 -0
  51. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
  52. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_location.yaml +229 -0
  53. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
  54. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +82 -0
  55. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
  56. endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +15 -0
  57. endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
  58. endoreg_db/data/_examples/finding_classification_choice/histology.yaml +24 -0
  59. endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +20 -0
  60. endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +19 -0
  61. endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +11 -0
  62. endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +48 -0
  63. endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +43 -0
  64. endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
  65. endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +128 -0
  66. endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +32 -0
  67. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +9 -0
  68. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +36 -0
  69. endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +15 -0
  70. endoreg_db/data/_examples/finding_type/data.yaml +43 -0
  71. endoreg_db/data/_examples/requirement/age.yaml +26 -0
  72. endoreg_db/data/_examples/requirement/colonoscopy_baseline_austria.yaml +45 -0
  73. endoreg_db/data/_examples/requirement/disease_cardiovascular.yaml +79 -0
  74. endoreg_db/data/_examples/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
  75. endoreg_db/data/_examples/requirement/disease_hepatology.yaml +12 -0
  76. endoreg_db/data/_examples/requirement/disease_misc.yaml +12 -0
  77. endoreg_db/data/_examples/requirement/disease_renal.yaml +96 -0
  78. endoreg_db/data/_examples/requirement/endoscopy_bleeding_risk.yaml +59 -0
  79. endoreg_db/data/_examples/requirement/event_cardiology.yaml +251 -0
  80. endoreg_db/data/_examples/requirement/event_requirements.yaml +145 -0
  81. endoreg_db/data/_examples/requirement/finding_colon_polyp.yaml +50 -0
  82. endoreg_db/data/_examples/requirement/gender.yaml +25 -0
  83. endoreg_db/data/_examples/requirement/lab_value.yaml +441 -0
  84. endoreg_db/data/_examples/requirement/medication.yaml +93 -0
  85. endoreg_db/data/_examples/requirement_operator/age.yaml +13 -0
  86. endoreg_db/data/_examples/requirement_operator/lab_operators.yaml +129 -0
  87. endoreg_db/data/_examples/requirement_operator/model_operators.yaml +96 -0
  88. endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +48 -0
  89. endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +57 -0
  90. endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
  91. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +4 -3
  92. endoreg_db/data/event_classification/data.yaml +4 -0
  93. endoreg_db/data/event_classification_choice/data.yaml +9 -0
  94. endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +43 -70
  95. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +22 -52
  96. endoreg_db/data/finding_classification/colonoscopy_location.yaml +31 -62
  97. endoreg_db/data/finding_classification/histology_colo.yaml +28 -36
  98. endoreg_db/data/requirement/colon_polyp_intervention.yaml +49 -0
  99. endoreg_db/data/requirement/coloreg_colon_polyp.yaml +49 -0
  100. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +31 -12
  101. endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
  102. endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
  103. endoreg_db/data/requirement_set/90_coloreg.yaml +178 -0
  104. endoreg_db/data/requirement_set/_old_ +109 -0
  105. endoreg_db/data/requirement_set_type/data.yaml +21 -0
  106. endoreg_db/data/setup_config.yaml +4 -4
  107. endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
  108. endoreg_db/exceptions.py +5 -2
  109. endoreg_db/helpers/data_loader.py +1 -1
  110. endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
  111. endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
  112. endoreg_db/management/commands/import_video.py +9 -10
  113. endoreg_db/management/commands/import_video_with_classification.py +1 -1
  114. endoreg_db/management/commands/init_default_ai_model.py +1 -1
  115. endoreg_db/management/commands/list_routes.py +18 -0
  116. endoreg_db/management/commands/load_ai_model_data.py +2 -1
  117. endoreg_db/management/commands/load_center_data.py +12 -12
  118. endoreg_db/management/commands/load_requirement_data.py +60 -31
  119. endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
  120. endoreg_db/management/commands/setup_endoreg_db.py +14 -10
  121. endoreg_db/management/commands/storage_management.py +271 -203
  122. endoreg_db/migrations/0001_initial.py +1799 -1300
  123. endoreg_db/migrations/0002_requirementset_depends_on.py +18 -0
  124. endoreg_db/migrations/_old/0001_initial.py +1857 -0
  125. endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +68 -0
  126. endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +77 -0
  127. endoreg_db/migrations/_old/0005_merge_20251111_1003.py +14 -0
  128. endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +68 -0
  129. endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +89 -0
  130. endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +27 -0
  131. endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +21 -0
  132. endoreg_db/models/__init__.py +78 -123
  133. endoreg_db/models/administration/__init__.py +21 -42
  134. endoreg_db/models/administration/ai/active_model.py +2 -2
  135. endoreg_db/models/administration/ai/ai_model.py +7 -6
  136. endoreg_db/models/administration/case/__init__.py +1 -15
  137. endoreg_db/models/administration/case/case.py +3 -3
  138. endoreg_db/models/administration/case/case_template/__init__.py +2 -14
  139. endoreg_db/models/administration/case/case_template/case_template.py +2 -124
  140. endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
  141. endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
  142. endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
  143. endoreg_db/models/administration/center/center.py +33 -19
  144. endoreg_db/models/administration/center/center_product.py +12 -9
  145. endoreg_db/models/administration/center/center_resource.py +25 -19
  146. endoreg_db/models/administration/center/center_shift.py +21 -17
  147. endoreg_db/models/administration/center/center_waste.py +16 -8
  148. endoreg_db/models/administration/person/__init__.py +2 -0
  149. endoreg_db/models/administration/person/employee/employee.py +10 -5
  150. endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
  151. endoreg_db/models/administration/person/employee/employee_type.py +12 -6
  152. endoreg_db/models/administration/person/examiner/examiner.py +13 -11
  153. endoreg_db/models/administration/person/patient/__init__.py +2 -0
  154. endoreg_db/models/administration/person/patient/patient.py +103 -100
  155. endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
  156. endoreg_db/models/administration/person/person.py +4 -0
  157. endoreg_db/models/administration/person/profession/__init__.py +8 -4
  158. endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
  159. endoreg_db/models/administration/product/product.py +20 -15
  160. endoreg_db/models/administration/product/product_material.py +17 -18
  161. endoreg_db/models/administration/product/product_weight.py +12 -8
  162. endoreg_db/models/administration/product/reference_product.py +23 -55
  163. endoreg_db/models/administration/qualification/qualification.py +7 -3
  164. endoreg_db/models/administration/qualification/qualification_type.py +7 -3
  165. endoreg_db/models/administration/shift/scheduled_days.py +8 -5
  166. endoreg_db/models/administration/shift/shift.py +16 -12
  167. endoreg_db/models/administration/shift/shift_type.py +23 -31
  168. endoreg_db/models/label/__init__.py +7 -8
  169. endoreg_db/models/label/annotation/image_classification.py +10 -9
  170. endoreg_db/models/label/annotation/video_segmentation_annotation.py +8 -5
  171. endoreg_db/models/label/label.py +15 -15
  172. endoreg_db/models/label/label_set.py +19 -6
  173. endoreg_db/models/label/label_type.py +1 -1
  174. endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
  175. endoreg_db/models/label/label_video_segment/label_video_segment.py +76 -102
  176. endoreg_db/models/label/video_segmentation_label.py +4 -0
  177. endoreg_db/models/label/video_segmentation_labelset.py +4 -3
  178. endoreg_db/models/media/frame/frame.py +22 -22
  179. endoreg_db/models/media/pdf/raw_pdf.py +249 -177
  180. endoreg_db/models/media/pdf/report_file.py +25 -29
  181. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +30 -46
  182. endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
  183. endoreg_db/models/media/video/__init__.py +1 -0
  184. endoreg_db/models/media/video/create_from_file.py +48 -56
  185. endoreg_db/models/media/video/pipe_1.py +30 -33
  186. endoreg_db/models/media/video/pipe_2.py +8 -9
  187. endoreg_db/models/media/video/video_file.py +359 -204
  188. endoreg_db/models/media/video/video_file_ai.py +288 -74
  189. endoreg_db/models/media/video/video_file_anonymize.py +38 -38
  190. endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
  191. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
  192. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
  193. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
  194. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
  195. endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
  196. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
  197. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
  198. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
  199. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
  200. endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
  201. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
  202. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
  203. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
  204. endoreg_db/models/media/video/video_file_io.py +109 -62
  205. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
  206. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
  207. endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
  208. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
  209. endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
  210. endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
  211. endoreg_db/models/media/video/video_file_segments.py +24 -17
  212. endoreg_db/models/media/video/video_metadata.py +19 -35
  213. endoreg_db/models/media/video/video_processing.py +96 -95
  214. endoreg_db/models/medical/contraindication/__init__.py +13 -3
  215. endoreg_db/models/medical/disease.py +22 -16
  216. endoreg_db/models/medical/event.py +31 -18
  217. endoreg_db/models/medical/examination/__init__.py +13 -6
  218. endoreg_db/models/medical/examination/examination.py +17 -18
  219. endoreg_db/models/medical/examination/examination_indication.py +26 -25
  220. endoreg_db/models/medical/examination/examination_time.py +16 -6
  221. endoreg_db/models/medical/examination/examination_time_type.py +9 -6
  222. endoreg_db/models/medical/examination/examination_type.py +3 -4
  223. endoreg_db/models/medical/finding/finding.py +38 -39
  224. endoreg_db/models/medical/finding/finding_classification.py +37 -48
  225. endoreg_db/models/medical/finding/finding_intervention.py +27 -22
  226. endoreg_db/models/medical/finding/finding_type.py +13 -12
  227. endoreg_db/models/medical/hardware/endoscope.py +20 -26
  228. endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
  229. endoreg_db/models/medical/laboratory/lab_value.py +62 -91
  230. endoreg_db/models/medical/medication/medication.py +22 -10
  231. endoreg_db/models/medical/medication/medication_indication.py +29 -3
  232. endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
  233. endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
  234. endoreg_db/models/medical/medication/medication_schedule.py +27 -16
  235. endoreg_db/models/medical/organ/__init__.py +15 -12
  236. endoreg_db/models/medical/patient/medication_examples.py +1 -5
  237. endoreg_db/models/medical/patient/patient_disease.py +20 -23
  238. endoreg_db/models/medical/patient/patient_event.py +19 -22
  239. endoreg_db/models/medical/patient/patient_examination.py +48 -54
  240. endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
  241. endoreg_db/models/medical/patient/patient_finding.py +122 -139
  242. endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
  243. endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
  244. endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
  245. endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
  246. endoreg_db/models/medical/patient/patient_medication.py +27 -38
  247. endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
  248. endoreg_db/models/medical/risk/risk.py +7 -6
  249. endoreg_db/models/medical/risk/risk_type.py +8 -5
  250. endoreg_db/models/metadata/model_meta.py +60 -29
  251. endoreg_db/models/metadata/model_meta_logic.py +139 -18
  252. endoreg_db/models/metadata/pdf_meta.py +19 -24
  253. endoreg_db/models/metadata/sensitive_meta.py +102 -85
  254. endoreg_db/models/metadata/sensitive_meta_logic.py +383 -43
  255. endoreg_db/models/metadata/video_meta.py +51 -31
  256. endoreg_db/models/metadata/video_prediction_logic.py +16 -23
  257. endoreg_db/models/metadata/video_prediction_meta.py +29 -33
  258. endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
  259. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
  260. endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
  261. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
  262. endoreg_db/models/other/emission/emission_factor.py +18 -8
  263. endoreg_db/models/other/gender.py +10 -5
  264. endoreg_db/models/other/information_source.py +25 -25
  265. endoreg_db/models/other/material.py +9 -5
  266. endoreg_db/models/other/resource.py +6 -4
  267. endoreg_db/models/other/tag.py +10 -5
  268. endoreg_db/models/other/transport_route.py +13 -8
  269. endoreg_db/models/other/unit.py +10 -6
  270. endoreg_db/models/other/waste.py +6 -5
  271. endoreg_db/models/requirement/requirement.py +580 -272
  272. endoreg_db/models/requirement/requirement_error.py +85 -0
  273. endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
  274. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
  275. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
  276. endoreg_db/models/requirement/requirement_operator.py +36 -33
  277. endoreg_db/models/requirement/requirement_set.py +74 -57
  278. endoreg_db/models/state/__init__.py +4 -4
  279. endoreg_db/models/state/abstract.py +2 -2
  280. endoreg_db/models/state/anonymization.py +12 -0
  281. endoreg_db/models/state/audit_ledger.py +46 -47
  282. endoreg_db/models/state/label_video_segment.py +9 -0
  283. endoreg_db/models/state/raw_pdf.py +40 -46
  284. endoreg_db/models/state/sensitive_meta.py +6 -2
  285. endoreg_db/models/state/video.py +58 -53
  286. endoreg_db/models/upload_job.py +32 -55
  287. endoreg_db/models/utils.py +1 -2
  288. endoreg_db/root_urls.py +21 -2
  289. endoreg_db/serializers/__init__.py +26 -57
  290. endoreg_db/serializers/anonymization.py +18 -10
  291. endoreg_db/serializers/meta/report_meta.py +1 -1
  292. endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
  293. endoreg_db/serializers/misc/__init__.py +1 -1
  294. endoreg_db/serializers/misc/file_overview.py +33 -91
  295. endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
  296. endoreg_db/serializers/requirements/requirement_sets.py +92 -22
  297. endoreg_db/serializers/video/segmentation.py +2 -1
  298. endoreg_db/serializers/video/video_processing_history.py +20 -5
  299. endoreg_db/serializers/video_examination.py +198 -0
  300. endoreg_db/services/anonymization.py +75 -73
  301. endoreg_db/services/lookup_service.py +256 -73
  302. endoreg_db/services/lookup_store.py +174 -30
  303. endoreg_db/services/pdf_import.py +711 -310
  304. endoreg_db/services/storage_aware_video_processor.py +140 -114
  305. endoreg_db/services/video_import.py +266 -117
  306. endoreg_db/urls/__init__.py +27 -27
  307. endoreg_db/urls/label_video_segments.py +2 -0
  308. endoreg_db/urls/media.py +108 -66
  309. endoreg_db/urls/root_urls.py +29 -0
  310. endoreg_db/utils/__init__.py +15 -5
  311. endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
  312. endoreg_db/utils/case_generator/__init__.py +3 -0
  313. endoreg_db/utils/dataloader.py +88 -16
  314. endoreg_db/utils/defaults/set_default_center.py +32 -0
  315. endoreg_db/utils/names.py +22 -16
  316. endoreg_db/utils/permissions.py +2 -1
  317. endoreg_db/utils/pipelines/process_video_dir.py +1 -1
  318. endoreg_db/utils/requirement_operator_logic/model_evaluators.py +414 -127
  319. endoreg_db/utils/setup_config.py +8 -5
  320. endoreg_db/utils/storage.py +115 -0
  321. endoreg_db/utils/validate_endo_roi.py +8 -2
  322. endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
  323. endoreg_db/views/__init__.py +5 -12
  324. endoreg_db/views/anonymization/media_management.py +198 -163
  325. endoreg_db/views/anonymization/overview.py +4 -1
  326. endoreg_db/views/anonymization/validate.py +174 -40
  327. endoreg_db/views/media/__init__.py +2 -0
  328. endoreg_db/views/media/pdf_media.py +131 -150
  329. endoreg_db/views/media/sensitive_metadata.py +46 -6
  330. endoreg_db/views/media/video_media.py +89 -82
  331. endoreg_db/views/media/video_segments.py +187 -260
  332. endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
  333. endoreg_db/views/patient/patient.py +5 -4
  334. endoreg_db/views/pdf/__init__.py +5 -8
  335. endoreg_db/views/pdf/pdf_stream.py +186 -0
  336. endoreg_db/views/pdf/pdf_stream_views.py +0 -127
  337. endoreg_db/views/pdf/reimport.py +86 -91
  338. endoreg_db/views/requirement/evaluate.py +188 -187
  339. endoreg_db/views/requirement/lookup.py +186 -288
  340. endoreg_db/views/requirement/requirement_utils.py +89 -0
  341. endoreg_db/views/video/__init__.py +0 -4
  342. endoreg_db/views/video/correction.py +2 -2
  343. endoreg_db/views/video/video_examination_viewset.py +202 -289
  344. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
  345. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +350 -255
  346. endoreg_db/models/administration/permissions/__init__.py +0 -44
  347. endoreg_db/models/media/video/refactor_plan.md +0 -0
  348. endoreg_db/models/media/video/video_file_frames.py +0 -0
  349. endoreg_db/models/metadata/frame_ocr_result.py +0 -0
  350. endoreg_db/models/rule/__init__.py +0 -13
  351. endoreg_db/models/rule/rule.py +0 -27
  352. endoreg_db/models/rule/rule_applicator.py +0 -224
  353. endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
  354. endoreg_db/models/rule/rule_type.py +0 -20
  355. endoreg_db/models/rule/ruleset.py +0 -17
  356. endoreg_db/serializers/video/video_metadata.py +0 -105
  357. endoreg_db/urls/report.py +0 -48
  358. endoreg_db/urls/video.py +0 -61
  359. endoreg_db/utils/case_generator/case_generator.py +0 -159
  360. endoreg_db/utils/case_generator/utils.py +0 -30
  361. endoreg_db/views/pdf/pdf_media.py +0 -239
  362. endoreg_db/views/report/__init__.py +0 -9
  363. endoreg_db/views/report/report_list.py +0 -112
  364. endoreg_db/views/report/report_with_secure_url.py +0 -28
  365. endoreg_db/views/report/start_examination.py +0 -7
  366. endoreg_db/views/video/video_media.py +0 -158
  367. endoreg_db/views.py +0 -0
  368. /endoreg_db/data/{requirement_set → _examples/requirement_set}/endoscopy_bleeding_risk.yaml +0 -0
  369. /endoreg_db/migrations/{0002_add_video_correction_models.py → _old/0002_add_video_correction_models.py} +0 -0
  370. /endoreg_db/migrations/{0003_add_center_display_name.py → _old/0003_add_center_display_name.py} +0 -0
  371. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
  372. {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,483 +1,381 @@
1
1
  # api/viewsets/lookup.py
2
+ import logging
3
+ from ast import literal_eval
4
+ from collections.abc import Mapping
2
5
 
3
- from rest_framework import viewsets, status
4
-
6
+ from django.core.cache import cache
7
+ from rest_framework import status, viewsets
5
8
  from rest_framework.decorators import action
6
-
9
+ from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
7
10
  from rest_framework.response import Response
8
11
 
9
-
10
- from rest_framework.parsers import JSONParser, FormParser, MultiPartParser
11
-
12
-
13
- from endoreg_db.services.lookup_store import LookupStore, DEFAULT_TTL_SECONDS
14
-
15
-
16
12
  # Use module import so tests can monkeypatch functions on the module
17
-
18
-
19
13
  from endoreg_db.services import lookup_service as ls
20
-
21
-
14
+ from endoreg_db.services.lookup_store import DEFAULT_TTL_SECONDS, LookupStore
22
15
  from endoreg_db.utils.permissions import EnvironmentAwarePermission
23
-
24
-
25
- from django.core.cache import cache
26
-
27
-
28
- import logging
29
-
30
-
31
- from ast import literal_eval
32
-
33
-
34
- from collections.abc import Mapping
35
-
36
-
37
-
38
-
16
+ from endoreg_db.models.other.tag import Tag
39
17
 
40
18
  ORIGIN_MAP_PREFIX = "lookup:origin:"
41
-
42
-
43
19
  ISSUED_MAP_PREFIX = "lookup:issued_for_internal:"
44
20
 
45
-
46
-
47
-
48
-
49
21
  logger = logging.getLogger(__name__)
50
22
 
51
23
 
52
-
53
24
  class LookupViewSet(viewsets.ViewSet):
54
-
25
+ """
26
+ Django REST Framework ViewSet for managing lookup sessions.
27
+
28
+ This ViewSet provides REST API endpoints for the lookup system, which
29
+ evaluates medical examination requirements against patient data. It uses
30
+ token-based sessions stored in Django cache to maintain state across
31
+ multiple client requests.
32
+
33
+ Key features:
34
+ - Session initialization with patient examination data
35
+ - Retrieval of lookup data by token
36
+ - Partial updates to session data with automatic recomputation
37
+ - Manual recomputation of derived data
38
+ - Automatic session recovery for expired tokens
39
+
40
+ The API supports both internal service tokens and public client tokens,
41
+ with origin mapping to enable session restart functionality.
42
+
43
+ Endpoints:
44
+ - POST /init: Initialize new lookup session
45
+ - GET /{token}/all: Retrieve complete session data
46
+ - GET/PATCH /{token}/parts: Get/update partial session data
47
+ - POST /{token}/recompute: Manually trigger recomputation
48
+ """
55
49
 
56
50
  permission_classes = [EnvironmentAwarePermission]
57
-
58
-
59
51
  parser_classes = (JSONParser, FormParser, MultiPartParser)
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
- INPUT_KEYS = {"patient_examination_id", "selectedRequirementSetIds", "selectedChoices"}
70
-
71
-
52
+ INPUT_KEYS = {
53
+ "patient_examination_id",
54
+ "selectedRequirementSetIds",
55
+ "selectedChoices",
56
+ }
57
+
58
+ user_tags = Tag
72
59
 
73
60
  @action(detail=False, methods=["post"])
74
-
75
61
  def init(self, request):
62
+ """
63
+ Initialize a new lookup session for a patient examination.
76
64
 
65
+ Creates a new token-based session containing initial lookup data
66
+ for the specified patient examination. Handles malformed payloads
67
+ and supports multiple initialization requests for the same examination
68
+ by issuing fresh public tokens while maintaining internal state.
77
69
 
70
+ Request body:
71
+ patient_examination_id: Integer ID of the patient examination
78
72
 
73
+ Returns:
74
+ JSON response with session token
79
75
 
76
+ Raises:
77
+ 400: Invalid patient_examination_id or creation failure
78
+ """
80
79
  try:
81
-
82
-
83
- debug_data = getattr(request, 'data', None)
84
-
85
-
86
- raw_post = getattr(getattr(request, '_request', None), 'POST', None)
87
-
88
-
80
+ debug_data = getattr(request, "data", None)
81
+ raw_post = getattr(getattr(request, "_request", None), "POST", None)
89
82
  body_preview = None
90
-
91
-
92
83
  try:
93
-
94
-
95
- body = getattr(getattr(request, '_request', None), 'body', b'')
96
-
97
-
84
+ body = getattr(getattr(request, "_request", None), "body", b"")
98
85
  body_preview = body[:200]
99
86
 
100
-
101
87
  except Exception:
102
-
103
-
104
88
  body_preview = None
105
-
106
-
107
- logger.debug("lookup.init incoming: data=%r POST=%r body[:200]=%r", debug_data, raw_post, body_preview)
108
-
89
+ logger.debug(
90
+ "lookup.init incoming: data=%r POST=%r body[:200]=%r",
91
+ debug_data,
92
+ raw_post,
93
+ body_preview,
94
+ )
109
95
 
110
96
  except Exception:
111
-
112
-
113
97
  pass
114
-
115
-
116
-
117
-
118
-
119
98
  # Prefer DRF data
120
-
121
-
122
- raw_pe = request.data.get("patient_examination_id") if hasattr(request, "data") else None
123
-
124
-
125
-
126
-
127
-
99
+ raw_pe = (
100
+ request.data.get("patient_examination_id")
101
+ if hasattr(request, "data")
102
+ else None
103
+ )
128
104
  # Fallback: parse malformed form payload where the entire dict was sent as a single key string
129
105
 
130
-
131
106
  if raw_pe is None:
132
-
133
-
134
- for candidate in (getattr(getattr(request, '_request', None), 'POST', None), request.data if hasattr(request, 'data') else None):
135
-
136
-
107
+ for candidate in (
108
+ getattr(getattr(request, "_request", None), "POST", None),
109
+ request.data if hasattr(request, "data") else None,
110
+ ):
137
111
  try:
138
-
139
-
140
112
  if isinstance(candidate, Mapping) and len(candidate.keys()) == 1:
141
-
142
-
143
113
  only_key = next(iter(candidate.keys()))
144
-
145
-
146
- if isinstance(only_key, str) and only_key.startswith('{') and only_key.endswith('}'):
147
-
148
-
114
+ if (
115
+ isinstance(only_key, str)
116
+ and only_key.startswith("{")
117
+ and only_key.endswith("}")
118
+ ):
149
119
  try:
150
-
151
-
152
120
  parsed = literal_eval(only_key)
153
-
154
-
155
- if isinstance(parsed, dict) and 'patient_examination_id' in parsed:
156
-
157
-
158
- raw_pe = parsed.get('patient_examination_id')
159
-
160
-
161
- logger.debug("lookup.init recovered pe_id from malformed payload: %r", raw_pe)
162
-
163
-
121
+ if (
122
+ isinstance(parsed, dict)
123
+ and "patient_examination_id" in parsed
124
+ ):
125
+ raw_pe = parsed.get("patient_examination_id")
126
+ logger.debug(
127
+ "lookup.init recovered pe_id from malformed payload: %r",
128
+ raw_pe,
129
+ )
164
130
  break
165
131
 
166
-
167
132
  except Exception:
168
-
169
-
170
133
  pass
171
134
 
172
-
173
135
  except Exception:
174
-
175
-
176
136
  pass
177
-
178
-
179
-
180
-
181
-
137
+
138
+ user_tags = request.data.get("user_tags", None)
139
+ if user_tags and not isinstance(user_tags, list):
140
+ user_tags = [user_tags]
182
141
  # Fallback to query params
183
-
184
-
185
142
  if raw_pe is None:
186
-
187
-
188
143
  raw_pe = request.query_params.get("patient_examination_id")
189
144
 
190
-
191
-
192
-
193
-
194
145
  logger.debug("lookup.init raw_pe=%r type=%s", raw_pe, type(raw_pe))
195
146
 
196
-
197
-
198
-
199
-
200
147
  # Normalize potential list/tuple inputs (e.g., from form submissions)
201
-
202
-
203
148
  if isinstance(raw_pe, (list, tuple)):
204
-
205
-
206
149
  raw_pe = raw_pe[0] if raw_pe else None
207
150
 
208
-
209
151
  if raw_pe in (None, ""):
210
-
211
-
212
- return Response({"detail": "patient_examination_id must be an integer"}, status=status.HTTP_400_BAD_REQUEST)
213
-
214
-
215
-
216
-
152
+ return Response(
153
+ {"detail": "patient_examination_id must be an integer"},
154
+ status=status.HTTP_400_BAD_REQUEST,
155
+ )
217
156
 
218
157
  # Coerce to int robustly
219
-
220
-
221
158
  try:
222
-
223
-
224
159
  pe_id = int(str(raw_pe))
225
160
 
226
-
227
161
  except (TypeError, ValueError):
228
-
229
-
230
162
  logger.warning("lookup.init failed to int() raw_pe=%r", raw_pe)
231
-
232
-
233
- return Response({"detail": "patient_examination_id must be an integer"}, status=status.HTTP_400_BAD_REQUEST)
234
-
163
+ return Response(
164
+ {"detail": "patient_examination_id must be an integer"},
165
+ status=status.HTTP_400_BAD_REQUEST,
166
+ )
235
167
 
236
168
  if pe_id <= 0:
237
-
238
-
239
- return Response({"detail": "patient_examination_id must be positive"}, status=status.HTTP_400_BAD_REQUEST)
240
-
241
-
242
-
243
-
169
+ return Response(
170
+ {"detail": "patient_examination_id must be positive"},
171
+ status=status.HTTP_400_BAD_REQUEST,
172
+ )
244
173
 
245
174
  try:
246
-
247
-
248
175
  # Create internal session via service (may seed its own token/cache)
176
+ service_kwargs = {}
177
+ if user_tags:
178
+ service_kwargs["user_tags"] = user_tags
249
179
 
250
-
251
- internal_token = ls.create_lookup_token_for_pe(pe_id)
252
-
253
-
180
+ internal_token = ls.create_lookup_token_for_pe(pe_id, **service_kwargs)
254
181
  internal_data = LookupStore(token=internal_token).get_all()
255
182
 
256
-
257
-
258
-
259
-
260
183
  issued_key = f"{ISSUED_MAP_PREFIX}{internal_token}"
261
184
 
262
-
263
185
  issued_count = cache.get(issued_key, 0)
264
186
 
265
-
266
-
267
-
268
-
269
187
  if issued_count == 0:
270
-
271
-
272
188
  # First issuance: return the service token directly
273
189
 
274
-
275
190
  token_to_return = internal_token
276
191
 
277
-
278
192
  cache.set(issued_key, 1, DEFAULT_TTL_SECONDS)
279
193
 
280
-
281
194
  else:
282
-
283
-
284
195
  # Subsequent inits for same internal token: issue a fresh public token seeded with internal data
285
196
 
286
-
287
197
  public_store = LookupStore()
288
198
 
289
-
290
- token_to_return = public_store.init(initial=internal_data, ttl=DEFAULT_TTL_SECONDS)
291
-
199
+ token_to_return = public_store.init(
200
+ initial=internal_data, ttl=DEFAULT_TTL_SECONDS
201
+ )
292
202
 
293
203
  cache.set(issued_key, issued_count + 1, DEFAULT_TTL_SECONDS)
294
204
 
295
-
296
-
297
-
298
-
299
205
  # Persist origin mapping so we can restart expired sessions
300
206
 
301
-
302
- cache.set(f"{ORIGIN_MAP_PREFIX}{token_to_return}", pe_id, DEFAULT_TTL_SECONDS)
207
+ cache.set(
208
+ f"{ORIGIN_MAP_PREFIX}{token_to_return}", pe_id, DEFAULT_TTL_SECONDS
209
+ )
303
210
 
304
211
  except Exception as e:
305
-
306
212
  return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
307
213
 
308
-
309
214
  return Response({"token": token_to_return}, status=status.HTTP_201_CREATED)
310
215
 
311
-
312
-
313
216
  @action(detail=True, methods=["get"], url_path="all")
314
-
315
217
  def get_all(self, request, pk=None):
218
+ """
219
+ Retrieve complete lookup data for a session token.
316
220
 
221
+ Returns all stored data for the given token. If data is not found,
222
+ attempts automatic session recovery using persisted origin mapping.
317
223
 
318
- if not pk:
224
+ Args:
225
+ pk: Session token
319
226
 
227
+ Returns:
228
+ Complete lookup data dictionary
320
229
 
321
- return Response({"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND)
230
+ Raises:
231
+ 404: Token not found and recovery failed
232
+ """
322
233
 
323
- store = LookupStore(token=pk)
234
+ if not pk:
235
+ return Response(
236
+ {"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND
237
+ )
324
238
 
239
+ store = LookupStore(token=pk)
325
240
 
326
241
  try:
327
-
328
-
329
242
  validated_data = store.validate_and_recover_data(pk)
330
243
 
331
-
332
244
  except Exception:
333
-
334
-
335
245
  validated_data = None
336
246
 
337
247
  if validated_data is None:
338
-
339
-
340
248
  # Try automatic restart once using persisted origin mapping
341
249
 
342
-
343
250
  pe_id = cache.get(f"{ORIGIN_MAP_PREFIX}{pk}")
344
251
 
345
-
346
252
  if pe_id:
347
-
348
-
349
253
  try:
350
-
351
-
352
254
  internal_token = ls.create_lookup_token_for_pe(int(pe_id))
353
255
 
354
-
355
256
  new_data = LookupStore(token=internal_token).get_all()
356
257
 
357
-
358
258
  if not new_data:
259
+ return Response(
260
+ {
261
+ "error": "Lookup data not available after restart",
262
+ "token": pk,
263
+ },
264
+ status=status.HTTP_404_NOT_FOUND,
265
+ )
359
266
 
360
-
361
- return Response({"error": "Lookup data not available after restart", "token": pk}, status=status.HTTP_404_NOT_FOUND)
362
-
363
-
364
- return Response(new_data, status=status.HTTP_200_OK)
267
+ # Hydrate the original token with recovered data and refresh origin TTL
268
+ store.set_many(new_data)
269
+ cache.set(f"{ORIGIN_MAP_PREFIX}{pk}", pe_id, DEFAULT_TTL_SECONDS)
270
+ return Response(store.get_all(), status=status.HTTP_200_OK)
365
271
 
366
272
 
367
273
  except Exception:
368
-
369
-
370
274
  pass
371
275
 
372
-
373
- return Response({"error": "Lookup data not found or expired", "token": pk}, status=status.HTTP_404_NOT_FOUND)
276
+ return Response(
277
+ {"error": "Lookup data not found or expired", "token": pk},
278
+ status=status.HTTP_404_NOT_FOUND,
279
+ )
374
280
 
375
281
  return Response(store.get_all())
376
282
 
377
-
378
-
379
283
  @action(detail=True, methods=["get", "patch"], url_path="parts")
380
-
381
284
  def parts(self, request, pk=None):
285
+ """
286
+ Get or update partial lookup data for a session.
382
287
 
288
+ GET: Retrieve specific keys from the session data.
289
+ PATCH: Update session data and trigger recomputation if input keys changed.
383
290
 
384
- if not pk:
291
+ GET query params:
292
+ keys: Comma-separated list of keys to retrieve
385
293
 
294
+ PATCH body:
295
+ updates: Dictionary of key-value pairs to update
386
296
 
387
- return Response({"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND)
297
+ Args:
298
+ pk: Session token
388
299
 
389
- store = LookupStore(token=pk)
300
+ Returns:
301
+ GET: Dictionary with requested keys
302
+ PATCH: Success confirmation
390
303
 
304
+ Raises:
305
+ 404: Token not found
306
+ 400: Invalid request parameters
307
+ """
391
308
 
309
+ if not pk:
310
+ return Response(
311
+ {"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND
312
+ )
392
313
 
393
- if request.method == "GET":
314
+ store = LookupStore(token=pk)
394
315
 
316
+ if request.method == "GET":
395
317
  keys_param = request.query_params.get("keys", "")
396
318
 
397
319
  keys = [k.strip() for k in keys_param.split(",") if k.strip()]
398
320
 
399
321
  if not keys:
400
-
401
- return Response({"detail": "Provide ?keys=key1,key2"}, status=status.HTTP_400_BAD_REQUEST)
402
-
322
+ return Response(
323
+ {"detail": "Provide ?keys=key1,key2"},
324
+ status=status.HTTP_400_BAD_REQUEST,
325
+ )
403
326
 
404
327
  try:
405
-
406
-
407
328
  return Response(store.get_many(keys))
408
329
 
409
-
410
330
  except Exception:
411
-
412
-
413
- return Response({"detail": "Lookup data not found or expired"}, status=status.HTTP_404_NOT_FOUND)
414
-
415
-
331
+ return Response(
332
+ {"detail": "Lookup data not found or expired"},
333
+ status=status.HTTP_404_NOT_FOUND,
334
+ )
416
335
 
417
336
  # PATCH
418
337
 
419
338
  updates = request.data.get("updates", {})
420
339
 
421
-
422
340
  if not isinstance(updates, dict) or not updates:
423
-
424
-
425
- return Response({"detail": "updates must be a non-empty object"}, status=status.HTTP_400_BAD_REQUEST)
426
-
427
-
341
+ return Response(
342
+ {"detail": "updates must be a non-empty object"},
343
+ status=status.HTTP_400_BAD_REQUEST,
344
+ )
428
345
 
429
346
  store.set_many(updates)
430
347
 
431
-
432
-
433
348
  if any(key in self.INPUT_KEYS for key in updates.keys()):
434
-
435
349
  try:
436
-
437
-
438
350
  ls.recompute_lookup(pk)
439
351
 
440
352
  except Exception as e:
441
-
442
353
  import logging
443
354
 
444
-
445
- logging.getLogger(__name__).error("Failed to recompute after patch for token %s: %s", pk, e)
446
-
447
-
355
+ logging.getLogger(__name__).error(
356
+ "Failed to recompute after patch for token %s: %s", pk, e
357
+ )
448
358
 
449
359
  return Response({"ok": True, "token": pk}, status=status.HTTP_200_OK)
450
360
 
451
-
452
-
453
361
  @action(detail=True, methods=["post"], url_path="recompute")
454
-
455
362
  def recompute(self, request, pk=None):
456
-
457
363
  """Recompute lookup data based on current PatientExamination and user selections"""
458
364
 
459
-
460
365
  if not pk:
461
-
462
-
463
- return Response({"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND)
366
+ return Response(
367
+ {"detail": "Token required"}, status=status.HTTP_404_NOT_FOUND
368
+ )
464
369
 
465
370
  try:
466
-
467
-
468
371
  updates = ls.recompute_lookup(pk)
469
372
 
470
-
471
- return Response({"ok": True, "token": pk, "updates": updates}, status=status.HTTP_200_OK)
472
-
473
-
474
-
475
-
373
+ return Response(
374
+ {"ok": True, "token": pk, "updates": updates}, status=status.HTTP_200_OK
375
+ )
476
376
 
477
377
  except ValueError as e:
478
-
479
378
  return Response({"detail": str(e)}, status=status.HTTP_404_NOT_FOUND)
480
379
 
481
380
  except Exception as e:
482
-
483
- return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
381
+ return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)