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.
- endoreg_db/authz/auth.py +74 -0
- endoreg_db/authz/backends.py +168 -0
- endoreg_db/authz/management/commands/list_routes.py +18 -0
- endoreg_db/authz/middleware.py +83 -0
- endoreg_db/authz/permissions.py +127 -0
- endoreg_db/authz/policy.py +218 -0
- endoreg_db/authz/views_auth.py +66 -0
- endoreg_db/config/env.py +13 -8
- endoreg_db/data/__init__.py +8 -31
- endoreg_db/data/_examples/disease.yaml +55 -0
- endoreg_db/data/_examples/disease_classification.yaml +13 -0
- endoreg_db/data/_examples/disease_classification_choice.yaml +62 -0
- endoreg_db/data/_examples/event.yaml +64 -0
- endoreg_db/data/_examples/examination.yaml +72 -0
- endoreg_db/data/_examples/finding/anatomy_colon.yaml +128 -0
- endoreg_db/data/_examples/finding/colonoscopy.yaml +40 -0
- endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +56 -0
- endoreg_db/data/_examples/finding/complication.yaml +16 -0
- endoreg_db/data/_examples/finding/data.yaml +105 -0
- endoreg_db/data/_examples/finding/examination_setting.yaml +16 -0
- endoreg_db/data/_examples/finding/medication_related.yaml +18 -0
- endoreg_db/data/_examples/finding/outcome.yaml +12 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +80 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +21 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +53 -0
- endoreg_db/data/_examples/finding_classification/complication_generic.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +40 -0
- endoreg_db/data/_examples/finding_classification/histology_colo.yaml +51 -0
- endoreg_db/data/_examples/finding_classification/intervention_required.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/medication_related.yaml +23 -0
- endoreg_db/data/_examples/finding_classification/visualized.yaml +33 -0
- endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +78 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +17 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_paris.yaml +57 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +14 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_location.yaml +229 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +82 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/histology.yaml +24 -0
- endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +20 -0
- endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +11 -0
- endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +48 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +43 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +128 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +32 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +9 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +36 -0
- endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +15 -0
- endoreg_db/data/_examples/finding_type/data.yaml +43 -0
- endoreg_db/data/_examples/requirement/age.yaml +26 -0
- endoreg_db/data/_examples/requirement/colonoscopy_baseline_austria.yaml +45 -0
- endoreg_db/data/_examples/requirement/disease_cardiovascular.yaml +79 -0
- endoreg_db/data/_examples/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
- endoreg_db/data/_examples/requirement/disease_hepatology.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_misc.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_renal.yaml +96 -0
- endoreg_db/data/_examples/requirement/endoscopy_bleeding_risk.yaml +59 -0
- endoreg_db/data/_examples/requirement/event_cardiology.yaml +251 -0
- endoreg_db/data/_examples/requirement/event_requirements.yaml +145 -0
- endoreg_db/data/_examples/requirement/finding_colon_polyp.yaml +50 -0
- endoreg_db/data/_examples/requirement/gender.yaml +25 -0
- endoreg_db/data/_examples/requirement/lab_value.yaml +441 -0
- endoreg_db/data/_examples/requirement/medication.yaml +93 -0
- endoreg_db/data/_examples/requirement_operator/age.yaml +13 -0
- endoreg_db/data/_examples/requirement_operator/lab_operators.yaml +129 -0
- endoreg_db/data/_examples/requirement_operator/model_operators.yaml +96 -0
- endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +48 -0
- endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +57 -0
- endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +4 -3
- endoreg_db/data/event_classification/data.yaml +4 -0
- endoreg_db/data/event_classification_choice/data.yaml +9 -0
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +43 -70
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +22 -52
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +31 -62
- endoreg_db/data/finding_classification/histology_colo.yaml +28 -36
- endoreg_db/data/requirement/colon_polyp_intervention.yaml +49 -0
- endoreg_db/data/requirement/coloreg_colon_polyp.yaml +49 -0
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +31 -12
- endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
- endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
- endoreg_db/data/requirement_set/90_coloreg.yaml +178 -0
- endoreg_db/data/requirement_set/_old_ +109 -0
- endoreg_db/data/requirement_set_type/data.yaml +21 -0
- endoreg_db/data/setup_config.yaml +4 -4
- endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
- endoreg_db/exceptions.py +5 -2
- endoreg_db/helpers/data_loader.py +1 -1
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
- endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
- endoreg_db/management/commands/import_video.py +9 -10
- endoreg_db/management/commands/import_video_with_classification.py +1 -1
- endoreg_db/management/commands/init_default_ai_model.py +1 -1
- endoreg_db/management/commands/list_routes.py +18 -0
- endoreg_db/management/commands/load_ai_model_data.py +2 -1
- endoreg_db/management/commands/load_center_data.py +12 -12
- endoreg_db/management/commands/load_requirement_data.py +60 -31
- endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
- endoreg_db/management/commands/setup_endoreg_db.py +14 -10
- endoreg_db/management/commands/storage_management.py +271 -203
- endoreg_db/migrations/0001_initial.py +1799 -1300
- endoreg_db/migrations/0002_requirementset_depends_on.py +18 -0
- endoreg_db/migrations/_old/0001_initial.py +1857 -0
- endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +68 -0
- endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +77 -0
- endoreg_db/migrations/_old/0005_merge_20251111_1003.py +14 -0
- endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +68 -0
- endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +89 -0
- endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +27 -0
- endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +21 -0
- endoreg_db/models/__init__.py +78 -123
- endoreg_db/models/administration/__init__.py +21 -42
- endoreg_db/models/administration/ai/active_model.py +2 -2
- endoreg_db/models/administration/ai/ai_model.py +7 -6
- endoreg_db/models/administration/case/__init__.py +1 -15
- endoreg_db/models/administration/case/case.py +3 -3
- endoreg_db/models/administration/case/case_template/__init__.py +2 -14
- endoreg_db/models/administration/case/case_template/case_template.py +2 -124
- endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
- endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
- endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
- endoreg_db/models/administration/center/center.py +33 -19
- endoreg_db/models/administration/center/center_product.py +12 -9
- endoreg_db/models/administration/center/center_resource.py +25 -19
- endoreg_db/models/administration/center/center_shift.py +21 -17
- endoreg_db/models/administration/center/center_waste.py +16 -8
- endoreg_db/models/administration/person/__init__.py +2 -0
- endoreg_db/models/administration/person/employee/employee.py +10 -5
- endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
- endoreg_db/models/administration/person/employee/employee_type.py +12 -6
- endoreg_db/models/administration/person/examiner/examiner.py +13 -11
- endoreg_db/models/administration/person/patient/__init__.py +2 -0
- endoreg_db/models/administration/person/patient/patient.py +103 -100
- endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
- endoreg_db/models/administration/person/person.py +4 -0
- endoreg_db/models/administration/person/profession/__init__.py +8 -4
- endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
- endoreg_db/models/administration/product/product.py +20 -15
- endoreg_db/models/administration/product/product_material.py +17 -18
- endoreg_db/models/administration/product/product_weight.py +12 -8
- endoreg_db/models/administration/product/reference_product.py +23 -55
- endoreg_db/models/administration/qualification/qualification.py +7 -3
- endoreg_db/models/administration/qualification/qualification_type.py +7 -3
- endoreg_db/models/administration/shift/scheduled_days.py +8 -5
- endoreg_db/models/administration/shift/shift.py +16 -12
- endoreg_db/models/administration/shift/shift_type.py +23 -31
- endoreg_db/models/label/__init__.py +7 -8
- endoreg_db/models/label/annotation/image_classification.py +10 -9
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +8 -5
- endoreg_db/models/label/label.py +15 -15
- endoreg_db/models/label/label_set.py +19 -6
- endoreg_db/models/label/label_type.py +1 -1
- endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
- endoreg_db/models/label/label_video_segment/label_video_segment.py +76 -102
- endoreg_db/models/label/video_segmentation_label.py +4 -0
- endoreg_db/models/label/video_segmentation_labelset.py +4 -3
- endoreg_db/models/media/frame/frame.py +22 -22
- endoreg_db/models/media/pdf/raw_pdf.py +249 -177
- endoreg_db/models/media/pdf/report_file.py +25 -29
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +30 -46
- endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
- endoreg_db/models/media/video/__init__.py +1 -0
- endoreg_db/models/media/video/create_from_file.py +48 -56
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/pipe_2.py +8 -9
- endoreg_db/models/media/video/video_file.py +359 -204
- endoreg_db/models/media/video/video_file_ai.py +288 -74
- endoreg_db/models/media/video/video_file_anonymize.py +38 -38
- endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
- endoreg_db/models/media/video/video_file_io.py +109 -62
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
- endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
- endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
- endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
- endoreg_db/models/media/video/video_file_segments.py +24 -17
- endoreg_db/models/media/video/video_metadata.py +19 -35
- endoreg_db/models/media/video/video_processing.py +96 -95
- endoreg_db/models/medical/contraindication/__init__.py +13 -3
- endoreg_db/models/medical/disease.py +22 -16
- endoreg_db/models/medical/event.py +31 -18
- endoreg_db/models/medical/examination/__init__.py +13 -6
- endoreg_db/models/medical/examination/examination.py +17 -18
- endoreg_db/models/medical/examination/examination_indication.py +26 -25
- endoreg_db/models/medical/examination/examination_time.py +16 -6
- endoreg_db/models/medical/examination/examination_time_type.py +9 -6
- endoreg_db/models/medical/examination/examination_type.py +3 -4
- endoreg_db/models/medical/finding/finding.py +38 -39
- endoreg_db/models/medical/finding/finding_classification.py +37 -48
- endoreg_db/models/medical/finding/finding_intervention.py +27 -22
- endoreg_db/models/medical/finding/finding_type.py +13 -12
- endoreg_db/models/medical/hardware/endoscope.py +20 -26
- endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
- endoreg_db/models/medical/laboratory/lab_value.py +62 -91
- endoreg_db/models/medical/medication/medication.py +22 -10
- endoreg_db/models/medical/medication/medication_indication.py +29 -3
- endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
- endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
- endoreg_db/models/medical/medication/medication_schedule.py +27 -16
- endoreg_db/models/medical/organ/__init__.py +15 -12
- endoreg_db/models/medical/patient/medication_examples.py +1 -5
- endoreg_db/models/medical/patient/patient_disease.py +20 -23
- endoreg_db/models/medical/patient/patient_event.py +19 -22
- endoreg_db/models/medical/patient/patient_examination.py +48 -54
- endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
- endoreg_db/models/medical/patient/patient_finding.py +122 -139
- endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
- endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
- endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
- endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
- endoreg_db/models/medical/patient/patient_medication.py +27 -38
- endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
- endoreg_db/models/medical/risk/risk.py +7 -6
- endoreg_db/models/medical/risk/risk_type.py +8 -5
- endoreg_db/models/metadata/model_meta.py +60 -29
- endoreg_db/models/metadata/model_meta_logic.py +139 -18
- endoreg_db/models/metadata/pdf_meta.py +19 -24
- endoreg_db/models/metadata/sensitive_meta.py +102 -85
- endoreg_db/models/metadata/sensitive_meta_logic.py +383 -43
- endoreg_db/models/metadata/video_meta.py +51 -31
- endoreg_db/models/metadata/video_prediction_logic.py +16 -23
- endoreg_db/models/metadata/video_prediction_meta.py +29 -33
- endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
- endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
- endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
- endoreg_db/models/other/emission/emission_factor.py +18 -8
- endoreg_db/models/other/gender.py +10 -5
- endoreg_db/models/other/information_source.py +25 -25
- endoreg_db/models/other/material.py +9 -5
- endoreg_db/models/other/resource.py +6 -4
- endoreg_db/models/other/tag.py +10 -5
- endoreg_db/models/other/transport_route.py +13 -8
- endoreg_db/models/other/unit.py +10 -6
- endoreg_db/models/other/waste.py +6 -5
- endoreg_db/models/requirement/requirement.py +580 -272
- endoreg_db/models/requirement/requirement_error.py +85 -0
- endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
- endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
- endoreg_db/models/requirement/requirement_operator.py +36 -33
- endoreg_db/models/requirement/requirement_set.py +74 -57
- endoreg_db/models/state/__init__.py +4 -4
- endoreg_db/models/state/abstract.py +2 -2
- endoreg_db/models/state/anonymization.py +12 -0
- endoreg_db/models/state/audit_ledger.py +46 -47
- endoreg_db/models/state/label_video_segment.py +9 -0
- endoreg_db/models/state/raw_pdf.py +40 -46
- endoreg_db/models/state/sensitive_meta.py +6 -2
- endoreg_db/models/state/video.py +58 -53
- endoreg_db/models/upload_job.py +32 -55
- endoreg_db/models/utils.py +1 -2
- endoreg_db/root_urls.py +21 -2
- endoreg_db/serializers/__init__.py +26 -57
- endoreg_db/serializers/anonymization.py +18 -10
- endoreg_db/serializers/meta/report_meta.py +1 -1
- endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +33 -91
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/requirements/requirement_sets.py +92 -22
- endoreg_db/serializers/video/segmentation.py +2 -1
- endoreg_db/serializers/video/video_processing_history.py +20 -5
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/anonymization.py +75 -73
- endoreg_db/services/lookup_service.py +256 -73
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +711 -310
- endoreg_db/services/storage_aware_video_processor.py +140 -114
- endoreg_db/services/video_import.py +266 -117
- endoreg_db/urls/__init__.py +27 -27
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +108 -66
- endoreg_db/urls/root_urls.py +29 -0
- endoreg_db/utils/__init__.py +15 -5
- endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
- endoreg_db/utils/case_generator/__init__.py +3 -0
- endoreg_db/utils/dataloader.py +88 -16
- endoreg_db/utils/defaults/set_default_center.py +32 -0
- endoreg_db/utils/names.py +22 -16
- endoreg_db/utils/permissions.py +2 -1
- endoreg_db/utils/pipelines/process_video_dir.py +1 -1
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +414 -127
- endoreg_db/utils/setup_config.py +8 -5
- endoreg_db/utils/storage.py +115 -0
- endoreg_db/utils/validate_endo_roi.py +8 -2
- endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
- endoreg_db/views/__init__.py +5 -12
- endoreg_db/views/anonymization/media_management.py +198 -163
- endoreg_db/views/anonymization/overview.py +4 -1
- endoreg_db/views/anonymization/validate.py +174 -40
- endoreg_db/views/media/__init__.py +2 -0
- endoreg_db/views/media/pdf_media.py +131 -150
- endoreg_db/views/media/sensitive_metadata.py +46 -6
- endoreg_db/views/media/video_media.py +89 -82
- endoreg_db/views/media/video_segments.py +187 -260
- endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
- endoreg_db/views/patient/patient.py +5 -4
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +186 -0
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/pdf/reimport.py +86 -91
- endoreg_db/views/requirement/evaluate.py +188 -187
- endoreg_db/views/requirement/lookup.py +186 -288
- endoreg_db/views/requirement/requirement_utils.py +89 -0
- endoreg_db/views/video/__init__.py +0 -4
- endoreg_db/views/video/correction.py +2 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +350 -255
- endoreg_db/models/administration/permissions/__init__.py +0 -44
- endoreg_db/models/media/video/refactor_plan.md +0 -0
- endoreg_db/models/media/video/video_file_frames.py +0 -0
- endoreg_db/models/metadata/frame_ocr_result.py +0 -0
- endoreg_db/models/rule/__init__.py +0 -13
- endoreg_db/models/rule/rule.py +0 -27
- endoreg_db/models/rule/rule_applicator.py +0 -224
- endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
- endoreg_db/models/rule/rule_type.py +0 -20
- endoreg_db/models/rule/ruleset.py +0 -17
- endoreg_db/serializers/video/video_metadata.py +0 -105
- endoreg_db/urls/report.py +0 -48
- endoreg_db/urls/video.py +0 -61
- endoreg_db/utils/case_generator/case_generator.py +0 -159
- endoreg_db/utils/case_generator/utils.py +0 -30
- endoreg_db/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/report/__init__.py +0 -9
- endoreg_db/views/report/report_list.py +0 -112
- endoreg_db/views/report/report_with_secure_url.py +0 -28
- endoreg_db/views/report/start_examination.py +0 -7
- endoreg_db/views/video/video_media.py +0 -158
- endoreg_db/views.py +0 -0
- /endoreg_db/data/{requirement_set → _examples/requirement_set}/endoscopy_bleeding_risk.yaml +0 -0
- /endoreg_db/migrations/{0002_add_video_correction_models.py → _old/0002_add_video_correction_models.py} +0 -0
- /endoreg_db/migrations/{0003_add_center_display_name.py → _old/0003_add_center_display_name.py} +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from ...utils import DOCUMENT_DIR, STORAGE_DIR
|
|
2
|
-
from django.db import models
|
|
3
1
|
from typing import TYPE_CHECKING
|
|
4
2
|
|
|
3
|
+
from django.db import models
|
|
4
|
+
|
|
5
|
+
from ...utils import DOCUMENT_DIR, STORAGE_DIR
|
|
6
|
+
|
|
5
7
|
if TYPE_CHECKING:
|
|
6
8
|
from ...administration import (
|
|
7
9
|
Center,
|
|
@@ -12,17 +14,21 @@ if TYPE_CHECKING:
|
|
|
12
14
|
)
|
|
13
15
|
from ...metadata import SensitiveMeta
|
|
14
16
|
|
|
17
|
+
|
|
15
18
|
class DocumentTypeManager(models.Manager):
|
|
16
19
|
"""
|
|
17
20
|
Custom manager for DocumentType.
|
|
18
21
|
"""
|
|
22
|
+
|
|
19
23
|
def get_by_natural_key(self, name):
|
|
20
24
|
return self.get(name=name)
|
|
21
25
|
|
|
26
|
+
|
|
22
27
|
class DocumentType(models.Model):
|
|
23
28
|
"""
|
|
24
29
|
Represents the type of a document.
|
|
25
30
|
"""
|
|
31
|
+
|
|
26
32
|
name = models.CharField(max_length=255, unique=True)
|
|
27
33
|
description = models.TextField(blank=True, null=True)
|
|
28
34
|
|
|
@@ -33,21 +39,23 @@ class DocumentType(models.Model):
|
|
|
33
39
|
|
|
34
40
|
def __str__(self):
|
|
35
41
|
return str(self.name)
|
|
36
|
-
|
|
42
|
+
|
|
37
43
|
class Meta:
|
|
38
44
|
verbose_name = "Document Type"
|
|
39
45
|
verbose_name_plural = "Document Types"
|
|
40
46
|
|
|
47
|
+
|
|
41
48
|
class AbstractDocument(models.Model):
|
|
42
49
|
"""
|
|
43
50
|
Abstract base class for documents.
|
|
44
51
|
"""
|
|
52
|
+
|
|
45
53
|
meta = models.JSONField(blank=True, null=True)
|
|
46
54
|
text = models.TextField(blank=True, null=True)
|
|
47
55
|
date = models.DateField(blank=True, null=True)
|
|
48
56
|
time = models.TimeField(blank=True, null=True)
|
|
49
57
|
file = models.FileField(
|
|
50
|
-
upload_to=DOCUMENT_DIR.relative_to(STORAGE_DIR),
|
|
58
|
+
upload_to=DOCUMENT_DIR.relative_to(STORAGE_DIR).as_posix(),
|
|
51
59
|
blank=True,
|
|
52
60
|
null=True,
|
|
53
61
|
)
|
|
@@ -67,22 +75,19 @@ class AbstractDocument(models.Model):
|
|
|
67
75
|
)
|
|
68
76
|
|
|
69
77
|
if TYPE_CHECKING:
|
|
70
|
-
center: "Center"
|
|
71
|
-
type: "DocumentType"
|
|
78
|
+
center: models.ForeignKey["Center|None"]
|
|
79
|
+
type: models.ForeignKey["DocumentType|None"]
|
|
72
80
|
|
|
73
81
|
class Meta:
|
|
74
82
|
abstract = True
|
|
75
83
|
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
79
85
|
class AbstractExaminationReport(AbstractDocument):
|
|
80
86
|
"""
|
|
81
87
|
Abstract base class for examination reports.
|
|
82
88
|
"""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
89
|
+
|
|
90
|
+
patient = models.ForeignKey("Patient", on_delete=models.DO_NOTHING, blank=True, null=True)
|
|
86
91
|
|
|
87
92
|
patient_examination = models.ForeignKey(
|
|
88
93
|
"PatientExamination",
|
|
@@ -96,25 +101,18 @@ class AbstractExaminationReport(AbstractDocument):
|
|
|
96
101
|
blank=True,
|
|
97
102
|
)
|
|
98
103
|
|
|
99
|
-
sensitive_meta = models.ForeignKey(
|
|
100
|
-
"SensitiveMeta",
|
|
101
|
-
on_delete=models.SET_NULL,
|
|
102
|
-
null=True,
|
|
103
|
-
blank=True
|
|
104
|
-
)
|
|
104
|
+
sensitive_meta = models.ForeignKey("SensitiveMeta", on_delete=models.SET_NULL, null=True, blank=True)
|
|
105
105
|
|
|
106
106
|
if TYPE_CHECKING:
|
|
107
|
-
center: "Center"
|
|
108
|
-
type: "DocumentType"
|
|
109
|
-
patient: "Patient"
|
|
110
|
-
patient_examination: "PatientExamination"
|
|
111
|
-
sensitive_meta: "SensitiveMeta"
|
|
112
|
-
|
|
107
|
+
center: models.ForeignKey["Center|None"]
|
|
108
|
+
type: models.ForeignKey["DocumentType|None"]
|
|
109
|
+
patient: models.ForeignKey["Patient|None"]
|
|
110
|
+
patient_examination: models.ForeignKey["PatientExamination|None"]
|
|
111
|
+
sensitive_meta: models.ForeignKey["SensitiveMeta|None"]
|
|
113
112
|
|
|
114
113
|
class Meta:
|
|
115
114
|
abstract = True
|
|
116
115
|
|
|
117
|
-
|
|
118
116
|
def get_or_create_examiner(self, examiner_first_name, examiner_last_name):
|
|
119
117
|
raise NotImplementedError("Subclasses must implement this method.")
|
|
120
118
|
|
|
@@ -122,10 +120,8 @@ class AbstractExaminationReport(AbstractDocument):
|
|
|
122
120
|
raise NotImplementedError("Subclasses must implement this method.")
|
|
123
121
|
|
|
124
122
|
|
|
125
|
-
|
|
126
123
|
class AnonymExaminationReport(AbstractExaminationReport):
|
|
127
|
-
|
|
128
|
-
def get_or_create_examiner(self, examiner_first_name:str, examiner_last_name:str):
|
|
124
|
+
def get_or_create_examiner(self, examiner_first_name: str, examiner_last_name: str):
|
|
129
125
|
from ...administration.person import Examiner
|
|
130
126
|
|
|
131
127
|
examiner_center = self.center
|
|
@@ -139,7 +135,7 @@ class AnonymExaminationReport(AbstractExaminationReport):
|
|
|
139
135
|
return examiner, created
|
|
140
136
|
|
|
141
137
|
def set_examination_date_and_time(self, report_meta=None):
|
|
142
|
-
#TODO
|
|
138
|
+
# TODO
|
|
143
139
|
if not report_meta:
|
|
144
140
|
report_meta = self.meta
|
|
145
141
|
# examination_date_str = report_meta["examination_date"]
|
|
@@ -152,11 +148,11 @@ class AnonymExaminationReport(AbstractExaminationReport):
|
|
|
152
148
|
# # TODO: get django TimeField compatible time from string (e.g. "12:00")
|
|
153
149
|
# self.time = time.fromisoformat(examination_time_str)
|
|
154
150
|
|
|
151
|
+
|
|
155
152
|
class AnonymHistologyReport(AbstractExaminationReport):
|
|
156
153
|
"""
|
|
157
154
|
Represents a histology report.
|
|
158
155
|
"""
|
|
159
156
|
|
|
160
|
-
|
|
161
157
|
def get_or_create_examiner(self, examiner_first_name, examiner_last_name):
|
|
162
158
|
raise NotImplementedError("Subclasses must implement this method.")
|
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
# ReportReaderConfig Class
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
# PATIENT_INFO_LINE_FLAG = "Patient: "
|
|
5
|
-
# ENDOSCOPE_INFO_LINE_FLAG = "Gerät: "
|
|
6
|
-
# EXAMINER_INFO_LINE_FLAG = "1. Unters.:"
|
|
7
|
-
# CUT_OFF_BELOW_LINE_FLAG = "________________"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# CUT_OFF_ABOVE_LINE_FLAGS = [
|
|
11
|
-
# ENDOSCOPE_INFO_LINE_FLAG,
|
|
12
|
-
# EXAMINER_INFO_LINE_FLAG,
|
|
13
|
-
# ]
|
|
14
|
-
|
|
15
|
-
# CUT_OFF_BELOW_LINE_FLAGS = [
|
|
16
|
-
# CUT_OFF_BELOW_LINE_FLAG
|
|
17
|
-
# ]
|
|
2
|
+
from typing import TYPE_CHECKING, cast
|
|
18
3
|
|
|
19
4
|
from django.db import models
|
|
20
5
|
|
|
21
|
-
|
|
22
|
-
from typing import TYPE_CHECKING
|
|
23
6
|
if TYPE_CHECKING:
|
|
24
|
-
from .report_reader_flag import ReportReaderFlag
|
|
25
|
-
from ....administration.person import FirstName, LastName
|
|
26
7
|
from ....administration.center import Center
|
|
8
|
+
from ....administration.person import FirstName, LastName
|
|
27
9
|
from ....metadata import PdfType
|
|
10
|
+
from .report_reader_flag import ReportReaderFlag
|
|
11
|
+
|
|
28
12
|
|
|
29
13
|
class ReportReaderConfig(models.Model):
|
|
30
14
|
"""
|
|
@@ -33,45 +17,45 @@ class ReportReaderConfig(models.Model):
|
|
|
33
17
|
Stores locale, name lists, date format, and flags used to identify key information lines
|
|
34
18
|
and text sections to ignore.
|
|
35
19
|
"""
|
|
20
|
+
|
|
36
21
|
locale = models.CharField(default="de_DE", max_length=10)
|
|
37
|
-
first_names = models.ManyToManyField(
|
|
38
|
-
last_names = models.ManyToManyField(
|
|
39
|
-
text_date_format = models.CharField(default
|
|
40
|
-
patient_info_line_flag = models.ForeignKey("ReportReaderFlag", related_name=
|
|
41
|
-
endoscope_info_line_flag = models.ForeignKey("ReportReaderFlag", related_name=
|
|
42
|
-
examiner_info_line_flag = models.ForeignKey("ReportReaderFlag", related_name=
|
|
43
|
-
cut_off_below = models.ManyToManyField("ReportReaderFlag", related_name=
|
|
44
|
-
cut_off_above = models.ManyToManyField("ReportReaderFlag", related_name=
|
|
45
|
-
|
|
22
|
+
first_names = models.ManyToManyField("FirstName", related_name="report_reader_configs")
|
|
23
|
+
last_names = models.ManyToManyField("LastName", related_name="report_reader_configs")
|
|
24
|
+
text_date_format = models.CharField(default="%d.%m.%Y", max_length=10)
|
|
25
|
+
patient_info_line_flag = models.ForeignKey("ReportReaderFlag", related_name="report_reader_configs_patient_info_line", on_delete=models.CASCADE)
|
|
26
|
+
endoscope_info_line_flag = models.ForeignKey("ReportReaderFlag", related_name="report_reader_configs_endoscope_info_line", on_delete=models.CASCADE)
|
|
27
|
+
examiner_info_line_flag = models.ForeignKey("ReportReaderFlag", related_name="report_reader_configs_examiner_info_line", on_delete=models.CASCADE)
|
|
28
|
+
cut_off_below = models.ManyToManyField("ReportReaderFlag", related_name="report_reader_configs_cut_off_below")
|
|
29
|
+
cut_off_above = models.ManyToManyField("ReportReaderFlag", related_name="report_reader_configs_cut_off_above")
|
|
30
|
+
|
|
46
31
|
if TYPE_CHECKING:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
32
|
+
patient_info_line_flag = models.ForeignKey["ReportReaderFlag"]
|
|
33
|
+
endoscope_info_line_flag = models.ForeignKey["ReportReaderFlag"]
|
|
34
|
+
examiner_info_line_flag = models.ForeignKey["ReportReaderFlag"]
|
|
35
|
+
|
|
36
|
+
first_names = cast(models.manager.RelatedManager["FirstName"], first_names)
|
|
37
|
+
last_names = cast(models.manager.RelatedManager["LastName"], last_names)
|
|
38
|
+
cut_off_below = cast(models.manager.RelatedManager["ReportReaderFlag"], cut_off_below)
|
|
39
|
+
cut_off_above = cast(models.manager.RelatedManager["ReportReaderFlag"], cut_off_above)
|
|
55
40
|
|
|
56
41
|
def __str__(self):
|
|
57
42
|
"""Returns a string representation including the locale and primary key."""
|
|
58
43
|
_str = f"ReportReaderConfig: {self.locale} (id: {self.pk}\n"
|
|
59
44
|
return _str
|
|
60
|
-
|
|
61
|
-
def update_names_by_center(self, center:"Center", save
|
|
45
|
+
|
|
46
|
+
def update_names_by_center(self, center: "Center", save=True):
|
|
62
47
|
"""Updates the first and last name lists based on the names associated with a Center."""
|
|
63
48
|
self.first_names.set(center.first_names.all())
|
|
64
49
|
self.last_names.set(center.last_names.all())
|
|
65
50
|
if save:
|
|
66
51
|
self.save()
|
|
67
52
|
|
|
68
|
-
def update_flags_by_pdf_type(self, pdf_type:"PdfType", save
|
|
53
|
+
def update_flags_by_pdf_type(self, pdf_type: "PdfType", save=True):
|
|
69
54
|
"""Updates the line identification flags based on a specific PdfType."""
|
|
70
|
-
self.patient_info_line_flag = pdf_type.
|
|
71
|
-
self.endoscope_info_line_flag = pdf_type.
|
|
72
|
-
self.examiner_info_line_flag = pdf_type.
|
|
73
|
-
self.cut_off_below.set(pdf_type.
|
|
74
|
-
self.cut_off_above.set(pdf_type.
|
|
55
|
+
self.patient_info_line_flag = pdf_type.patient_info_line
|
|
56
|
+
self.endoscope_info_line_flag = pdf_type.endoscope_info_line
|
|
57
|
+
self.examiner_info_line_flag = pdf_type.examiner_info_line
|
|
58
|
+
self.cut_off_below.set(pdf_type.cut_off_below_lines.all())
|
|
59
|
+
self.cut_off_above.set(pdf_type.cut_off_above_lines.all())
|
|
75
60
|
if save:
|
|
76
61
|
self.save()
|
|
77
|
-
|
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
# have name and value
|
|
3
|
-
# name is natural key
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
4
2
|
|
|
5
3
|
from django.db import models
|
|
6
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from endoreg_db.models import ReportReaderConfig
|
|
7
|
+
|
|
8
|
+
|
|
7
9
|
class ReportReaderFlagManager(models.Manager):
|
|
8
10
|
def get_by_natural_key(self, name):
|
|
9
11
|
return self.get(name=name)
|
|
10
|
-
|
|
12
|
+
|
|
13
|
+
|
|
11
14
|
class ReportReaderFlag(models.Model):
|
|
12
15
|
objects = ReportReaderFlagManager()
|
|
13
16
|
name = models.CharField(max_length=255, unique=True)
|
|
14
17
|
value = models.CharField(max_length=255)
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def report_reader_configs_patient_info_line(self) -> models.QuerySet["ReportReaderConfig"]: ...
|
|
23
|
+
@property
|
|
24
|
+
def report_reader_configs_endoscope_info_line(self) -> models.QuerySet["ReportReaderConfig"]: ...
|
|
25
|
+
@property
|
|
26
|
+
def report_reader_configs_examiner_info_line(self) -> models.QuerySet["ReportReaderConfig"]: ...
|
|
27
|
+
@property
|
|
28
|
+
def report_reader_configs_cut_off_below(self) -> models.QuerySet["ReportReaderConfig"]: ...
|
|
29
|
+
@property
|
|
30
|
+
def report_reader_configs_cut_off_above(self) -> models.QuerySet["ReportReaderConfig"]: ...
|
|
31
|
+
|
|
16
32
|
def natural_key(self):
|
|
17
33
|
return (self.name,)
|
|
18
|
-
|
|
34
|
+
|
|
19
35
|
def __str__(self):
|
|
20
|
-
return self.name
|
|
36
|
+
return self.name
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import shutil
|
|
2
1
|
import logging
|
|
2
|
+
import shutil
|
|
3
3
|
import uuid
|
|
4
|
+
from importlib import import_module
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import TYPE_CHECKING, Optional, Type
|
|
6
7
|
|
|
7
8
|
# Import the new exceptions from the correct path
|
|
8
9
|
from endoreg_db.exceptions import InsufficientStorageError, TranscodingError
|
|
9
|
-
|
|
10
|
-
from
|
|
10
|
+
|
|
11
|
+
from ...utils import TMP_VIDEO_DIR, VIDEO_DIR
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from endoreg_db.models import VideoFile
|
|
14
15
|
|
|
15
|
-
from ....utils.video.ffmpeg_wrapper import transcode_videofile_if_required
|
|
16
|
-
from ....utils.hashs import get_video_hash
|
|
17
16
|
from ....utils.file_operations import get_uuid_filename
|
|
17
|
+
from ....utils.hashs import get_video_hash
|
|
18
|
+
from ....utils.video.ffmpeg_wrapper import transcode_videofile_if_required
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
@@ -22,33 +23,31 @@ logger = logging.getLogger(__name__)
|
|
|
22
23
|
def check_storage_capacity(src_path: Path, dst_root: Path, safety_margin: float = 1.2) -> None:
|
|
23
24
|
"""
|
|
24
25
|
Check if there's enough storage space before starting operations.
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
Args:
|
|
27
28
|
src_path: Source file path
|
|
28
29
|
dst_root: Destination root directory
|
|
29
30
|
safety_margin: Safety factor (1.2 = 20% extra space required)
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
Raises:
|
|
32
33
|
InsufficientStorageError: If insufficient storage space
|
|
33
34
|
"""
|
|
34
35
|
try:
|
|
35
36
|
src_size = src_path.stat().st_size
|
|
36
37
|
required_space = int(src_size * safety_margin)
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
# Check free space on destination
|
|
39
40
|
free_space = shutil.disk_usage(dst_root).free
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
if free_space < required_space:
|
|
42
43
|
raise InsufficientStorageError(
|
|
43
|
-
f"Insufficient storage space. Required: {required_space/1e9:.1f} GB, "
|
|
44
|
-
f"Available: {free_space/1e9:.1f} GB on {dst_root}",
|
|
44
|
+
f"Insufficient storage space. Required: {required_space / 1e9:.1f} GB, Available: {free_space / 1e9:.1f} GB on {dst_root}",
|
|
45
45
|
required_space=required_space,
|
|
46
|
-
available_space=free_space
|
|
46
|
+
available_space=free_space,
|
|
47
47
|
)
|
|
48
|
-
|
|
49
|
-
logger.info(f"Storage check passed: {free_space/1e9:.1f} GB available, "
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
|
|
49
|
+
logger.info(f"Storage check passed: {free_space / 1e9:.1f} GB available, {required_space / 1e9:.1f} GB required")
|
|
50
|
+
|
|
52
51
|
except OSError as e:
|
|
53
52
|
logger.warning(f"Could not check storage capacity: {e}")
|
|
54
53
|
# Don't fail the operation, just log the warning
|
|
@@ -57,14 +56,14 @@ def check_storage_capacity(src_path: Path, dst_root: Path, safety_margin: float
|
|
|
57
56
|
def atomic_copy_with_fallback(src_path: Path, dst_path: Path) -> bool:
|
|
58
57
|
"""
|
|
59
58
|
Atomically copy file from src to dst, preserving the source file.
|
|
60
|
-
|
|
59
|
+
|
|
61
60
|
Args:
|
|
62
61
|
src_path: Source file path
|
|
63
62
|
dst_path: Destination file path
|
|
64
|
-
|
|
63
|
+
|
|
65
64
|
Returns:
|
|
66
65
|
True if successful
|
|
67
|
-
|
|
66
|
+
|
|
68
67
|
Raises:
|
|
69
68
|
InsufficientStorageError: If not enough space for the operation
|
|
70
69
|
OSError: For other file system errors
|
|
@@ -73,18 +72,17 @@ def atomic_copy_with_fallback(src_path: Path, dst_path: Path) -> bool:
|
|
|
73
72
|
# Check space before copy
|
|
74
73
|
src_size = src_path.stat().st_size
|
|
75
74
|
free_space = shutil.disk_usage(dst_path.parent).free
|
|
76
|
-
|
|
75
|
+
|
|
77
76
|
if free_space < src_size * 1.1: # 10% safety margin
|
|
78
77
|
raise InsufficientStorageError(
|
|
79
|
-
f"Insufficient space for copy operation. Required: {src_size/1e9:.1f} GB, "
|
|
80
|
-
f"Available: {free_space/1e9:.1f} GB",
|
|
78
|
+
f"Insufficient space for copy operation. Required: {src_size / 1e9:.1f} GB, Available: {free_space / 1e9:.1f} GB",
|
|
81
79
|
required_space=src_size,
|
|
82
|
-
available_space=free_space
|
|
80
|
+
available_space=free_space,
|
|
83
81
|
)
|
|
84
|
-
|
|
82
|
+
|
|
85
83
|
# Use a temporary name during copy for atomicity
|
|
86
|
-
temp_dst = dst_path.with_suffix(dst_path.suffix +
|
|
87
|
-
|
|
84
|
+
temp_dst = dst_path.with_suffix(dst_path.suffix + ".tmp")
|
|
85
|
+
|
|
88
86
|
try:
|
|
89
87
|
shutil.copy2(str(src_path), str(temp_dst))
|
|
90
88
|
temp_dst.rename(dst_path)
|
|
@@ -95,7 +93,7 @@ def atomic_copy_with_fallback(src_path: Path, dst_path: Path) -> bool:
|
|
|
95
93
|
if temp_dst.exists():
|
|
96
94
|
temp_dst.unlink(missing_ok=True)
|
|
97
95
|
raise
|
|
98
|
-
|
|
96
|
+
|
|
99
97
|
except Exception as e:
|
|
100
98
|
logger.error(f"Copy operation failed: {src_path} -> {dst_path}: {e}")
|
|
101
99
|
raise
|
|
@@ -104,14 +102,14 @@ def atomic_copy_with_fallback(src_path: Path, dst_path: Path) -> bool:
|
|
|
104
102
|
def atomic_move_with_fallback(src_path: Path, dst_path: Path) -> bool:
|
|
105
103
|
"""
|
|
106
104
|
Atomically move file from src to dst, with fallback to copy+remove.
|
|
107
|
-
|
|
105
|
+
|
|
108
106
|
Args:
|
|
109
107
|
src_path: Source file path
|
|
110
108
|
dst_path: Destination file path
|
|
111
|
-
|
|
109
|
+
|
|
112
110
|
Returns:
|
|
113
111
|
True if successful
|
|
114
|
-
|
|
112
|
+
|
|
115
113
|
Raises:
|
|
116
114
|
InsufficientStorageError: If not enough space for the operation
|
|
117
115
|
OSError: For other file system errors
|
|
@@ -127,32 +125,31 @@ def atomic_move_with_fallback(src_path: Path, dst_path: Path) -> bool:
|
|
|
127
125
|
logger.debug("Cross-device move detected, falling back to copy+remove")
|
|
128
126
|
else:
|
|
129
127
|
raise
|
|
130
|
-
|
|
128
|
+
|
|
131
129
|
# Check space before cross-filesystem copy
|
|
132
130
|
src_size = src_path.stat().st_size
|
|
133
131
|
free_space = shutil.disk_usage(dst_path.parent).free
|
|
134
|
-
|
|
132
|
+
|
|
135
133
|
if free_space < src_size * 1.1: # 10% safety margin
|
|
136
134
|
raise InsufficientStorageError(
|
|
137
|
-
f"Insufficient space for copy operation. Required: {src_size/1e9:.1f} GB, "
|
|
138
|
-
f"Available: {free_space/1e9:.1f} GB",
|
|
135
|
+
f"Insufficient space for copy operation. Required: {src_size / 1e9:.1f} GB, Available: {free_space / 1e9:.1f} GB",
|
|
139
136
|
required_space=src_size,
|
|
140
|
-
available_space=free_space
|
|
137
|
+
available_space=free_space,
|
|
141
138
|
)
|
|
142
|
-
|
|
139
|
+
|
|
143
140
|
# Fallback to copy+remove for cross-filesystem moves
|
|
144
141
|
logger.info(f"Copying file (cross-filesystem): {src_path} -> {dst_path}")
|
|
145
|
-
|
|
142
|
+
|
|
146
143
|
# Use a temporary name during copy for atomicity
|
|
147
|
-
temp_dst = dst_path.with_suffix(dst_path.suffix +
|
|
148
|
-
|
|
144
|
+
temp_dst = dst_path.with_suffix(dst_path.suffix + ".tmp")
|
|
145
|
+
|
|
149
146
|
try:
|
|
150
147
|
shutil.copy2(str(src_path), str(temp_dst))
|
|
151
148
|
temp_dst.rename(dst_path)
|
|
152
149
|
src_path.unlink() # Remove source only after successful copy
|
|
153
150
|
logger.debug(f"Copy+remove successful: {src_path} -> {dst_path}")
|
|
154
151
|
return True
|
|
155
|
-
|
|
152
|
+
|
|
156
153
|
except OSError as e:
|
|
157
154
|
# Clean up temp file on failure
|
|
158
155
|
if temp_dst.exists():
|
|
@@ -160,12 +157,10 @@ def atomic_move_with_fallback(src_path: Path, dst_path: Path) -> bool:
|
|
|
160
157
|
# Re-raise with better context
|
|
161
158
|
if e.errno == 28: # No space left on device
|
|
162
159
|
raise InsufficientStorageError(
|
|
163
|
-
f"No space left on device during copy: {e}",
|
|
164
|
-
required_space=src_path.stat().st_size,
|
|
165
|
-
available_space=shutil.disk_usage(dst_path.parent).free
|
|
160
|
+
f"No space left on device during copy: {e}", required_space=src_path.stat().st_size, available_space=shutil.disk_usage(dst_path.parent).free
|
|
166
161
|
)
|
|
167
162
|
raise
|
|
168
|
-
|
|
163
|
+
|
|
169
164
|
except Exception as e:
|
|
170
165
|
logger.error(f"Failed to move {src_path} -> {dst_path}: {e}")
|
|
171
166
|
raise
|
|
@@ -195,11 +190,11 @@ def _create_from_file(
|
|
|
195
190
|
video_dir: Path = VIDEO_DIR,
|
|
196
191
|
save: bool = True,
|
|
197
192
|
delete_source: bool = False,
|
|
198
|
-
**kwargs
|
|
193
|
+
**kwargs,
|
|
199
194
|
) -> "VideoFile":
|
|
200
195
|
"""
|
|
201
196
|
Creates a VideoFile instance from a given video file path with improved error handling.
|
|
202
|
-
|
|
197
|
+
|
|
203
198
|
Raises:
|
|
204
199
|
InsufficientStorageError: When not enough disk space
|
|
205
200
|
TranscodingError: When video transcoding fails
|
|
@@ -208,7 +203,7 @@ def _create_from_file(
|
|
|
208
203
|
"""
|
|
209
204
|
from endoreg_db.models.administration.center.center import Center
|
|
210
205
|
from endoreg_db.models.medical.hardware import EndoscopyProcessor
|
|
211
|
-
|
|
206
|
+
|
|
212
207
|
original_file_name = file_path.name
|
|
213
208
|
original_suffix = file_path.suffix
|
|
214
209
|
final_storage_path = None
|
|
@@ -223,23 +218,20 @@ def _create_from_file(
|
|
|
223
218
|
resolved_storage_root = _get_path(data_paths, "storage", storage_root_default)
|
|
224
219
|
storage_root = Path(resolved_storage_root)
|
|
225
220
|
storage_root.mkdir(parents=True, exist_ok=True)
|
|
226
|
-
|
|
221
|
+
|
|
227
222
|
# Check storage capacity before starting any work
|
|
228
223
|
check_storage_capacity(file_path, storage_root)
|
|
229
224
|
|
|
230
225
|
# 1. Transcode if necessary
|
|
231
226
|
logger.debug("Checking transcoding requirement for %s", file_path)
|
|
232
|
-
temp_transcode_dir = TMP_VIDEO_DIR /
|
|
227
|
+
temp_transcode_dir = TMP_VIDEO_DIR / "transcoding"
|
|
233
228
|
temp_transcode_dir.mkdir(parents=True, exist_ok=True)
|
|
234
|
-
|
|
229
|
+
|
|
235
230
|
# Use a unique name for the potential transcoded file
|
|
236
231
|
temp_transcoded_output_path = temp_transcode_dir / f"{uuid.uuid4()}{original_suffix}"
|
|
237
232
|
|
|
238
233
|
try:
|
|
239
|
-
transcoded_file_path = transcode_videofile_if_required(
|
|
240
|
-
input_path=file_path,
|
|
241
|
-
output_path=temp_transcoded_output_path
|
|
242
|
-
)
|
|
234
|
+
transcoded_file_path = transcode_videofile_if_required(input_path=file_path, output_path=temp_transcoded_output_path)
|
|
243
235
|
if transcoded_file_path is None:
|
|
244
236
|
raise TranscodingError(f"Transcoding check/process failed for {file_path}")
|
|
245
237
|
except Exception as e:
|
|
@@ -257,7 +249,7 @@ def _create_from_file(
|
|
|
257
249
|
if cls_model.check_hash_exists(video_hash=video_hash):
|
|
258
250
|
existing_video = cls_model.objects.get(video_hash=video_hash)
|
|
259
251
|
logger.warning("Video with hash %s already exists (UUID: %s)", video_hash, existing_video.uuid)
|
|
260
|
-
|
|
252
|
+
|
|
261
253
|
# Check if the existing video has a valid file
|
|
262
254
|
existing_raw_path = existing_video.get_raw_file_path()
|
|
263
255
|
if existing_video.has_raw and existing_raw_path and existing_raw_path.exists():
|
|
@@ -355,4 +347,4 @@ def _create_from_file(
|
|
|
355
347
|
if transcoded_file_path and transcoded_file_path != file_path and transcoded_file_path.exists():
|
|
356
348
|
logger.warning("Cleaning up orphaned transcoded file: %s", transcoded_file_path)
|
|
357
349
|
transcoded_file_path.unlink(missing_ok=True)
|
|
358
|
-
raise RuntimeError(f"Video processing failed: {e}") from e
|
|
350
|
+
raise RuntimeError(f"Video processing failed: {e}") from e
|