From 0b9773326d18c5870a1fefc5ccfd98bdad34aabd Mon Sep 17 00:00:00 2001 From: syamkumar Date: Mon, 14 Oct 2024 09:58:19 +0530 Subject: [PATCH 01/27] ADDITIONAL_PLUGS build arg (#2535) ADDITIONAL_PLUGS build arg --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e2aa44c12f..cccc03b16c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -102,6 +102,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} build-args: | APP_VERSION=${{ github.sha }} + ADDITIONAL_PLUGS=${{ secrets.ADDITIONAL_PLUGS }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max From a7fb6ea9e31e574bad795374bd8eda316f14884e Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:04:23 +0200 Subject: [PATCH 02/27] Modified the dummy data to support new cypress test (#2536) modified the dummy data --- data/dummy/facility.json | 502 +++++++++++++++++++++++++-------------- data/dummy/users.json | 42 ++++ 2 files changed, 372 insertions(+), 172 deletions(-) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index cea1986085..17d98574ff 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -12,14 +12,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], + "features": "[\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"]", "longitude": null, "latitude": null, "pincode": 670000, @@ -56,10 +49,7 @@ "verified": false, "facility_type": 1300, "kasp_empanelled": false, - "features": [ - 1, - 6 - ], + "features": "[\"1\", \"6\"]", "longitude": null, "latitude": null, "pincode": 670112, @@ -96,11 +86,7 @@ "verified": false, "facility_type": 1500, "kasp_empanelled": false, - "features": [ - 1, - 4, - 6 - ], + "features": "[\"1\", \"4\", \"6\"]", "longitude": "78.6757364624373000", "latitude": "21.4009146842158660", "pincode": 670000, @@ -137,11 +123,7 @@ "verified": false, "facility_type": 1510, "kasp_empanelled": false, - "features": [ - 1, - 3, - 5 - ], + "features": "[\"1\", \"3\", \"5\"]", "longitude": "75.2139014820876600", "latitude": "18.2774285038890340", "pincode": 670000, @@ -178,7 +160,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -215,7 +197,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -252,7 +234,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -289,7 +271,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -326,7 +308,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -363,7 +345,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -400,7 +382,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -437,7 +419,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -474,7 +456,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -511,7 +493,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -548,7 +530,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -585,7 +567,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -622,7 +604,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -659,7 +641,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -696,7 +678,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -733,7 +715,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -1055,6 +1037,15 @@ "created_by": 2 } }, + { + "model": "facility.facilityuser", + "pk": 25, + "fields": { + "facility": 1, + "user": 25, + "created_by": 2 + } + }, { "model": "facility.assetlocation", "pk": 1, @@ -1984,6 +1975,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-01T08:35:00Z", @@ -2011,7 +2003,8 @@ "weight": 0.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2043,6 +2036,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:33:03.700Z", @@ -2070,7 +2064,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2102,6 +2097,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:39:29.394Z", @@ -2129,7 +2125,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2161,6 +2158,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:10.532Z", @@ -2188,7 +2186,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2220,6 +2219,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:33.614Z", @@ -2247,7 +2247,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2279,6 +2280,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:56.180Z", @@ -2306,7 +2308,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2338,6 +2341,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:43:18.480Z", @@ -2365,7 +2369,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2397,6 +2402,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:43:41.540Z", @@ -2424,7 +2430,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2456,6 +2463,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:05.398Z", @@ -2483,7 +2491,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2515,6 +2524,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:28.550Z", @@ -2542,7 +2552,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2574,6 +2585,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:51.239Z", @@ -2601,7 +2613,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2633,6 +2646,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:45:13.721Z", @@ -2660,7 +2674,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2692,6 +2707,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:45:37.972Z", @@ -2719,7 +2735,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2751,6 +2768,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:00.645Z", @@ -2778,7 +2796,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2810,6 +2829,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:23.492Z", @@ -2837,7 +2857,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2869,6 +2890,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:46.028Z", @@ -2896,7 +2918,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2928,6 +2951,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:11.141Z", @@ -2955,7 +2979,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2987,6 +3012,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:34.395Z", @@ -3014,7 +3040,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3046,6 +3073,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-07T08:47:53.746Z", @@ -3073,7 +3101,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3105,6 +3134,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-15T08:47:53.746Z", @@ -3132,7 +3162,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3140,8 +3171,8 @@ "pk": 21, "fields": { "external_id": "40fa5cc6-6199-48cd-bc2a-dd9e73b920f9", - "created_date": "2024-1-30T08:47:53.746Z", - "modified_date": "2024-1-30T08:47:53.746Z", + "created_date": "2024-01-30T08:47:53.746Z", + "modified_date": "2024-01-30T08:47:53.746Z", "deleted": false, "patient": 18, "patient_no": "IP0010", @@ -3164,9 +3195,10 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, - "encounter_date": "2024-1-30T08:47:53.746Z", + "encounter_date": "2024-01-30T08:47:53.746Z", "icu_admission_date": null, "discharge_date": null, "discharge_reason": null, @@ -3191,7 +3223,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3199,8 +3232,8 @@ "pk": 22, "fields": { "external_id": "40faecc6-6199-48cd-bc2a-6d9e73b920f9", - "created_date": "2024-2-28T08:47:53.746Z", - "modified_date": "2024-2-28T08:47:53.746Z", + "created_date": "2024-02-28T08:47:53.746Z", + "modified_date": "2024-02-28T08:47:53.746Z", "deleted": false, "patient": 18, "patient_no": "IP011", @@ -3223,9 +3256,10 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, - "encounter_date": "2024-2-28T08:47:53.746Z", + "encounter_date": "2024-02-28T08:47:53.746Z", "icu_admission_date": null, "discharge_date": null, "discharge_reason": null, @@ -3250,7 +3284,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3282,6 +3317,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2024-04-01T08:47:53.746Z", @@ -3309,7 +3345,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3341,6 +3378,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2022-05-06T08:47:53.746Z", @@ -3368,7 +3406,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3400,6 +3439,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2022-02-06T08:47:53.746Z", @@ -3427,7 +3467,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3459,6 +3500,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:34.395Z", @@ -3486,7 +3528,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3585,6 +3628,54 @@ "location": 1 } }, + { + "model": "facility.bed", + "pk": 9, + "fields": { + "external_id": "a9ea05d2-4f58-4fc2-bc24-2979a3502dc4", + "created_date": "2024-10-14T07:56:52.641Z", + "modified_date": "2024-10-14T07:56:52.641Z", + "deleted": false, + "name": "Dummy Bed 7", + "description": "", + "bed_type": 2, + "facility": 1, + "meta": {}, + "location": 2 + } + }, + { + "model": "facility.bed", + "pk": 10, + "fields": { + "external_id": "aff5a088-c278-4075-9a4c-64453f50f216", + "created_date": "2024-10-14T07:56:59.896Z", + "modified_date": "2024-10-14T07:56:59.896Z", + "deleted": false, + "name": "Dummy Bed 8", + "description": "", + "bed_type": 6, + "facility": 1, + "meta": {}, + "location": 2 + } + }, + { + "model": "facility.bed", + "pk": 11, + "fields": { + "external_id": "882ce4de-4e2c-4afa-b3e6-29fceec30730", + "created_date": "2024-10-14T07:57:06.701Z", + "modified_date": "2024-10-14T07:57:06.701Z", + "deleted": false, + "name": "Dummy Bed 9", + "description": "", + "bed_type": 2, + "facility": 1, + "meta": {}, + "location": 2 + } + }, { "model": "facility.consultationdiagnosis", "pk": 1, @@ -3985,14 +4076,8 @@ "default_unit": 1, "description": "", "min_quantity": 150.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 1, - 2 - ] + "allowed_units": [1, 2], + "tags": [1, 2] } }, { @@ -4003,13 +4088,8 @@ "default_unit": 1, "description": "", "min_quantity": 2.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 2 - ] + "allowed_units": [1, 2], + "tags": [2] } }, { @@ -4020,12 +4100,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -4036,9 +4112,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4050,9 +4124,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4064,9 +4136,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4078,12 +4148,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -4107,8 +4173,10 @@ "pincode": 600115, "date_of_birth": "2005-10-16", "year_of_birth": 2005, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4123,6 +4191,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 5896, "local_body": 95, @@ -4186,8 +4256,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4202,6 +4274,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4265,8 +4339,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4281,6 +4357,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4344,8 +4422,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4360,6 +4440,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4423,8 +4505,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4439,6 +4523,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4502,8 +4588,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4518,6 +4606,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4581,8 +4671,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4597,6 +4689,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4660,8 +4754,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4676,6 +4772,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4739,8 +4837,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4755,6 +4855,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4818,8 +4920,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4834,6 +4938,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4897,8 +5003,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4913,6 +5021,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4976,8 +5086,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4992,6 +5104,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5055,8 +5169,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5071,6 +5187,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5134,8 +5252,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5150,6 +5270,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5213,8 +5335,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5229,6 +5353,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5292,8 +5418,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5308,6 +5436,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5371,8 +5501,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5387,6 +5519,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5450,8 +5584,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5466,6 +5602,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -7122,6 +7260,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7151,6 +7291,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7180,6 +7322,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7209,6 +7353,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7238,6 +7384,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7267,6 +7415,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7296,6 +7446,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7325,6 +7477,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7354,6 +7508,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7383,6 +7539,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7412,6 +7570,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7441,6 +7601,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7470,6 +7632,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7499,6 +7663,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7528,6 +7694,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7557,6 +7725,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7586,6 +7756,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7877,8 +8049,8 @@ "pk": 9, "fields": { "external_id": "09876543-210e-4567-a890-1234567890ab", - "created_date": "2023-12-06T09:00:00.000Z", - "modified_date": "2023-12-06T09:00:00.000Z", + "created_date": "2023-12-06T09:00:00Z", + "modified_date": "2023-12-06T09:00:00Z", "deleted": false, "origin_facility": 1, "shifting_approving_facility": 2, @@ -8008,40 +8180,6 @@ "last_edited_by": 1 } }, - { - "model": "facility.shiftingrequest", - "pk": 15, - "fields": { - "external_id": "98765432-10e2-40f1-a0b9-876543210abc", - "created_date": "2023-09-05T22:50:10.221Z", - "modified_date": "2023-12-04T14:30:45.501Z", - "deleted": false, - "origin_facility": 1, - "shifting_approving_facility": 2, - "assigned_facility_type": 2, - "assigned_facility": null, - "assigned_facility_external": null, - "patient": 15, - "emergency": true, - "is_up_shift": true, - "reason": "Test", - "vehicle_preference": "", - "preferred_vehicle_choice": 10, - "comments": "", - "refering_facility_contact_name": "Someone at Facility", - "refering_facility_contact_number": "+914455666777", - "is_kasp": false, - "status": 100, - "breathlessness_level": 30, - "is_assigned_to_user": false, - "assigned_to": null, - "ambulance_driver_name": "", - "ambulance_phone_number": "", - "ambulance_number": "", - "created_by": 2, - "last_edited_by": 2 - } - }, { "model": "facility.shiftingrequest", "pk": 14, @@ -8114,6 +8252,10 @@ "model": "facility.resourcerequest", "pk": 1, "fields": { + "external_id": "067ad8fc-2551-4267-8a09-6facaebf0e1f", + "created_date": "2023-09-05T22:50:10.221Z", + "modified_date": "2023-09-05T22:50:10.221Z", + "deleted": false, "origin_facility": 1, "approving_facility": 2, "assigned_facility": 3, @@ -8131,15 +8273,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 1, - "last_edited_by": 1, - "created_date": "2023-09-05T22:50:10.221Z", - "modified_date": "2023-09-05T22:50:10.221Z" + "last_edited_by": 1 } }, { "model": "facility.resourcerequest", "pk": 2, "fields": { + "external_id": "0c77c84a-3b91-419c-807c-65e53f11a74f", + "created_date": "2023-09-06T22:50:10.221Z", + "modified_date": "2023-09-06T22:50:10.221Z", + "deleted": false, "origin_facility": 1, "approving_facility": 5, "assigned_facility": 8, @@ -8157,15 +8301,17 @@ "is_assigned_to_user": true, "assigned_to": 3, "created_by": 2, - "last_edited_by": 2, - "created_date": "2023-09-06T22:50:10.221Z", - "modified_date": "2023-09-06T22:50:10.221Z" + "last_edited_by": 2 } }, { "model": "facility.resourcerequest", "pk": 3, "fields": { + "external_id": "b08312cc-c301-46c0-8347-c521cbae8c54", + "created_date": "2023-09-07T22:50:10.221Z", + "modified_date": "2023-09-07T22:50:10.221Z", + "deleted": false, "origin_facility": 7, "approving_facility": 2, "assigned_facility": 3, @@ -8183,15 +8329,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 3, - "last_edited_by": 3, - "created_date": "2023-09-07T22:50:10.221Z", - "modified_date": "2023-09-07T22:50:10.221Z" + "last_edited_by": 3 } }, { "model": "facility.resourcerequest", "pk": 4, "fields": { + "external_id": "e9664dcf-224f-4e1e-a003-c4ff0d3699d3", + "created_date": "2023-09-08T22:50:10.221Z", + "modified_date": "2023-09-08T22:50:10.221Z", + "deleted": false, "origin_facility": 9, "approving_facility": 1, "assigned_facility": 4, @@ -8209,15 +8357,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 4, - "last_edited_by": 4, - "created_date": "2023-09-08T22:50:10.221Z", - "modified_date": "2023-09-08T22:50:10.221Z" + "last_edited_by": 4 } }, { "model": "facility.resourcerequest", "pk": 5, "fields": { + "external_id": "ce300e4d-fae6-4ee4-8bf9-f70b98abd7be", + "created_date": "2023-09-09T22:50:10.221Z", + "modified_date": "2023-09-09T22:50:10.221Z", + "deleted": false, "origin_facility": 5, "approving_facility": 6, "assigned_facility": 7, @@ -8235,15 +8385,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 5, - "last_edited_by": 5, - "created_date": "2023-09-09T22:50:10.221Z", - "modified_date": "2023-09-09T22:50:10.221Z" + "last_edited_by": 5 } }, { "model": "facility.resourcerequest", "pk": 6, "fields": { + "external_id": "79b90b87-1cac-449b-95d2-656f0c543652", + "created_date": "2023-09-10T22:50:10.221Z", + "modified_date": "2023-09-10T22:50:10.221Z", + "deleted": false, "origin_facility": 12, "approving_facility": 9, "assigned_facility": 1, @@ -8261,15 +8413,17 @@ "is_assigned_to_user": true, "assigned_to": 6, "created_by": 6, - "last_edited_by": 6, - "created_date": "2023-09-10T22:50:10.221Z", - "modified_date": "2023-09-10T22:50:10.221Z" + "last_edited_by": 6 } }, { "model": "facility.resourcerequest", "pk": 7, "fields": { + "external_id": "fb5fcb52-8640-4a34-95f3-8adaa7c34f6d", + "created_date": "2023-09-11T22:50:10.221Z", + "modified_date": "2023-09-11T22:50:10.221Z", + "deleted": false, "origin_facility": 10, "approving_facility": 11, "assigned_facility": 9, @@ -8287,15 +8441,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 7, - "last_edited_by": 7, - "created_date": "2023-09-11T22:50:10.221Z", - "modified_date": "2023-09-11T22:50:10.221Z" + "last_edited_by": 7 } }, { "model": "facility.resourcerequest", "pk": 8, "fields": { + "external_id": "94ac6a65-cf45-4a01-aa5a-11238ae7dca5", + "created_date": "2023-09-12T22:50:10.221Z", + "modified_date": "2023-09-12T22:50:10.221Z", + "deleted": false, "origin_facility": 15, "approving_facility": 7, "assigned_facility": 6, @@ -8313,15 +8469,17 @@ "is_assigned_to_user": true, "assigned_to": 8, "created_by": 8, - "last_edited_by": 8, - "created_date": "2023-09-12T22:50:10.221Z", - "modified_date": "2023-09-12T22:50:10.221Z" + "last_edited_by": 8 } }, { "model": "facility.resourcerequest", "pk": 9, "fields": { + "external_id": "b8137245-82de-48d8-add1-132d8cf4458a", + "created_date": "2023-09-13T22:50:10.221Z", + "modified_date": "2023-09-13T22:50:10.221Z", + "deleted": false, "origin_facility": 3, "approving_facility": 9, "assigned_facility": 1, @@ -8339,15 +8497,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 9, - "last_edited_by": 9, - "created_date": "2023-09-13T22:50:10.221Z", - "modified_date": "2023-09-13T22:50:10.221Z" + "last_edited_by": 9 } }, { "model": "facility.resourcerequest", "pk": 10, "fields": { + "external_id": "996d4aa0-3694-4b05-bb75-53a194d65158", + "created_date": "2023-09-14T22:50:10.221Z", + "modified_date": "2023-09-14T22:50:10.221Z", + "deleted": false, "origin_facility": 10, "approving_facility": 1, "assigned_facility": 5, @@ -8365,9 +8525,7 @@ "is_assigned_to_user": true, "assigned_to": 10, "created_by": 10, - "last_edited_by": 10, - "created_date": "2023-09-14T22:50:10.221Z", - "modified_date": "2023-09-14T22:50:10.221Z" + "last_edited_by": 10 } } ] diff --git a/data/dummy/users.json b/data/dummy/users.json index fc55f68670..e7b0115614 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -886,5 +886,47 @@ "groups": [], "user_permissions": [] } + }, + { + "model": "users.user", + "pk": 25, + "fields": { + "password": "argon2$argon2id$v=19$m=102400,t=2,p=8$bUNTR1MwejJYNXdXd2VUYjJHMmN5bw$alS6S9Ay3bvIHe9U18luyn7LyVaArgrgHIt+vh4ta48", + "last_login": null, + "is_superuser": false, + "first_name": "Dev", + "last_name": "Doctor Two", + "email": "devdoctor1@test.com", + "is_staff": false, + "is_active": true, + "date_joined": "2024-10-14T07:53:32.400Z", + "external_id": "009c4fc2-f7af-4a02-9383-6fbb4af2fdbb", + "username": "devdoctor1", + "user_type": 15, + "created_by": 2, + "ward": null, + "local_body": null, + "district": 7, + "state": 1, + "phone_number": "+917644536346", + "alt_phone_number": "+917644536346", + "video_connect_link": null, + "gender": 1, + "date_of_birth": "2005-01-01", + "profile_picture_url": null, + "home_facility": null, + "weekly_working_hours": null, + "qualification": "MBBS", + "doctor_experience_commenced_on": "2020-10-14", + "doctor_medical_council_registration": "23532093", + "verified": true, + "deleted": false, + "pf_endpoint": null, + "pf_p256dh": null, + "pf_auth": null, + "asset": null, + "groups": [], + "user_permissions": [] + } } ] From 02c90560eb936eb5610344afe1ccc39fff6064b6 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 14 Oct 2024 19:05:34 +0530 Subject: [PATCH 03/27] load additional plugs on manager startup (#2537) * load additional plugs on manager startup * cleanup --- install_plugins.py | 15 --------------- plugs/manager.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/install_plugins.py b/install_plugins.py index b320f0caaf..8324ff795b 100644 --- a/install_plugins.py +++ b/install_plugins.py @@ -1,18 +1,3 @@ -import json -import logging -import os - from plug_config import manager -from plugs.plug import Plug - -logger = logging.getLogger(__name__) - - -if ADDITIONAL_PLUGS := os.getenv("ADDITIONAL_PLUGS"): - try: - for plug in json.loads(ADDITIONAL_PLUGS): - manager.add_plug(Plug(**plug)) - except json.JSONDecodeError: - logger.error("ADDITIONAL_PLUGS is not a valid JSON") manager.install() diff --git a/plugs/manager.py b/plugs/manager.py index dfdac9fa93..2e4516ebb6 100644 --- a/plugs/manager.py +++ b/plugs/manager.py @@ -1,9 +1,14 @@ +import json +import logging +import os import subprocess import sys from collections import defaultdict from plugs.plug import Plug +logger = logging.getLogger(__name__) + class PlugManager: """ @@ -13,6 +18,14 @@ class PlugManager: def __init__(self, plugs: list[Plug]): self.plugs: list[Plug] = plugs + # load additional plugs from environment variable + if additional_plugs := os.getenv("ADDITIONAL_PLUGS"): + try: + for plug in json.loads(additional_plugs): + self.add_plug(Plug(**plug)) + except json.JSONDecodeError: + logger.error("ADDITIONAL_PLUGS is not a valid JSON") + def install(self) -> None: packages: list[str] = [f"{x.package_name}{x.version}" for x in self.plugs] if packages: From e440d463fedac40c042bbd5e7db76b127b995326 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 15 Oct 2024 12:19:40 +0530 Subject: [PATCH 04/27] Adds route to fetch facility hubs (#2520) * Sending hub and spoke data in facility serializer * remove unwanted changes * Added /hubs route to list hubs * Update care/facility/api/viewsets/facility.py Co-authored-by: Aakash Singh --------- Co-authored-by: Aakash Singh --- care/facility/api/viewsets/facility.py | 16 ++++++++++++++++ care/facility/tests/test_facility_api.py | 19 +++++++++++++++++++ config/api_router.py | 2 ++ 3 files changed, 37 insertions(+) diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index dd8221c0cb..1f0ac69442 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -204,3 +204,19 @@ def get_serializer_context(self): context = super().get_serializer_context() context["facility"] = facility return context + + +class FacilityHubsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = FacilityHubSpoke.objects.all().select_related("spoke", "hub") + serializer_class = FacilitySpokeSerializer + permission_classes = (IsAuthenticated,) + lookup_field = "external_id" + + def get_queryset(self): + return self.queryset.filter(spoke=self.get_facility()) + + def get_facility(self): + facilities = get_facility_queryset(self.request.user) + return get_object_or_404( + facilities.filter(external_id=self.kwargs["facility_external_id"]) + ) diff --git a/care/facility/tests/test_facility_api.py b/care/facility/tests/test_facility_api.py index 800f45fb8e..b86cb0db86 100644 --- a/care/facility/tests/test_facility_api.py +++ b/care/facility/tests/test_facility_api.py @@ -229,6 +229,25 @@ def test_spoke_is_not_ancestor(self): ) self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_hubs_list(self): + facility_a = self.create_facility( + self.super_user, self.district, self.local_body + ) + facility_b = self.create_facility( + self.super_user, self.district, self.local_body + ) + + FacilityHubSpoke.objects.create(hub=facility_a, spoke=facility_b) + + self.client.force_authenticate(user=self.super_user) + response = self.client.get(f"/api/v1/facility/{facility_b.external_id}/hubs/") + self.assertIs(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["count"], 1) + self.assertEqual( + data["results"][0]["hub_object"]["id"], str(facility_a.external_id) + ) + class FacilityCoverImageTests(TestUtils, APITestCase): @classmethod diff --git a/config/api_router.py b/config/api_router.py index 917b187395..a2e3de78c4 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -37,6 +37,7 @@ ) from care.facility.api.viewsets.facility import ( AllFacilityViewSet, + FacilityHubsViewSet, FacilitySpokesViewSet, FacilityViewSet, ) @@ -218,6 +219,7 @@ facility_nested_router.register( r"spokes", FacilitySpokesViewSet, basename="facility-spokes" ) +facility_nested_router.register(r"hubs", FacilityHubsViewSet, basename="facility-hubs") router.register("asset", AssetViewSet, basename="asset") asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") From 7bdedf48f5c143933dd9268d132021798602f3b4 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Tue, 15 Oct 2024 20:50:34 +0530 Subject: [PATCH 05/27] disable filtering inactive users by default (#2529) Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/users/admin.py | 7 +++++++ care/users/models.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/care/users/admin.py b/care/users/admin.py index ba40369025..3f0a7f9f5c 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -58,6 +58,13 @@ class UserAdmin(auth_admin.UserAdmin, ExportCsvMixin): list_display = ["username", "is_superuser"] search_fields = ["first_name", "last_name"] + def get_queryset(self, request): + # use the base manager to avoid filtering out soft deleted objects + qs = self.model._base_manager.get_queryset() # noqa: SLF001 + if ordering := self.get_ordering(request): + qs = qs.order_by(*ordering) + return qs + @admin.register(State) class StateAdmin(admin.ModelAdmin): diff --git a/care/users/models.py b/care/users/models.py index 0d66392279..5f214871e0 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -130,7 +130,7 @@ def __str__(self): class CustomUserManager(UserManager): def get_queryset(self): qs = super().get_queryset() - return qs.filter(deleted=False, is_active=True).select_related( + return qs.filter(deleted=False).select_related( "local_body", "district", "state" ) From 0fc526031a74aef09c7af2dcc48d348b6c0c187e Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Tue, 15 Oct 2024 23:59:38 +0530 Subject: [PATCH 06/27] disable codecov annotations (#2542) --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..e00ce3d698 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +github_checks: + annotations: false From 14d3ef948146951d4b02ef049165bf5fa8e33ff8 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 18 Oct 2024 18:03:12 +0530 Subject: [PATCH 07/27] Improved asset bed relations for camera preset (#2387) * Adds camera preset model * Migration to backfill and soft delete duplicate asset bed records * Delete assed bed records that has no asset class * rebase migrations * stash * rebase migrations * rebase migrations and fix issues * fix accidentally creating preset in update preset * remove boundary preset support * optimize preset name valdiation check --------- Co-authored-by: Aakash Singh * refactor viewsets * make asset, bed, assetbed get_queryset reusable based on user * prevent accidentally attempting to evaluate queryset early * migration: skip purging data, handle exceptions; add tests --------- Co-authored-by: Aakash Singh Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- care/facility/api/serializers/bed.py | 13 +- .../facility/api/serializers/camera_preset.py | 49 +++++ care/facility/api/viewsets/asset.py | 17 +- care/facility/api/viewsets/bed.py | 48 +---- care/facility/api/viewsets/camera_preset.py | 63 ++++++ .../migrations/0466_camera_presets.py | 169 ++++++++++++++++ care/facility/models/__init__.py | 1 + care/facility/models/bed.py | 13 ++ care/facility/models/camera_preset.py | 33 ++++ care/facility/tests/test_asset_bed_api.py | 183 ++++++++++++++++++ care/utils/queryset/asset_bed.py | 47 +++++ care/utils/tests/test_utils.py | 8 +- config/api_router.py | 18 ++ 13 files changed, 600 insertions(+), 62 deletions(-) create mode 100644 care/facility/api/serializers/camera_preset.py create mode 100644 care/facility/api/viewsets/camera_preset.py create mode 100644 care/facility/migrations/0466_camera_presets.py create mode 100644 care/facility/models/camera_preset.py create mode 100644 care/facility/tests/test_asset_bed_api.py create mode 100644 care/utils/queryset/asset_bed.py diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 41597e186d..508c2f9619 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -123,11 +123,14 @@ def validate(self, attrs): {"asset": "Should be in the same facility as the bed"} ) if ( - asset.asset_class == AssetClasses.HL7MONITOR.name - and AssetBed.objects.filter( - bed=bed, asset__asset_class=asset.asset_class - ).exists() - ): + asset.asset_class + in [ + AssetClasses.HL7MONITOR.name, + AssetClasses.ONVIF.name, + ] + ) and AssetBed.objects.filter( + bed=bed, asset__asset_class=asset.asset_class + ).exists(): raise ValidationError( { "asset": "Bed is already in use by another asset of the same class" diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py new file mode 100644 index 0000000000..7157b5245a --- /dev/null +++ b/care/facility/api/serializers/camera_preset.py @@ -0,0 +1,49 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from care.facility.api.serializers.bed import AssetBedSerializer +from care.facility.models import CameraPreset +from care.users.api.serializers.user import UserBaseMinimumSerializer + + +class CameraPresetSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + created_by = UserBaseMinimumSerializer(read_only=True) + updated_by = UserBaseMinimumSerializer(read_only=True) + asset_bed = AssetBedSerializer(read_only=True) + + class Meta: + model = CameraPreset + exclude = ( + "external_id", + "deleted", + ) + read_only_fields = ( + "created_date", + "modified_date", + "is_migrated", + "created_by", + "updated_by", + ) + + def get_asset_bed_obj(self): + return ( + self.instance.asset_bed if self.instance else self.context.get("asset_bed") + ) + + def validate_name(self, value): + if CameraPreset.objects.filter( + asset_bed__bed_id=self.get_asset_bed_obj().bed_id, name=value + ).exists(): + msg = "Name should be unique. Another preset related to this bed already uses the same name." + raise ValidationError(msg) + return value + + def create(self, validated_data): + validated_data["created_by"] = self.context["request"].user + validated_data["asset_bed"] = self.get_asset_bed_obj() + return super().create(validated_data) + + def update(self, instance, validated_data): + validated_data["updated_by"] = self.context["request"].user + return super().update(instance, validated_data) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 15dd00e2aa..fc66eff4bf 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -62,6 +62,7 @@ from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_queryset from care.utils.queryset.asset_location import get_asset_location_queryset from care.utils.queryset.facility import get_facility_queryset from config.authentication import MiddlewareAuthentication @@ -290,21 +291,7 @@ class AssetViewSet( filterset_class = AssetFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(current_location__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter( - current_location__facility__district=user.district - ) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter( - current_location__facility__id__in=allowed_facilities - ) + queryset = get_asset_queryset(user=self.request.user, queryset=self.queryset) return queryset.annotate( latest_status=Subquery( AvailabilityRecord.objects.filter( diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 336b5f83c2..db9dd6652f 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -30,6 +30,7 @@ from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_bed_queryset, get_bed_queryset inverse_bed_type = inverse_choices(BedTypeChoices) @@ -76,27 +77,14 @@ class BedViewSet( filterset_class = BedFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - - queryset = queryset.annotate( + queryset = self.queryset.annotate( is_occupied=Exists( ConsultationBed.objects.filter( bed__id=OuterRef("id"), end_date__isnull=True ) ) ) - - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(facility__id__in=allowed_facilities) - return queryset + return get_bed_queryset(user=self.request.user, queryset=queryset) @transaction.atomic def create(self, request, *args, **kwargs): @@ -168,18 +156,7 @@ class AssetBedViewSet( lookup_field = "external_id" def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset + return get_asset_bed_queryset(user=self.request.user, queryset=self.queryset) class PatientAssetBedFilter(filters.FilterSet): @@ -212,20 +189,9 @@ class PatientAssetBedViewSet(ListModelMixin, GenericViewSet): ] def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset.filter( - bed__facility__external_id=self.kwargs["facility_external_id"] - ) + return get_asset_bed_queryset( + user=self.request.user, queryset=self.queryset + ).filter(bed__facility__external_id=self.kwargs["facility_external_id"]) class ConsultationBedFilter(filters.FilterSet): diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py new file mode 100644 index 0000000000..bfb168834b --- /dev/null +++ b/care/facility/api/viewsets/camera_preset.py @@ -0,0 +1,63 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.exceptions import NotFound +from rest_framework.mixins import ListModelMixin +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from care.facility.api.serializers.camera_preset import CameraPresetSerializer +from care.facility.models import CameraPreset +from care.utils.queryset.asset_bed import ( + get_asset_bed_queryset, + get_asset_queryset, + get_bed_queryset, +) + + +class AssetBedCameraPresetViewSet(ModelViewSet): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) + lookup_field = "external_id" + permission_classes = (IsAuthenticated,) + + def get_asset_bed_obj(self): + queryset = get_asset_bed_queryset(self.request.user).filter( + external_id=self.kwargs["assetbed_external_id"] + ) + return get_object_or_404(queryset) + + def get_queryset(self): + return super().get_queryset().filter(asset_bed=self.get_asset_bed_obj()) + + def get_serializer_context(self): + context = super().get_serializer_context() + context["asset_bed"] = self.get_asset_bed_obj() + return context + + +class CameraPresetViewSet(GenericViewSet, ListModelMixin): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) + lookup_field = "external_id" + permission_classes = (IsAuthenticated,) + + def get_bed_obj(self, external_id: str): + queryset = get_bed_queryset(self.request.user).filter(external_id=external_id) + return get_object_or_404(queryset) + + def get_asset_obj(self, external_id: str): + queryset = get_asset_queryset(self.request.user).filter(external_id=external_id) + return get_object_or_404(queryset) + + def get_queryset(self): + queryset = super().get_queryset() + if asset_external_id := self.kwargs.get("asset_external_id"): + return queryset.filter( + asset_bed__asset=self.get_asset_obj(asset_external_id) + ) + if bed_external_id := self.kwargs.get("bed_external_id"): + return queryset.filter(asset_bed__bed=self.get_bed_obj(bed_external_id)) + raise NotFound diff --git a/care/facility/migrations/0466_camera_presets.py b/care/facility/migrations/0466_camera_presets.py new file mode 100644 index 0000000000..8ee6942342 --- /dev/null +++ b/care/facility/migrations/0466_camera_presets.py @@ -0,0 +1,169 @@ +# Generated by Django 4.2.8 on 2024-05-30 06:56 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.core.paginator import Paginator +from django.db import migrations, models +from django.db.models import F, Window +from django.db.models.functions import RowNumber + +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0465_merge_20240923_1045"), + ] + + def delete_asset_beds_without_asset_class(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + AssetBed.objects.filter(asset__asset_class__isnull=True).delete() + + def backfill_camera_presets(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + CameraPreset = apps.get_model("facility", "CameraPreset") + + paginator = Paginator( + AssetBed.objects.annotate( + row_number=Window( + expression=RowNumber(), + partition_by=[F("asset"), F("bed")], + order_by=F("id").asc(), + ) + ) + .filter(deleted=False, asset__asset_class="ONVIF") + .order_by("asset", "bed", "id"), + 1000, + ) + + for page_number in paginator.page_range: + assetbeds_to_delete = [] + presets_to_create = [] + + for asset_bed in paginator.page(page_number).object_list: + name = asset_bed.meta.get("preset_name") + + if position := asset_bed.meta.get("position"): + try: + presets_to_create.append( + CameraPreset( + name=name, + asset_bed=AssetBed.objects.filter( + asset=asset_bed.asset, bed=asset_bed.bed + ).order_by("id")[0], + position={ + "x": float(position["x"]), + "y": float(position["y"]), + "zoom": float(position["zoom"]), + }, + is_migrated=True, + ) + ) + except: + pass + if asset_bed.row_number != 1: + assetbeds_to_delete.append(asset_bed.id) + else: + assetbeds_to_delete.append(asset_bed.id) + + CameraPreset.objects.bulk_create(presets_to_create) + AssetBed.objects.filter(id__in=assetbeds_to_delete).update(deleted=True) + + operations = [ + migrations.CreateModel( + name="CameraPreset", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("name", models.CharField(max_length=255, null=True)), + ( + "position", + models.JSONField( + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": False, + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "zoom": {"type": "number"}, + }, + "required": ["x", "y", "zoom"], + "type": "object", + } + ) + ], + ), + ), + ("is_migrated", models.BooleanField(default=False)), + ( + "asset_bed", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="camera_presets", + to="facility.assetbed", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.RunPython( + delete_asset_beds_without_asset_class, + migrations.RunPython.noop, + ), + migrations.RunPython( + backfill_camera_presets, + migrations.RunPython.noop, + ), + migrations.AddConstraint( + model_name="assetbed", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("asset", "bed"), + name="unique_together_asset_bed", + ), + ), + ] diff --git a/care/facility/models/__init__.py b/care/facility/models/__init__.py index df41476768..d6d63cacca 100644 --- a/care/facility/models/__init__.py +++ b/care/facility/models/__init__.py @@ -4,6 +4,7 @@ from .ambulance import * # noqa from .asset import * # noqa from .bed import * # noqa +from .camera_preset import * # noqa from .daily_round import * # noqa from .encounter_symptom import * # noqa from .events import * # noqa diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index a06db2729c..992f36ac74 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -68,9 +68,22 @@ class AssetBed(BaseModel): bed = models.ForeignKey(Bed, on_delete=models.PROTECT, null=False, blank=False) meta = JSONField(default=dict, blank=True) + class Meta: + constraints = [ + models.UniqueConstraint( + name="unique_together_asset_bed", + fields=("asset", "bed"), + condition=models.Q(deleted=False), + ), + ] + def __str__(self): return f"{self.asset.name} - {self.bed.name}" + def delete(self, *args): + self.camera_presets.update(deleted=True) + return super().delete(*args) + class ConsultationBed(BaseModel): consultation = models.ForeignKey( diff --git a/care/facility/models/camera_preset.py b/care/facility/models/camera_preset.py new file mode 100644 index 0000000000..b1128f8817 --- /dev/null +++ b/care/facility/models/camera_preset.py @@ -0,0 +1,33 @@ +from django.db import models + +from care.utils.models.base import BaseModel +from care.utils.models.validators import JSONFieldSchemaValidator + +CAMERA_PRESET_POSITION_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "zoom": {"type": "number"}, + }, + "required": ["x", "y", "zoom"], + "additionalProperties": False, +} + + +class CameraPreset(BaseModel): + name = models.CharField(max_length=255, null=True) + asset_bed = models.ForeignKey( + "facility.AssetBed", on_delete=models.PROTECT, related_name="camera_presets" + ) + position = models.JSONField( + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)] + ) + created_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + updated_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + is_migrated = models.BooleanField(default=False) diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py new file mode 100644 index 0000000000..d22aae9bfd --- /dev/null +++ b/care/facility/tests/test_asset_bed_api.py @@ -0,0 +1,183 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.users.models import User +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.tests.test_utils import TestUtils + + +class AssetBedViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset = cls.create_asset(cls.asset_location) + cls.camera_asset = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + + def test_link_disallowed_asset_class_asset_to_bed(self): + data = { + "asset": self.asset.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_link_asset_to_bed_and_attempt_duplicate_linking(self): + data = { + "asset": self.camera_asset.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + # Attempt linking same camera to the same bed again. + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + # List asset beds filtered by asset and bed ID and check only 1 result exists + res = self.client.get("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.data["count"], 1) + + +class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.asset2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + cls.asset_bed1 = cls.create_asset_bed(cls.asset1, cls.bed) + cls.asset_bed2 = cls.create_asset_bed(cls.asset2, cls.bed) + + def get_base_url(self, asset_bed_id=None): + return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" + + def test_create_camera_preset_without_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset without position", + "position": {}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_missing_required_keys_in_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": {"key": "value"}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_not_number(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "not a number", + "y": 1, + "zoom": 1, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_values_as_string(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "1", + "y": "1", + "zoom": "1", + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): + asset_bed = self.asset_bed1 + res = self.client.post( + self.get_base_url(asset_bed.external_id), + { + "name": "Preset with proper position", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + preset_external_id = res.data["id"] + + # Check if preset in asset-bed preset list + res = self.client.get(self.get_base_url(asset_bed.external_id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in asset preset list + res = self.client.get( + f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in bed preset list + res = self.client.get( + f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + def test_create_camera_preset_with_same_name_in_same_bed(self): + data = { + "name": "Duplicate Preset Name", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + } + self.client.post( + self.get_base_url(self.asset_bed1.external_id), data, format="json" + ) + res = self.client.post( + self.get_base_url(self.asset_bed2.external_id), data, format="json" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/utils/queryset/asset_bed.py b/care/utils/queryset/asset_bed.py new file mode 100644 index 0000000000..f9fe8f925e --- /dev/null +++ b/care/utils/queryset/asset_bed.py @@ -0,0 +1,47 @@ +from care.facility.models import Asset, AssetBed, Bed +from care.users.models import User +from care.utils.cache.cache_allowed_facilities import get_accessible_facilities + + +def get_asset_bed_queryset(user, queryset=None): + queryset = AssetBed.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(bed__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(bed__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(bed__facility__id__in=allowed_facilities) + return queryset + + +def get_bed_queryset(user, queryset=None): + queryset = Bed.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(facility__id__in=allowed_facilities) + return queryset + + +def get_asset_queryset(user, queryset=None): + queryset = Asset.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(current_location__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(current_location__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter( + current_location__facility__id__in=allowed_facilities + ) + return queryset diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 1f858c7258..91d4ac8d67 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -38,7 +38,7 @@ Ward, ) from care.facility.models.asset import Asset, AssetLocation -from care.facility.models.bed import Bed, ConsultationBed +from care.facility.models.bed import AssetBed, Bed, ConsultationBed from care.facility.models.facility import FacilityUser from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -446,6 +446,12 @@ def create_bed(cls, facility: Facility, location: AssetLocation, **kwargs): data.update(kwargs) return Bed.objects.create(**data) + @classmethod + def create_asset_bed(cls, asset: Asset, bed: Bed, **kwargs): + data = {"asset": asset, "bed": bed} + data.update(kwargs) + return AssetBed.objects.create(**data) + @classmethod def create_consultation_bed( cls, diff --git a/config/api_router.py b/config/api_router.py index a2e3de78c4..b4a3aa0f4a 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -26,6 +26,10 @@ ConsultationBedViewSet, PatientAssetBedViewSet, ) +from care.facility.api.viewsets.camera_preset import ( + AssetBedCameraPresetViewSet, + CameraPresetViewSet, +) from care.facility.api.viewsets.consultation_diagnosis import ( ConsultationDiagnosisViewSet, ) @@ -223,6 +227,9 @@ router.register("asset", AssetViewSet, basename="asset") asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") +asset_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="asset-camera-presets" +) asset_nested_router.register( r"availability", AvailabilityViewSet, basename="asset-availability" ) @@ -234,8 +241,17 @@ router.register("asset_transaction", AssetTransactionViewSet) router.register("bed", BedViewSet, basename="bed") +bed_nested_router = NestedSimpleRouter(router, r"bed", lookup="bed") +bed_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="bed-camera-presets" +) + router.register("assetbed", AssetBedViewSet, basename="asset-bed") router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") +assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") +assetbed_nested_router.register( + r"camera_presets", AssetBedCameraPresetViewSet, basename="assetbed-camera-presets" +) router.register("patient/search", PatientSearchViewSet, basename="patient-search") router.register("patient", PatientViewSet, basename="patient") @@ -329,6 +345,8 @@ path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), path("", include(asset_nested_router.urls)), + path("", include(bed_nested_router.urls)), + path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), path("", include(consultation_nested_router.urls)), From 0677656a93ad530d16bfc42b3660f7a04c34e0b5 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:12:34 +0530 Subject: [PATCH 08/27] Convert ABDM into Plug - Part 2/3 (#2313) Convert ABDM into Plug - Part 2/3 (#2313) --------- Co-authored-by: Aakash Singh Co-authored-by: Khavin Shankar --- care/abdm/migrations/0014_replace_0013.py | 30 +++++++++++++++++++ .../0013_abhanumber_patient.py | 4 +-- ...atientregistration_abha_number_and_more.py | 12 ++------ ...atientregistration_abha_number_and_more.py | 2 +- 4 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 care/abdm/migrations/0014_replace_0013.py rename care/abdm/{migrations => migrations_old}/0013_abhanumber_patient.py (89%) diff --git a/care/abdm/migrations/0014_replace_0013.py b/care/abdm/migrations/0014_replace_0013.py new file mode 100644 index 0000000000..1c30a4e0a7 --- /dev/null +++ b/care/abdm/migrations/0014_replace_0013.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.10 on 2024-04-21 17:40 + +# This is a replacement migration for abdm.0013 that omits the RunPython operation (reverse_patient_abhanumber_relation) and facility migration dependency. + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("abdm", "0012_consentrequest_status"), + ] + + replaces = [ + ("abdm", "0013_abhanumber_patient"), + ] + + operations = [ + migrations.AddField( + model_name="abhanumber", + name="patient", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="abha_number", + to="facility.patientregistration", + ), + ), + ] diff --git a/care/abdm/migrations/0013_abhanumber_patient.py b/care/abdm/migrations_old/0013_abhanumber_patient.py similarity index 89% rename from care/abdm/migrations/0013_abhanumber_patient.py rename to care/abdm/migrations_old/0013_abhanumber_patient.py index 41d3854797..433dea8576 100644 --- a/care/abdm/migrations/0013_abhanumber_patient.py +++ b/care/abdm/migrations_old/0013_abhanumber_patient.py @@ -11,9 +11,7 @@ def reverse_patient_abhanumber_relation(apps, schema_editor): AbhaNumber = apps.get_model("abdm", "AbhaNumber") patients = ( - Patient.objects.annotate(removed_field=RawSQL("abha_number_id", ())) - .filter(abha_number__isnull=False) - .select_related("abha_number") + Patient.objects.filter(abha_number__isnull=False) ) abha_numbers_to_update = [] diff --git a/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py index 304bebb163..194458e274 100644 --- a/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py +++ b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), + # ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), ("facility", "0373_remove_patientconsultation_hba1c"), ] @@ -22,23 +22,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name="historicalpatientregistration", name="abha_number", - field=models.ForeignKey( + field=models.IntegerField( blank=True, - db_constraint=False, null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to="abdm.abhanumber", ), ), migrations.AddField( model_name="patientregistration", name="abha_number", - field=models.OneToOneField( + field=models.IntegerField( blank=True, null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="abdm.abhanumber", ), ), ] diff --git a/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py b/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py index 4dc7fd5054..9e10ce90d1 100644 --- a/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py +++ b/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ ("facility", "0453_merge_20240824_2040"), - ("abdm", "0013_abhanumber_patient"), + # ("abdm", "0013_abhanumber_patient"), ] operations = [ From e9303a077989060ad0e87371d3120e94c021b0cb Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:13:12 +0530 Subject: [PATCH 09/27] Convert ABDM into Plug - Part 3/3 (#2312) Convert ABDM into Plug - Part 3/3 (#2312) --------- Co-authored-by: Aakash Singh Co-authored-by: Khavin Shankar Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- .env.example | 2 - aws/backend.json | 16 +- aws/celery.json | 42 +- care/abdm/__init__.py | 0 care/abdm/admin.py | 1 - care/abdm/api/__init__.py | 0 care/abdm/api/serializers/abha_number.py | 20 - care/abdm/api/serializers/auth.py | 24 - care/abdm/api/serializers/consent.py | 33 - care/abdm/api/serializers/health_facility.py | 12 - care/abdm/api/serializers/healthid.py | 66 - care/abdm/api/serializers/hip.py | 33 - care/abdm/api/viewsets/abha_number.py | 50 - care/abdm/api/viewsets/auth.py | 341 ----- care/abdm/api/viewsets/consent.py | 260 ---- care/abdm/api/viewsets/health_facility.py | 117 -- care/abdm/api/viewsets/health_information.py | 144 -- care/abdm/api/viewsets/healthid.py | 765 ----------- care/abdm/api/viewsets/hip.py | 146 -- care/abdm/api/viewsets/monitoring.py | 22 - care/abdm/api/viewsets/patients.py | 77 -- care/abdm/api/viewsets/status.py | 33 - care/abdm/apps.py | 7 - ...itial_squashed_0007_alter_abhanumber_id.py | 69 - care/abdm/migrations/0008_abhanumber_new.py | 17 - care/abdm/migrations/0009_healthfacility.py | 55 - .../0010_healthfacility_registered.py | 17 - ...1_alter_abhanumber_abha_number_and_more.py | 429 ------ .../migrations/0012_consentrequest_status.py | 27 - care/abdm/migrations/__init__.py | 0 care/abdm/models/__init__.py | 3 - care/abdm/models/abha_number.py | 41 - care/abdm/models/base.py | 43 - care/abdm/models/consent.py | 160 --- care/abdm/models/health_facility.py | 15 - care/abdm/models/json_schema.py | 15 - .../models/permissions/health_facility.py | 29 - care/abdm/receivers/consultation.py | 27 - care/abdm/service/gateway.py | 215 --- care/abdm/service/request.py | 106 -- care/abdm/tests.py | 3 - care/abdm/urls.py | 141 -- care/abdm/utils/api_call.py | 812 ----------- care/abdm/utils/cipher.py | 95 -- care/abdm/utils/fhir.py | 1220 ----------------- care/abdm/views.py | 1 - .../management/commands/load_dummy_data.py | 25 + config/api_router.py | 23 - config/authentication.py | 53 - config/settings/base.py | 16 - config/urls.py | 6 +- docker-compose.yaml | 6 - docker/.local.env | 8 + docker/dev.Dockerfile | 2 +- plug_config.py | 9 +- 55 files changed, 60 insertions(+), 5839 deletions(-) delete mode 100644 care/abdm/__init__.py delete mode 100644 care/abdm/admin.py delete mode 100644 care/abdm/api/__init__.py delete mode 100644 care/abdm/api/serializers/abha_number.py delete mode 100644 care/abdm/api/serializers/auth.py delete mode 100644 care/abdm/api/serializers/consent.py delete mode 100644 care/abdm/api/serializers/health_facility.py delete mode 100644 care/abdm/api/serializers/healthid.py delete mode 100644 care/abdm/api/serializers/hip.py delete mode 100644 care/abdm/api/viewsets/abha_number.py delete mode 100644 care/abdm/api/viewsets/auth.py delete mode 100644 care/abdm/api/viewsets/consent.py delete mode 100644 care/abdm/api/viewsets/health_facility.py delete mode 100644 care/abdm/api/viewsets/health_information.py delete mode 100644 care/abdm/api/viewsets/healthid.py delete mode 100644 care/abdm/api/viewsets/hip.py delete mode 100644 care/abdm/api/viewsets/monitoring.py delete mode 100644 care/abdm/api/viewsets/patients.py delete mode 100644 care/abdm/api/viewsets/status.py delete mode 100644 care/abdm/apps.py delete mode 100644 care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py delete mode 100644 care/abdm/migrations/0008_abhanumber_new.py delete mode 100644 care/abdm/migrations/0009_healthfacility.py delete mode 100644 care/abdm/migrations/0010_healthfacility_registered.py delete mode 100644 care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py delete mode 100644 care/abdm/migrations/0012_consentrequest_status.py delete mode 100644 care/abdm/migrations/__init__.py delete mode 100644 care/abdm/models/__init__.py delete mode 100644 care/abdm/models/abha_number.py delete mode 100644 care/abdm/models/base.py delete mode 100644 care/abdm/models/consent.py delete mode 100644 care/abdm/models/health_facility.py delete mode 100644 care/abdm/models/json_schema.py delete mode 100644 care/abdm/models/permissions/health_facility.py delete mode 100644 care/abdm/receivers/consultation.py delete mode 100644 care/abdm/service/gateway.py delete mode 100644 care/abdm/service/request.py delete mode 100644 care/abdm/tests.py delete mode 100644 care/abdm/urls.py delete mode 100644 care/abdm/utils/api_call.py delete mode 100644 care/abdm/utils/cipher.py delete mode 100644 care/abdm/utils/fhir.py delete mode 100644 care/abdm/views.py diff --git a/.env.example b/.env.example index 091bea02fd..057f853951 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,6 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5433/care REDIS_URL=redis://localhost:6380 CELERY_BROKER_URL=redis://localhost:6380/0 -FIDELIUS_URL=http://localhost:8092 - DJANGO_DEBUG=False BUCKET_REGION=ap-south-1 diff --git a/aws/backend.json b/aws/backend.json index fcacb36194..145b36061b 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -114,29 +114,21 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { "name": "ABDM_FACILITY_URL", "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "X_CM_ID", + "name": "ABDM_CM_ID", "value": "sbx" }, - { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" - }, { "name": "SENTRY_TRACES_SAMPLE_RATE", "value": "1.0" diff --git a/aws/celery.json b/aws/celery.json index efb6182134..197c6d7346 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -11,9 +11,7 @@ } }, "portMappings": [], - "command": [ - "/app/celery_beat-ecs.sh" - ], + "command": ["/app/celery_beat-ecs.sh"], "cpu": 128, "environment": [ { @@ -97,29 +95,21 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { "name": "ABDM_FACILITY_URL", "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "X_CM_ID", + "name": "ABDM_CM_ID", "value": "sbx" }, - { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" - }, { "name": "SENTRY_TRACES_SAMPLE_RATE", "value": "1.0" @@ -296,9 +286,7 @@ "awslogs-stream-prefix": "ecs" } }, - "command": [ - "/app/celery_worker-ecs.sh" - ], + "command": ["/app/celery_worker-ecs.sh"], "cpu": 384, "memory": 1536, "memoryReservation": 1536, @@ -384,24 +372,20 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { - "name": "X_CM_ID", - "value": "sbx" + "name": "ABDM_FACILITY_URL", + "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" + "name": "ABDM_CM_ID", + "value": "sbx" }, { "name": "SENTRY_TRACES_SAMPLE_RATE", diff --git a/care/abdm/__init__.py b/care/abdm/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/abdm/admin.py b/care/abdm/admin.py deleted file mode 100644 index 846f6b4061..0000000000 --- a/care/abdm/admin.py +++ /dev/null @@ -1 +0,0 @@ -# Register your models here. diff --git a/care/abdm/api/__init__.py b/care/abdm/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/abdm/api/serializers/abha_number.py b/care/abdm/api/serializers/abha_number.py deleted file mode 100644 index c166d57228..0000000000 --- a/care/abdm/api/serializers/abha_number.py +++ /dev/null @@ -1,20 +0,0 @@ -# ModelSerializer -from rest_framework import serializers - -from care.abdm.models import AbhaNumber -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models import PatientRegistration -from care.utils.serializers.fields import ExternalIdSerializerField - - -class AbhaNumberSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - patient = ExternalIdSerializerField( - queryset=PatientRegistration.objects.all(), required=False, allow_null=True - ) - patient_object = PatientDetailSerializer(source="patient", read_only=True) - new = serializers.BooleanField(read_only=True) - - class Meta: - model = AbhaNumber - exclude = ("deleted", "access_token", "refresh_token", "txn_id") diff --git a/care/abdm/api/serializers/auth.py b/care/abdm/api/serializers/auth.py deleted file mode 100644 index 9b533d0c9b..0000000000 --- a/care/abdm/api/serializers/auth.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework.serializers import CharField, IntegerField, Serializer - - -class AbdmAuthResponseSerializer(Serializer): - """ - Serializer for the response of the authentication API - """ - - accessToken = CharField() - refreshToken = CharField() - expiresIn = IntegerField() - refreshExpiresIn = IntegerField() - tokenType = CharField() - - -class AbdmAuthInitResponseSerializer(Serializer): - """ - Serializer for the response of the authentication API - """ - - token = CharField() - refreshToken = CharField() - expiresIn = IntegerField() - refreshExpiresIn = IntegerField() diff --git a/care/abdm/api/serializers/consent.py b/care/abdm/api/serializers/consent.py deleted file mode 100644 index e2f3d6cb93..0000000000 --- a/care/abdm/api/serializers/consent.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework import serializers - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.users.api.serializers.user import UserBaseMinimumSerializer - - -class ConsentArtefactSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - - class Meta: - model = ConsentArtefact - exclude = ( - "deleted", - "external_id", - "key_material_private_key", - "key_material_public_key", - "key_material_nonce", - "key_material_algorithm", - "key_material_curve", - "signature", - ) - - -class ConsentRequestSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - patient_abha_object = AbhaNumberSerializer(source="patient_abha", read_only=True) - requester = UserBaseMinimumSerializer(read_only=True) - consent_artefacts = ConsentArtefactSerializer(many=True, read_only=True) - - class Meta: - model = ConsentRequest - exclude = ("deleted", "external_id") diff --git a/care/abdm/api/serializers/health_facility.py b/care/abdm/api/serializers/health_facility.py deleted file mode 100644 index 336f348584..0000000000 --- a/care/abdm/api/serializers/health_facility.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework import serializers - -from care.abdm.models import HealthFacility - - -class HealthFacilitySerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - registered = serializers.BooleanField(read_only=True) - - class Meta: - model = HealthFacility - exclude = ("deleted",) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py deleted file mode 100644 index 2c1910b823..0000000000 --- a/care/abdm/api/serializers/healthid.py +++ /dev/null @@ -1,66 +0,0 @@ -from rest_framework.serializers import CharField, Serializer, UUIDField - - -class AadharOtpGenerateRequestPayloadSerializer(Serializer): - aadhaar = CharField(max_length=16, min_length=12, required=True) - - -class AadharOtpResendRequestPayloadSerializer(Serializer): - txnId = CharField(max_length=64, min_length=1, required=True) - - -class HealthIdSerializer(Serializer): - healthId = CharField(max_length=64, min_length=1, required=True) - - -class QRContentSerializer(Serializer): - hidn = CharField(max_length=17, min_length=17, required=True) - phr = CharField(max_length=64, min_length=1, required=True) - name = CharField(max_length=64, min_length=1, required=True) - gender = CharField(max_length=1, min_length=1, required=True) - dob = CharField(max_length=10, min_length=8, required=True) - - -class HealthIdAuthSerializer(Serializer): - authMethod = CharField(max_length=64, min_length=1, required=True) - healthid = CharField(max_length=64, min_length=1, required=True) - - -class ABHASearchRequestSerializer: - name = CharField(max_length=64, min_length=1, required=False) - mobile = CharField( - max_length=10, - min_length=10, - required=False, - ) - gender = CharField(max_length=1, min_length=1, required=False) - yearOfBirth = CharField(max_length=4, min_length=4, required=False) - - -class GenerateMobileOtpRequestPayloadSerializer(Serializer): - mobile = CharField(max_length=10, min_length=10, required=True) - txnId = CharField(max_length=64, min_length=1, required=True) - - -class VerifyOtpRequestPayloadSerializer(Serializer): - otp = CharField(max_length=6, min_length=6, required=True, help_text="OTP") - txnId = CharField(max_length=64, min_length=1, required=True) - patientId = UUIDField(required=False) - - -class VerifyDemographicsRequestPayloadSerializer(Serializer): - gender = CharField(max_length=10, min_length=1, required=True) - name = CharField(max_length=64, min_length=1, required=True) - yearOfBirth = CharField(max_length=4, min_length=4, required=True) - txnId = CharField(max_length=64, min_length=1, required=True) - - -class CreateHealthIdSerializer(Serializer): - healthId = CharField(max_length=64, min_length=1, required=False) - txnId = CharField(max_length=64, min_length=1, required=True) - patientId = UUIDField(required=False) - - -class LinkPatientSerializer(Serializer): - abha_number = UUIDField(required=True) - patient = UUIDField(required=True) diff --git a/care/abdm/api/serializers/hip.py b/care/abdm/api/serializers/hip.py deleted file mode 100644 index 4e3bb0f9ab..0000000000 --- a/care/abdm/api/serializers/hip.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework.serializers import CharField, IntegerField, Serializer - - -class AddressSerializer(Serializer): - line = CharField() - district = CharField() - state = CharField() - pincode = CharField() - - -class PatientSerializer(Serializer): - healthId = CharField(allow_null=True) - healthIdNumber = CharField() - name = CharField() - gender = CharField() - yearOfBirth = IntegerField() - dayOfBirth = IntegerField() - monthOfBirth = IntegerField() - address = AddressSerializer() - - -class ProfileSerializer(Serializer): - hipCode = CharField() - patient = PatientSerializer() - - -class HipShareProfileSerializer(Serializer): - """ - Serializer for the request of the share_profile - """ - - requestId = CharField() - profile = ProfileSerializer() diff --git a/care/abdm/api/viewsets/abha_number.py b/care/abdm/api/viewsets/abha_number.py deleted file mode 100644 index 2e94f2aae6..0000000000 --- a/care/abdm/api/viewsets/abha_number.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.db.models import Q -from django.http import Http404 -from rest_framework.decorators import action -from rest_framework.mixins import RetrieveModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway -from care.utils.queryset.patient import get_patient_queryset - - -class AbhaNumberViewSet( - GenericViewSet, - RetrieveModelMixin, -): - serializer_class = AbhaNumberSerializer - model = AbhaNumber - queryset = AbhaNumber.objects.all() - - def get_object(self): - id = self.kwargs.get("pk") - - instance = self.queryset.filter( - Q(abha_number=id) | Q(health_id=id) | Q(patient__external_id=id) - ).first() - - if not instance or not get_patient_queryset(self.request.user).contains( - instance.patient - ): - raise Http404 - - self.check_object_permissions(self.request, instance) - - return instance - - @action(detail=True, methods=["GET"]) - def qr_code(self, request, *args, **kwargs): - obj = self.get_object() - serializer = self.get_serializer(obj) - response = HealthIdGateway().get_qr_code(serializer.data) - return Response(response) - - @action(detail=True, methods=["GET"]) - def profile(self, request, *args, **kwargs): - obj = self.get_object() - serializer = self.get_serializer(obj) - response = HealthIdGateway().get_profile(serializer.data) - return Response(response) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py deleted file mode 100644 index b63b484583..0000000000 --- a/care/abdm/api/viewsets/auth.py +++ /dev/null @@ -1,341 +0,0 @@ -import json -import logging -from datetime import datetime, timedelta - -from django.core.cache import cache -from rest_framework import status -from rest_framework.generics import GenericAPIView, get_object_or_404 -from rest_framework.response import Response - -from care.abdm.utils.api_call import AbdmGateway -from care.abdm.utils.cipher import Cipher -from care.abdm.utils.fhir import Fhir -from care.facility.models.patient import PatientRegistration -from care.facility.models.patient_consultation import PatientConsultation -from config.authentication import ABDMAuthentication - -logger = logging.getLogger(__name__) - - -class OnFetchView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - AbdmGateway().init(data["resp"]["requestId"]) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class OnInitView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class OnConfirmView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - if "validity" in data["auth"]: - if data["auth"]["validity"]["purpose"] == "LINK": - AbdmGateway().add_care_context( - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - else: - AbdmGateway().save_linking_token( - data["auth"]["patient"], - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - else: - AbdmGateway().save_linking_token( - data["auth"]["patient"], - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - AbdmGateway().add_care_context( - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class AuthNotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - if data["auth"]["status"] != "GRANTED": - return - - AbdmGateway.auth_on_notify({"request_id": data["auth"]["transactionId"]}) - - # AbdmGateway().add_care_context( - # data["auth"]["accessToken"], - # data["resp"]["requestId"], - # ) - - -class OnAddContextsView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class DiscoverView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - patients = PatientRegistration.objects.all() - verified_identifiers = data["patient"]["verifiedIdentifiers"] - matched_by = [] - if len(verified_identifiers) == 0: - return Response( - "No matching records found, need more data", - status=status.HTTP_404_NOT_FOUND, - ) - else: - for identifier in verified_identifiers: - if identifier["value"] is None: - continue - - # if identifier["type"] == "MOBILE": - # matched_by.append(identifier["value"]) - # mobile = identifier["value"].replace("+91", "").replace("-", "") - # patients = patients.filter( - # Q(phone_number=f"+91{mobile}") | Q(phone_number=mobile) - # ) - - if identifier["type"] == "NDHM_HEALTH_NUMBER": - matched_by.append(identifier["value"]) - patients = patients.filter( - abha_number__abha_number=identifier["value"] - ) - - if identifier["type"] == "HEALTH_ID": - matched_by.append(identifier["value"]) - patients = patients.filter( - abha_number__health_id=identifier["value"] - ) - - # TODO: also filter by demographics - patient = patients.last() - - if not patient: - return Response( - "No matching records found, need more data", - status=status.HTTP_404_NOT_FOUND, - ) - - AbdmGateway().on_discover( - { - "request_id": data["requestId"], - "transaction_id": data["transactionId"], - "patient_id": str(patient.external_id), - "patient_name": patient.name, - "care_contexts": list( - map( - lambda consultation: { - "id": str(consultation.external_id), - "name": f"Encounter: {consultation.created_date.date()!s}", - }, - PatientConsultation.objects.filter(patient=patient), - ) - ), - "matched_by": matched_by, - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class LinkInitView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - # TODO: send otp to patient - - AbdmGateway().on_link_init( - { - "request_id": data["requestId"], - "transaction_id": data["transactionId"], - "patient_id": data["patient"]["referenceNumber"], - "phone": "7639899448", - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class LinkConfirmView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - # TODO: verify otp - - patient = get_object_or_404( - PatientRegistration.objects.filter( - external_id=data["confirmation"]["linkRefNumber"] - ) - ) - AbdmGateway().on_link_confirm( - { - "request_id": data["requestId"], - "patient_id": str(patient.external_id), - "patient_name": patient.name, - "care_contexts": list( - map( - lambda consultation: { - "id": str(consultation.external_id), - "name": f"Encounter: {consultation.created_date.date()!s}", - }, - PatientConsultation.objects.filter(patient=patient), - ) - ), - } - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class NotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - cache.set(data["notification"]["consentId"], json.dumps(data)) - - AbdmGateway().on_notify( - { - "request_id": data["requestId"], - "consent_id": data["notification"]["consentId"], - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class RequestDataView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - consent_id = data["hiRequest"]["consent"]["id"] - consent = json.loads(cache.get(consent_id)) if consent_id in cache else None - if not consent or consent["notification"]["status"] != "GRANTED": - return Response({}, status=status.HTTP_401_UNAUTHORIZED) - - # TODO: check if from and to are in range and consent expiry is greater than today - # consent_from = datetime.fromisoformat( - # consent["notification"]["permission"]["dateRange"]["from"][:-1] - # ) - # consent_to = datetime.fromisoformat( - # consent["notification"]["permission"]["dateRange"]["to"][:-1] - # ) - # now = datetime.now() - # if not consent_from < now and now > consent_to: - # return Response({}, status=status.HTTP_403_FORBIDDEN) - - on_data_request_response = AbdmGateway().on_data_request( - {"request_id": data["requestId"], "transaction_id": data["transactionId"]} - ) - - if on_data_request_response.status_code != 202: - return Response({}, status=status.HTTP_202_ACCEPTED) - return Response( - on_data_request_response, status=status.HTTP_400_BAD_REQUEST - ) - - cipher = Cipher( - data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"], - data["hiRequest"]["keyMaterial"]["nonce"], - ) - - data_transfer_response = AbdmGateway().data_transfer( - { - "transaction_id": data["transactionId"], - "data_push_url": data["hiRequest"]["dataPushUrl"], - "care_contexts": sum( - list( - map( - lambda context: list( - map( - lambda record: { - "patient_id": context["patientReference"], - "consultation_id": context[ - "careContextReference" - ], - "data": cipher.encrypt( - Fhir( - PatientConsultation.objects.filter( - external_id=context[ - "careContextReference" - ] - ).first() - ).create_record(record) - )["data"], - }, - consent["notification"]["consentDetail"]["hiTypes"], - ) - ), - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) - ), - [], - ), - "key_material": { - "cryptoAlg": "ECDH", - "curve": "Curve25519", - "dhPublicKey": { - "expiry": (datetime.now() + timedelta(days=2)).isoformat(), - "parameters": "Curve25519/32byte random key", - "keyValue": cipher.key_to_share, - }, - "nonce": cipher.internal_nonce, - }, - } - ) - - AbdmGateway().data_notify( - { - "health_id": consent["notification"]["consentDetail"]["patient"]["id"], - "consent_id": data["hiRequest"]["consent"]["id"], - "transaction_id": data["transactionId"], - "session_status": ( - "TRANSFERRED" - if data_transfer_response - and data_transfer_response.status_code == 202 - else "FAILED" - ), - "care_contexts": list( - map( - lambda context: {"id": context["careContextReference"]}, - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) - ), - } - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/consent.py b/care/abdm/api/viewsets/consent.py deleted file mode 100644 index da6fc0ac4f..0000000000 --- a/care/abdm/api/viewsets/consent.py +++ /dev/null @@ -1,260 +0,0 @@ -import logging - -from django_filters import rest_framework as filters -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.mixins import ListModelMixin, RetrieveModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.consent import ConsentRequestSerializer -from care.abdm.api.viewsets.health_information import HealthInformationViewSet -from care.abdm.models.base import Status -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.abdm.service.gateway import Gateway -from care.utils.queryset.facility import get_facility_queryset -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -class ConsentRequestFilter(filters.FilterSet): - patient = filters.UUIDFilter(field_name="patient_abha__patient__external_id") - health_id = filters.CharFilter(field_name="patient_abha__health_id") - ordering = filters.OrderingFilter( - fields=( - "created_date", - "updated_date", - ) - ) - facility = filters.UUIDFilter( - field_name="patient_abha__patient__facility__external_id" - ) - - class Meta: - model = ConsentRequest - fields = ["patient", "health_id", "purpose"] - - -class ConsentViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin): - serializer_class = ConsentRequestSerializer - model = ConsentRequest - queryset = ConsentRequest.objects.all() - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = ConsentRequestFilter - - def get_queryset(self): - queryset = self.queryset - facilities = get_facility_queryset(self.request.user) - return queryset.filter(requester__facility__in=facilities).distinct() - - def create(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - if ratelimit( - request, "consent__create", [serializer.validated_data["patient_abha"]] - ): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = ConsentRequest(**serializer.validated_data, requester=request.user) - - response = Gateway().consent_requests__init(consent) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - consent.save() - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_201_CREATED - ) - - @action(detail=True, methods=["GET"]) - def status(self, request, pk): - if ratelimit(request, "consent__status", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = self.queryset.filter(external_id=pk).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - response = Gateway().consent_requests__status(str(consent.consent_id)) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK - ) - - @action(detail=True, methods=["GET"]) - def fetch(self, request, pk): - if ratelimit(request, "consent__fetch", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = self.queryset.filter(external_id=pk).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - for artefact in consent.consent_artefacts.all(): - response = Gateway().consents__fetch(str(artefact.artefact_id)) - - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK - ) - - -class ConsentCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def consent_request__on_init(self, request): - data = request.data - consent = ConsentRequest.objects.filter( - external_id=data["resp"]["requestId"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - consent.consent_id = data["consentRequest"]["id"] - consent.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def consent_request__on_status(self, request): - data = request.data - consent = ConsentRequest.objects.filter( - consent_id=data["consentRequest"]["id"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - if "notification" not in data: - return Response(status=status.HTTP_202_ACCEPTED) - - if data["notification"]["status"] != Status.DENIED: - consent_artefacts = data["notification"]["consentArtefacts"] or [] - for artefact in consent_artefacts: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - if not consent_artefact: - consent_artefact = ConsentArtefact( - external_id=artefact["id"], - consent_request=consent, - **consent.consent_details_dict(), - ) - - consent_artefact.status = data["notification"]["status"] - consent_artefact.save() - consent.status = data["notification"]["status"] - consent.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def consents__hiu__notify(self, request): - data = request.data - - if not data["notification"]["consentRequestId"]: - for artefact in data["notification"]["consentArtefacts"]: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - - consent_artefact.status = Status.REVOKED - consent_artefact.save() - return Response(status=status.HTTP_202_ACCEPTED) - - consent = ConsentRequest.objects.filter( - consent_id=data["notification"]["consentRequestId"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - if data["notification"]["status"] != Status.DENIED: - consent_artefacts = data["notification"]["consentArtefacts"] or [] - for artefact in consent_artefacts: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - if not consent_artefact: - consent_artefact = ConsentArtefact( - external_id=artefact["id"], - consent_request=consent, - **consent.consent_details_dict(), - ) - - consent_artefact.status = data["notification"]["status"] - consent_artefact.save() - consent.status = data["notification"]["status"] - consent.save() - - Gateway().consents__hiu__on_notify(consent, data["requestId"]) - - if data["notification"]["status"] == Status.GRANTED: - ConsentViewSet().fetch(request, consent.external_id) - - return Response(status=status.HTTP_202_ACCEPTED) - - def consents__on_fetch(self, request): - data = request.data["consent"] - artefact = ConsentArtefact.objects.filter( - external_id=data["consentDetail"]["consentId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - artefact.hip = data["consentDetail"]["hip"]["id"] - artefact.hiu = data["consentDetail"]["hiu"]["id"] - artefact.cm = data["consentDetail"]["consentManager"]["id"] - - artefact.care_contexts = data["consentDetail"]["careContexts"] - artefact.hi_types = data["consentDetail"]["hiTypes"] - - artefact.access_mode = data["consentDetail"]["permission"]["accessMode"] - artefact.from_time = data["consentDetail"]["permission"]["dateRange"]["from"] - artefact.to_time = data["consentDetail"]["permission"]["dateRange"]["to"] - artefact.expiry = data["consentDetail"]["permission"]["dataEraseAt"] - - artefact.frequency_unit = data["consentDetail"]["permission"]["frequency"][ - "unit" - ] - artefact.frequency_value = data["consentDetail"]["permission"]["frequency"][ - "value" - ] - artefact.frequency_repeats = data["consentDetail"]["permission"]["frequency"][ - "repeats" - ] - - artefact.signature = data["signature"] - artefact.save() - - HealthInformationViewSet().request(request, artefact.external_id) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/health_facility.py b/care/abdm/api/viewsets/health_facility.py deleted file mode 100644 index 8b1acbeab0..0000000000 --- a/care/abdm/api/viewsets/health_facility.py +++ /dev/null @@ -1,117 +0,0 @@ -import re - -from celery import shared_task -from django.conf import settings -from dry_rest_permissions.generics import DRYPermissions -from rest_framework.decorators import action -from rest_framework.mixins import ( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.health_facility import HealthFacilitySerializer -from care.abdm.models import HealthFacility -from care.abdm.utils.api_call import Facility -from care.utils.queryset.facility import get_facility_queryset - - -@shared_task -def register_health_facility_as_service(facility_external_id): - if settings.ENABLE_ABDM: - return [False, "ABDM Services are currently disabled"] - - health_facility = HealthFacility.objects.filter( - facility__external_id=facility_external_id - ).first() - - if not health_facility: - return [False, "Health Facility Not Found"] - - if health_facility.registered: - return [True, None] - - clean_facility_name = re.sub(r"[^A-Za-z0-9 ]+", " ", health_facility.facility.name) - clean_facility_name = re.sub(r"\s+", " ", clean_facility_name).strip() - hip_name = settings.HIP_NAME_PREFIX + clean_facility_name + settings.HIP_NAME_SUFFIX - response = Facility().add_update_service( - { - "facilityId": health_facility.hf_id, - "facilityName": hip_name, - "HRP": [ - { - "bridgeId": settings.ABDM_CLIENT_ID, - "hipName": hip_name, - "type": "HIP", - "active": True, - "alias": ["CARE_HIP"], - } - ], - } - ) - - if response.status_code == 200: - data = response.json()[0] - - if "error" in data: - if ( - data["error"].get("code") == "2500" - and settings.ABDM_CLIENT_ID in data["error"].get("message") - and "already associated" in data["error"].get("message") - ): - health_facility.registered = True - health_facility.save() - return [True, None] - - return [ - False, - data["error"].get("message", "Error while registering HIP as service"), - ] - - if "servicesLinked" in data: - health_facility.registered = True - health_facility.save() - return [True, None] - - return [False, None] - - -class HealthFacilityViewSet( - GenericViewSet, - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -): - serializer_class = HealthFacilitySerializer - model = HealthFacility - queryset = HealthFacility.objects.all() - permission_classes = (IsAuthenticated, DRYPermissions) - lookup_field = "facility__external_id" - - def get_queryset(self): - queryset = self.queryset - facilities = get_facility_queryset(self.request.user) - return queryset.filter(facility__in=facilities) - - @action(detail=True, methods=["POST"]) - def register_service(self, request, facility__external_id): - [registered, error] = register_health_facility_as_service(facility__external_id) - - if error: - return Response({"detail": error}, status=400) - - return Response({"registered": registered}) - - def perform_create(self, serializer): - instance = serializer.save() - register_health_facility_as_service.delay(instance.facility.external_id) - - def perform_update(self, serializer): - serializer.validated_data["registered"] = False - instance = serializer.save() - register_health_facility_as_service.delay(instance.facility.external_id) diff --git a/care/abdm/api/viewsets/health_information.py b/care/abdm/api/viewsets/health_information.py deleted file mode 100644 index 98a2825276..0000000000 --- a/care/abdm/api/viewsets/health_information.py +++ /dev/null @@ -1,144 +0,0 @@ -import json -import logging - -from django.db.models import Q -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.models.consent import ConsentArtefact -from care.abdm.service.gateway import Gateway -from care.abdm.utils.cipher import Cipher -from care.facility.models.file_upload import FileUpload -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -class HealthInformationViewSet(GenericViewSet): - - def retrieve(self, request, pk): - files = FileUpload.objects.filter( - Q(internal_name=f"{pk}.json") | Q(associating_id=pk), - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - ) - - if files.count() == 0 or all([not file.upload_completed for file in files]): - return Response( - {"detail": "No Health Information found with the given id"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if files.count() == 1: - file = files.first() - - if file.is_archived: - return Response( - { - "is_archived": True, - "archived_reason": file.archive_reason, - "archived_time": file.archived_datetime, - "detail": f"This file has been archived as {file.archive_reason} at {file.archived_datetime}", - }, - status=status.HTTP_404_NOT_FOUND, - ) - - contents = [] - for file in files: - if file.upload_completed: - content_type, content = file.file_contents() - contents.extend(content) - - return Response({"data": json.loads(content)}, status=status.HTTP_200_OK) - - @action(detail=True, methods=["POST"]) - def request(self, request, pk): - if ratelimit(request, "health_information__request", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - artefact = ConsentArtefact.objects.filter(external_id=pk).first() - - if not artefact: - return Response( - {"detail": "No Consent artefact found with the given id"}, - status=status.HTTP_404_NOT_FOUND, - ) - - response = Gateway().health_information__cm__request(artefact) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response(status=status.HTTP_200_OK) - - -class HealthInformationCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def health_information__hiu__on_request(self, request): - data = request.data - - artefact = ConsentArtefact.objects.filter( - consent_id=data["resp"]["requestId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - if "hiRequest" in data: - artefact.consent_id = data["hiRequest"]["transactionId"] - artefact.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def health_information__transfer(self, request): - data = request.data - - artefact = ConsentArtefact.objects.filter( - consent_id=data["transactionId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - cipher = Cipher( - data["keyMaterial"]["dhPublicKey"]["keyValue"], - data["keyMaterial"]["nonce"], - artefact.key_material_private_key, - artefact.key_material_public_key, - artefact.key_material_nonce, - ) - entries = [] - for entry in data["entries"]: - if "content" in entry: - entries.append( - { - "content": cipher.decrypt(entry["content"]), - "care_context_reference": entry["careContextReference"], - } - ) - - if "link" in entry: - # TODO: handle link - pass - - file = FileUpload( - internal_name=f"{artefact.external_id}.json", - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - associating_id=artefact.consent_request.external_id, - ) - file.put_object(json.dumps(entries), ContentType="application/json") - file.upload_completed = True - file.save() - - Gateway().health_information__notify(artefact) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py deleted file mode 100644 index 347f1a01b2..0000000000 --- a/care/abdm/api/viewsets/healthid.py +++ /dev/null @@ -1,765 +0,0 @@ -# ABDM HealthID APIs - -import logging -from datetime import datetime - -from drf_spectacular.utils import extend_schema -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError -from rest_framework.mixins import CreateModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.api.serializers.healthid import ( - AadharOtpGenerateRequestPayloadSerializer, - AadharOtpResendRequestPayloadSerializer, - CreateHealthIdSerializer, - GenerateMobileOtpRequestPayloadSerializer, - HealthIdAuthSerializer, - HealthIdSerializer, - LinkPatientSerializer, - QRContentSerializer, - VerifyDemographicsRequestPayloadSerializer, - VerifyOtpRequestPayloadSerializer, -) -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models.patient import PatientConsultation, PatientRegistration -from care.utils.queryset.patient import get_patient_queryset -from config.auth_views import CaptchaRequiredException -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -# API for Generating OTP for HealthID -class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): - base_name = "healthid" - model = AbhaNumber - - @extend_schema( - operation_id="generate_aadhaar_otp", - request=AadharOtpGenerateRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def generate_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "generate_aadhaar_otp", [data["aadhaar"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/resendAadhaarOtp - operation_id="resend_aadhaar_otp", - request=AadharOtpResendRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def resend_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "resend_aadhaar_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = AadharOtpResendRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().resend_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/verifyAadhaarOtp - operation_id="verify_aadhaar_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def verify_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "verify_aadhaar_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_aadhaar_otp( - data - ) # HealthIdGatewayV2().verify_document_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/generateMobileOTP - operation_id="generate_mobile_otp", - request=GenerateMobileOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def generate_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "generate_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/verifyMobileOTP - operation_id="verify_mobile_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def verify_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "verify_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - def create_abha(self, abha_profile, token): - abha_object = AbhaNumber.objects.filter( - abha_number=abha_profile["healthIdNumber"] - ).first() - - if abha_object: - return abha_object - - abha_object = AbhaNumber.objects.create( - abha_number=abha_profile["healthIdNumber"], - health_id=abha_profile["healthId"], - name=abha_profile["name"], - first_name=abha_profile["firstName"], - middle_name=abha_profile["middleName"], - last_name=abha_profile["lastName"], - gender=abha_profile["gender"], - date_of_birth=str( - datetime.strptime( - f"{abha_profile['yearOfBirth']}-{abha_profile['monthOfBirth']}-{abha_profile['dayOfBirth']}", - "%Y-%m-%d", - ) - )[0:10], - address=abha_profile["address"] if "address" in abha_profile else "", - district=abha_profile["districtName"], - state=abha_profile["stateName"], - pincode=abha_profile["pincode"], - email=abha_profile["email"], - profile_photo=abha_profile["profilePhoto"], - new=abha_profile["new"], - txn_id=token["txn_id"], - access_token=token["access_token"], - refresh_token=token["refresh_token"], - ) - abha_object.save() - - return abha_object - - def add_abha_details_to_patient(self, abha_object, patient_object): - if abha_object.patient is not None: - raise ValidationError(detail="Abha Number is already linked to a patient") - - if getattr(patient_object, "abha_number", None) is not None: - raise ValidationError(detail="Patient already has an Abha Number linked") - - abha_object.patient = patient_object - abha_object.save() - - @extend_schema( - # /v1/registration/aadhaar/createHealthId - operation_id="create_health_id", - request=CreateHealthIdSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def create_health_id(self, request): - data = request.data - - if ratelimit(request, "create_health_id", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = CreateHealthIdSerializer(data=data) - serializer.is_valid(raise_exception=True) - abha_profile = HealthIdGateway().create_health_id(data) - - if "token" not in abha_profile: - raise ValidationError( - detail="\n\n".join( - detail.get("message", "") - for detail in abha_profile.get("details", []) - ) - or abha_profile.get("message", "Error while fetching abha profile") - ) - - # have a serializer to verify data of abha_profile - abha_object = self.create_abha( - abha_profile, - { - "txn_id": data["txnId"], - "access_token": abha_profile["token"], - "refresh_token": abha_profile["refreshToken"], - }, - ) - - if "patientId" in data: - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - # APIs to Find & Link Existing HealthID - # searchByHealthId - @extend_schema( - # /v1/registration/aadhaar/searchByHealthId - operation_id="search_by_health_id", - request=HealthIdSerializer, - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def search_by_health_id(self, request): - data = request.data - - if ratelimit( - request, "search_by_health_id", [data["healthId"]], increment=False - ): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = HealthIdSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().search_by_health_id(data) - return Response(response, status=status.HTTP_200_OK) - - @action(detail=False, methods=["post"]) - def get_abha_card(self, request): - data = request.data - - if ratelimit(request, "get_abha_card", [data["patient"]], increment=False): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - allowed_patients = get_patient_queryset(request.user) - patient = allowed_patients.filter(external_id=data["patient"]).first() - if not patient: - raise ValidationError(detail="Patient not found") - - if getattr(patient, "abha_number", None) is None: - raise ValidationError(detail="Patient hasn't linked thier abha") - - if data["type"] == "png": - response = HealthIdGateway().get_abha_card_png( - {"refreshToken": patient.abha_number.refresh_token} - ) - return Response(response, status=status.HTTP_200_OK) - - response = HealthIdGateway().get_abha_card_pdf( - {"refreshToken": patient.abha_number.refresh_token} - ) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/searchByHealthId - operation_id="link_via_qr", - request=HealthIdSerializer, - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def link_via_qr(self, request): - data = request.data - - if ratelimit(request, "link_via_qr", [data["hidn"]], increment=False): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = QRContentSerializer(data=data) - serializer.is_valid(raise_exception=True) - - dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() - - patient = PatientRegistration.objects.filter( - abha_number__abha_number=data["hidn"] - ).first() - if patient: - return Response( - { - "message": "A patient is already associated with the provided Abha Number" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number = AbhaNumber.objects.filter(abha_number=data["hidn"]).first() - - if not abha_number: - abha_number = AbhaNumber.objects.create( - abha_number=data["hidn"], - health_id=data["phr"], - name=data["name"], - gender=data["gender"], - date_of_birth=str(dob)[0:10], - address=data["address"], - district=data["dist name"], - state=data["state name"], - ) - - AbdmGateway().fetch_modes( - { - "healthId": data["phr"] or data["hidn"], - "name": data["name"], - "gender": data["gender"], - "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[ - 0:10 - ], - } - ) - - abha_number.save() - - if "patientId" in data and data["patientId"] is not None: - patient = PatientRegistration.objects.filter( - external_id=data["patientId"] - ).first() - - if not patient: - return Response( - {"message": "Enter a valid patientId"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - abha_serialized = AbhaNumberSerializer(abha_number).data - return Response( - {"id": abha_serialized["external_id"], "abha_profile": abha_serialized}, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="search_by_health_id", - request=LinkPatientSerializer, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def link_patient(self, request): - data = request.data - - serializer = LinkPatientSerializer(data=data) - serializer.is_valid(raise_exception=True) - - patient_queryset = get_patient_queryset(request.user) - patient = patient_queryset.filter(external_id=data.get("patient")).first() - - if not patient: - return Response( - { - "detail": "Patient not found or you do not have permission to access the patient", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if hasattr(patient, "abha_number"): - return Response( - { - "detail": "Patient already linked to an ABHA Number", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number = AbhaNumber.objects.filter( - external_id=data.get("abha_number") - ).first() - - if not abha_number: - return Response( - { - "detail": "ABHA Number not found", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if abha_number.patient is not None: - return Response( - { - "detail": "ABHA Number already linked to a patient", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - return Response( - AbhaNumberSerializer(abha_number).data, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="get_new_linking_token", - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def get_new_linking_token(self, request): - data = request.data - - if ratelimit(request, "get_new_linking_token", [data["patient"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - patient = PatientDetailSerializer( - PatientRegistration.objects.get(external_id=data["patient"]) - ).data - - AbdmGateway().fetch_modes( - { - "healthId": patient["abha_number_object"]["abha_number"], - "name": patient["abha_number_object"]["name"], - "gender": patient["abha_number_object"]["gender"], - "dateOfBirth": str(patient["abha_number_object"]["date_of_birth"]), - } - ) - - return Response({}, status=status.HTTP_200_OK) - - @action(detail=False, methods=["POST"]) - def add_care_context(self, request, *args, **kwargs): - consultation_id = request.data["consultation"] - - if ratelimit(request, "add_care_context", [consultation_id]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consultation = PatientConsultation.objects.get(external_id=consultation_id) - - if not consultation: - raise ValidationError(detail="Consultation not found") - - if getattr(consultation.patient, "abha_number", None) is None: - raise ValidationError(detail="Patient hasn't linked thier abha") - - AbdmGateway().fetch_modes( - { - "healthId": consultation.patient.abha_number.health_id, - "name": ( - request.data["name"] - if "name" in request.data - else consultation.patient.abha_number.name - ), - "gender": ( - request.data["gender"] - if "gender" in request.data - else consultation.patient.abha_number.gender - ), - "dateOfBirth": ( - request.data["dob"] - if "dob" in request.data - else str(consultation.patient.abha_number.date_of_birth) - ), - "consultationId": consultation_id, - # "authMode": "DIRECT", - "purpose": "LINK", - } - ) - - return Response(status=status.HTTP_202_ACCEPTED) - - @action(detail=False, methods=["POST"]) - def patient_sms_notify(self, request, *args, **kwargs): - patient_id = request.data["patient"] - - if ratelimit(request, "patient_sms_notify", [patient_id]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - patient = PatientRegistration.objects.filter(external_id=patient_id).first() - - if not patient: - return Response( - {"patient": "No matching records found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if getattr(patient, "abha_number", None) is None: - return Response( - {"abha": "Patient hasn't linked thier abha"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - response = AbdmGateway().patient_sms_notify( - { - "phone": patient.phone_number, - "healthId": patient.abha_number.health_id, - } - ) - - return Response(response, status=status.HTTP_202_ACCEPTED) - - # auth/init - @extend_schema( - # /v1/auth/init - operation_id="auth_init", - request=HealthIdAuthSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def auth_init(self, request): - data = request.data - - if ratelimit(request, "auth_init", [data["healthid"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = HealthIdAuthSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().auth_init(data) - return Response(response, status=status.HTTP_200_OK) - - # /v1/auth/confirmWithAadhaarOtp - @extend_schema( - operation_id="confirm_with_aadhaar_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def confirm_with_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "confirm_with_aadhaar_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_aadhaar_otp(data) - abha_profile = HealthIdGateway().get_profile(response) - - # have a serializer to verify data of abha_profile - abha_object = self.create_abha( - abha_profile, - { - "access_token": response["token"], - "refresh_token": response["refreshToken"], - "txn_id": data["txnId"], - }, - ) - - if "patientId" in data: - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - # /v1/auth/confirmWithMobileOtp - @extend_schema( - operation_id="confirm_with_mobile_otp", - request=VerifyOtpRequestPayloadSerializer, - # responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def confirm_with_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "confirm_with_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_mobile_otp(data) - abha_profile = HealthIdGateway().get_profile(response) - - # have a serializer to verify data of abha_profile - abha_object = self.create_abha( - abha_profile, - { - "access_token": response["token"], - "refresh_token": response["refreshToken"], - "txn_id": data["txnId"], - }, - ) - - if "patientId" in data: - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="confirm_with_demographics", - request=VerifyDemographicsRequestPayloadSerializer, - responses={"200": "{'status': true}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def confirm_with_demographics(self, request): - data = request.data - - if ratelimit(request, "confirm_with_demographics", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyDemographicsRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_demographics(data) - return Response(response, status=status.HTTP_200_OK) - - ############################################################################################################ - # HealthID V2 APIs - @extend_schema( - # /v2/registration/aadhaar/checkAndGenerateMobileOTP - operation_id="check_and_generate_mobile_otp", - request=GenerateMobileOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID V2"], - ) - @action(detail=False, methods=["post"]) - def check_and_generate_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "check_and_generate_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().check_and_generate_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py deleted file mode 100644 index aa6abb5b1e..0000000000 --- a/care/abdm/api/viewsets/hip.py +++ /dev/null @@ -1,146 +0,0 @@ -import uuid -from datetime import UTC, datetime - -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.hip import HipShareProfileSerializer -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway -from care.facility.models.facility import Facility -from care.facility.models.patient import PatientRegistration -from config.authentication import ABDMAuthentication - - -class HipViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def get_linking_token(self, data): - AbdmGateway().fetch_modes(data) - return True - - @action(detail=False, methods=["POST"]) - def share(self, request, *args, **kwargs): - data = request.data - - patient_data = data["profile"]["patient"] - counter_id = ( - data["profile"]["hipCode"] - if len(data["profile"]["hipCode"]) == 36 - else Facility.objects.first().external_id - ) - - patient_data["mobile"] = "" - for identifier in patient_data["identifiers"]: - if identifier["type"] == "MOBILE": - patient_data["mobile"] = identifier["value"] - - serializer = HipShareProfileSerializer(data=data) - serializer.is_valid(raise_exception=True) - - if HealthIdGateway().verify_demographics( - patient_data["healthIdNumber"], - patient_data["name"], - patient_data["gender"], - patient_data["yearOfBirth"], - ): - patient = PatientRegistration.objects.filter( - abha_number__abha_number=patient_data["healthIdNumber"] - ).first() - - if not patient: - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=counter_id), - name=patient_data["name"], - gender={"M": 1, "F": 2}.get(patient_data["gender"], 3), - is_antenatal=False, - phone_number=patient_data["mobile"], - emergency_phone_number=patient_data["mobile"], - date_of_birth=datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ).date(), - blood_group="UNK", - nationality="India", - address=patient_data["address"]["line"], - pincode=patient_data["address"]["pincode"], - ) - - abha_number = AbhaNumber.objects.create( - abha_number=patient_data["healthIdNumber"], - health_id=patient_data["healthId"], - name=patient_data["name"], - gender=patient_data["gender"], - date_of_birth=str( - datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ) - )[0:10], - address=patient_data["address"]["line"], - district=patient_data["address"]["district"], - state=patient_data["address"]["state"], - pincode=patient_data["address"]["pincode"], - ) - - try: - self.get_linking_token( - { - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - "name": patient_data["name"], - "gender": patient_data["gender"], - "dateOfBirth": str( - datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ) - )[0:10], - } - ) - except Exception: - return Response( - { - "status": "FAILED", - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - payload = { - "requestId": str(uuid.uuid4()), - "timestamp": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "acknowledgement": { - "status": "SUCCESS", - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - "tokenNumber": "100", - }, - "error": None, - "resp": { - "requestId": data["requestId"], - }, - } - - on_share_response = AbdmGateway().on_share(payload) - if on_share_response.status_code == 202: - return Response( - on_share_response.request.body, - status=status.HTTP_202_ACCEPTED, - ) - - return Response( - { - "status": "ACCEPTED", - "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], - }, - status=status.HTTP_202_ACCEPTED, - ) diff --git a/care/abdm/api/viewsets/monitoring.py b/care/abdm/api/viewsets/monitoring.py deleted file mode 100644 index b1ee830398..0000000000 --- a/care/abdm/api/viewsets/monitoring.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import UTC, datetime - -from rest_framework import status -from rest_framework.generics import GenericAPIView -from rest_framework.response import Response - - -class HeartbeatView(GenericAPIView): - permission_classes = () - authentication_classes = () - - def get(self, request, *args, **kwargs): - return Response( - { - "timestamp": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "status": "UP", - "error": None, - }, - status=status.HTTP_200_OK, - ) diff --git a/care/abdm/api/viewsets/patients.py b/care/abdm/api/viewsets/patients.py deleted file mode 100644 index e29a72487f..0000000000 --- a/care/abdm/api/viewsets/patients.py +++ /dev/null @@ -1,77 +0,0 @@ -import json - -from django.core.cache import cache -from django.db.models import Q -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.models.abha_number import AbhaNumber -from care.abdm.service.gateway import Gateway -from care.utils.notification_handler import send_webpush -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - - -class PatientsViewSet(GenericViewSet): - - @action(detail=False, methods=["POST"]) - def find(self, request): - identifier = request.data["id"] - - if ratelimit(request, "patients__find", [identifier]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - abha_object = AbhaNumber.objects.filter( - Q(abha_number=identifier) | Q(health_id=identifier) - ).first() - - if not abha_object: - return Response( - {"error": "Patient with given id not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - response = Gateway().patients__find(abha_object) - if response.status_code != 202: - return Response(response.text, status=status.HTTP_400_BAD_REQUEST) - - cache.set( - f"abdm__patients__find__{json.loads(response.request.body)['requestId']}", - request.user.username, - timeout=60 * 60, - ) - return Response( - {"detail": "Requested ABDM for patient details"}, status=status.HTTP_200_OK - ) - - -class PatientsCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def patients__on_find(self, request): - username = cache.get( - f"abdm__patients__find__{request.data['resp']['requestId']}" - ) - - if username: - send_webpush( - username=username, - message=json.dumps( - { - "type": "MESSAGE", - "from": "patients/on_find", - "message": request.data, - } - ), - ) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py deleted file mode 100644 index 72913c847a..0000000000 --- a/care/abdm/api/viewsets/status.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework import status -from rest_framework.generics import GenericAPIView -from rest_framework.response import Response - -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import AbdmGateway -from care.facility.models.patient import PatientRegistration -from config.authentication import ABDMAuthentication - - -class NotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - PatientRegistration.objects.filter( - abha_number__health_id=data["notification"]["patient"]["id"] - ).update(abha_number=None) - AbhaNumber.objects.filter( - health_id=data["notification"]["patient"]["id"] - ).delete() - - AbdmGateway().patient_status_on_notify({"request_id": data["requestId"]}) - - return Response(status=status.HTTP_202_ACCEPTED) - - -class SMSOnNotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/apps.py b/care/abdm/apps.py deleted file mode 100644 index 54e278d631..0000000000 --- a/care/abdm/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class AbdmConfig(AppConfig): - name = "care.abdm" - verbose_name = _("ABDM Integration") diff --git a/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py b/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py deleted file mode 100644 index ad5d70caa0..0000000000 --- a/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py +++ /dev/null @@ -1,69 +0,0 @@ -# Generated by Django 4.2.2 on 2023-07-20 17:41 - -import uuid - -from django.db import migrations, models - - -class Migration(migrations.Migration): - replaces = [ - ("abdm", "0001_initial"), - ("abdm", "0002_auto_20221220_2312"), - ("abdm", "0003_auto_20221220_2321"), - ("abdm", "0004_auto_20221220_2325"), - ("abdm", "0005_auto_20221220_2327"), - ("abdm", "0006_auto_20230208_0915"), - ("abdm", "0007_alter_abhanumber_id"), - ] - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="AbhaNumber", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("abha_number", models.TextField(blank=True, null=True)), - ("email", models.EmailField(blank=True, max_length=254, null=True)), - ("first_name", models.TextField(blank=True, null=True)), - ("health_id", models.TextField(blank=True, null=True)), - ("last_name", models.TextField(blank=True, null=True)), - ("middle_name", models.TextField(blank=True, null=True)), - ("profile_photo", models.TextField(blank=True, null=True)), - ("txn_id", models.TextField(blank=True, null=True)), - ("access_token", models.TextField(blank=True, null=True)), - ("refresh_token", models.TextField(blank=True, null=True)), - ("address", models.TextField(blank=True, null=True)), - ("date_of_birth", models.TextField(blank=True, null=True)), - ("district", models.TextField(blank=True, null=True)), - ("gender", models.TextField(blank=True, null=True)), - ("name", models.TextField(blank=True, null=True)), - ("pincode", models.TextField(blank=True, null=True)), - ("state", models.TextField(blank=True, null=True)), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0008_abhanumber_new.py b/care/abdm/migrations/0008_abhanumber_new.py deleted file mode 100644 index 74fdc32e78..0000000000 --- a/care/abdm/migrations/0008_abhanumber_new.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.2 on 2023-08-07 07:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), - ] - - operations = [ - migrations.AddField( - model_name="abhanumber", - name="new", - field=models.BooleanField(default=False), - ), - ] diff --git a/care/abdm/migrations/0009_healthfacility.py b/care/abdm/migrations/0009_healthfacility.py deleted file mode 100644 index 0480e91d9a..0000000000 --- a/care/abdm/migrations/0009_healthfacility.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 4.2.2 on 2023-08-21 09:53 - -import uuid - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0378_consultationbedasset_consultationbed_assets"), - ("abdm", "0008_abhanumber_new"), - ] - - operations = [ - migrations.CreateModel( - name="HealthFacility", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("hf_id", models.CharField(max_length=50, unique=True)), - ( - "facility", - models.OneToOneField( - on_delete=django.db.models.deletion.PROTECT, - to="facility.facility", - to_field="external_id", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0010_healthfacility_registered.py b/care/abdm/migrations/0010_healthfacility_registered.py deleted file mode 100644 index 5a5d753925..0000000000 --- a/care/abdm/migrations/0010_healthfacility_registered.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.2 on 2023-09-05 06:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0009_healthfacility"), - ] - - operations = [ - migrations.AddField( - model_name="healthfacility", - name="registered", - field=models.BooleanField(default=False), - ), - ] diff --git a/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py b/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py deleted file mode 100644 index 905cd09719..0000000000 --- a/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py +++ /dev/null @@ -1,429 +0,0 @@ -# Generated by Django 4.2.2 on 2023-10-01 16:44 - -import uuid - -import django.contrib.postgres.fields -import django.core.validators -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import care.abdm.models.consent -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("abdm", "0010_healthfacility_registered"), - ] - - operations = [ - migrations.AlterField( - model_name="abhanumber", - name="abha_number", - field=models.TextField(blank=True, null=True, unique=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="health_id", - field=models.TextField(blank=True, null=True, unique=True), - ), - migrations.CreateModel( - name="ConsentRequest", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), - ( - "care_contexts", - models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "careContextReference": { - "type": "string" - }, - "patientReference": {"type": "string"}, - }, - "required": [ - "patientReference", - "careContextReference", - ], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "purpose", - models.CharField( - choices=[ - ("CAREMGT", "Care Management"), - ("BTG", "Break The Glass"), - ("PUBHLTH", "Public Health"), - ("HPAYMT", "Healthcare Payment"), - ("DSRCH", "Disease Specific Healthcare Research"), - ("PATRQT", "Self Requested"), - ], - default="CAREMGT", - max_length=20, - ), - ), - ( - "hi_types", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - choices=[ - ("Prescription", "Prescription"), - ("DiagnosticReport", "Diagnostic Report"), - ("OPConsultation", "Op Consultation"), - ("DischargeSummary", "Discharge Summary"), - ("ImmunizationRecord", "Immunization Record"), - ("HealthDocumentRecord", "Record Artifact"), - ("WellnessRecord", "Wellness Record"), - ], - max_length=20, - ), - default=list, - size=None, - ), - ), - ("hip", models.CharField(blank=True, max_length=50, null=True)), - ("hiu", models.CharField(blank=True, max_length=50, null=True)), - ( - "access_mode", - models.CharField( - choices=[ - ("VIEW", "View"), - ("STORE", "Store"), - ("QUERY", "Query"), - ("STREAM", "Stream"), - ], - default="VIEW", - max_length=20, - ), - ), - ( - "from_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_from_time, - null=True, - ), - ), - ( - "to_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_to_time, - null=True, - ), - ), - ( - "expiry", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_expiry, - null=True, - ), - ), - ( - "frequency_unit", - models.CharField( - choices=[ - ("HOUR", "Hour"), - ("DAY", "Day"), - ("WEEK", "Week"), - ("MONTH", "Month"), - ("YEAR", "Year"), - ], - default="HOUR", - max_length=20, - ), - ), - ( - "frequency_value", - models.PositiveSmallIntegerField( - default=1, - validators=[django.core.validators.MinValueValidator(1)], - ), - ), - ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), - ( - "patient_abha", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="abdm.abhanumber", - to_field="health_id", - ), - ), - ( - "requester", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="ConsentArtefact", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), - ( - "care_contexts", - models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "careContextReference": { - "type": "string" - }, - "patientReference": {"type": "string"}, - }, - "required": [ - "patientReference", - "careContextReference", - ], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "purpose", - models.CharField( - choices=[ - ("CAREMGT", "Care Management"), - ("BTG", "Break The Glass"), - ("PUBHLTH", "Public Health"), - ("HPAYMT", "Healthcare Payment"), - ("DSRCH", "Disease Specific Healthcare Research"), - ("PATRQT", "Self Requested"), - ], - default="CAREMGT", - max_length=20, - ), - ), - ( - "hi_types", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - choices=[ - ("Prescription", "Prescription"), - ("DiagnosticReport", "Diagnostic Report"), - ("OPConsultation", "Op Consultation"), - ("DischargeSummary", "Discharge Summary"), - ("ImmunizationRecord", "Immunization Record"), - ("HealthDocumentRecord", "Record Artifact"), - ("WellnessRecord", "Wellness Record"), - ], - max_length=20, - ), - default=list, - size=None, - ), - ), - ("hip", models.CharField(blank=True, max_length=50, null=True)), - ("hiu", models.CharField(blank=True, max_length=50, null=True)), - ( - "access_mode", - models.CharField( - choices=[ - ("VIEW", "View"), - ("STORE", "Store"), - ("QUERY", "Query"), - ("STREAM", "Stream"), - ], - default="VIEW", - max_length=20, - ), - ), - ( - "from_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_from_time, - null=True, - ), - ), - ( - "to_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_to_time, - null=True, - ), - ), - ( - "expiry", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_expiry, - null=True, - ), - ), - ( - "frequency_unit", - models.CharField( - choices=[ - ("HOUR", "Hour"), - ("DAY", "Day"), - ("WEEK", "Week"), - ("MONTH", "Month"), - ("YEAR", "Year"), - ], - default="HOUR", - max_length=20, - ), - ), - ( - "frequency_value", - models.PositiveSmallIntegerField( - default=1, - validators=[django.core.validators.MinValueValidator(1)], - ), - ), - ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), - ( - "status", - models.CharField( - choices=[ - ("REQUESTED", "Requested"), - ("GRANTED", "Granted"), - ("DENIED", "Denied"), - ("EXPIRED", "Expired"), - ("REVOKED", "Revoked"), - ], - default="REQUESTED", - max_length=20, - ), - ), - ("cm", models.CharField(blank=True, max_length=50, null=True)), - ( - "key_material_algorithm", - models.CharField( - blank=True, default="ECDH", max_length=20, null=True - ), - ), - ( - "key_material_curve", - models.CharField( - blank=True, default="Curve25519", max_length=20, null=True - ), - ), - ( - "key_material_public_key", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "key_material_private_key", - models.CharField(blank=True, max_length=200, null=True), - ), - ( - "key_material_nonce", - models.CharField(blank=True, max_length=100, null=True), - ), - ("signature", models.TextField(blank=True, null=True)), - ( - "consent_request", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="consent_artefacts", - to="abdm.consentrequest", - to_field="consent_id", - ), - ), - ( - "patient_abha", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="abdm.abhanumber", - to_field="health_id", - ), - ), - ( - "requester", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0012_consentrequest_status.py b/care/abdm/migrations/0012_consentrequest_status.py deleted file mode 100644 index 43bf1ecb62..0000000000 --- a/care/abdm/migrations/0012_consentrequest_status.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2.2 on 2023-12-02 04:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0011_alter_abhanumber_abha_number_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="consentrequest", - name="status", - field=models.CharField( - choices=[ - ("REQUESTED", "Requested"), - ("GRANTED", "Granted"), - ("DENIED", "Denied"), - ("EXPIRED", "Expired"), - ("REVOKED", "Revoked"), - ], - default="REQUESTED", - max_length=20, - ), - ), - ] diff --git a/care/abdm/migrations/__init__.py b/care/abdm/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/abdm/models/__init__.py b/care/abdm/models/__init__.py deleted file mode 100644 index 5b7edbb6fb..0000000000 --- a/care/abdm/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .abha_number import * # noqa -from .consent import * # noqa -from .health_facility import * # noqa diff --git a/care/abdm/models/abha_number.py b/care/abdm/models/abha_number.py deleted file mode 100644 index f5d67d2132..0000000000 --- a/care/abdm/models/abha_number.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import models - -from care.utils.models.base import BaseModel - - -class AbhaNumber(BaseModel): - abha_number = models.TextField(null=True, blank=True, unique=True) - health_id = models.TextField(null=True, blank=True, unique=True) - - patient = models.OneToOneField( - "facility.PatientRegistration", - related_name="abha_number", - on_delete=models.PROTECT, - null=True, - blank=True, - ) - - name = models.TextField(null=True, blank=True) - first_name = models.TextField(null=True, blank=True) - middle_name = models.TextField(null=True, blank=True) - last_name = models.TextField(null=True, blank=True) - - gender = models.TextField(null=True, blank=True) - date_of_birth = models.TextField(null=True, blank=True) - - address = models.TextField(null=True, blank=True) - district = models.TextField(null=True, blank=True) - state = models.TextField(null=True, blank=True) - pincode = models.TextField(null=True, blank=True) - - email = models.EmailField(null=True, blank=True) - profile_photo = models.TextField(null=True, blank=True) - - new = models.BooleanField(default=False) - - txn_id = models.TextField(null=True, blank=True) - access_token = models.TextField(null=True, blank=True) - refresh_token = models.TextField(null=True, blank=True) - - def __str__(self): - return f"{self.pk} {self.abha_number}" diff --git a/care/abdm/models/base.py b/care/abdm/models/base.py deleted file mode 100644 index 82ff16ca85..0000000000 --- a/care/abdm/models/base.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import models - - -class Status(models.TextChoices): - REQUESTED = "REQUESTED" - GRANTED = "GRANTED" - DENIED = "DENIED" - EXPIRED = "EXPIRED" - REVOKED = "REVOKED" - - -class Purpose(models.TextChoices): - CARE_MANAGEMENT = "CAREMGT" - BREAK_THE_GLASS = "BTG" - PUBLIC_HEALTH = "PUBHLTH" - HEALTHCARE_PAYMENT = "HPAYMT" - DISEASE_SPECIFIC_HEALTHCARE_RESEARCH = "DSRCH" - SELF_REQUESTED = "PATRQT" - - -class HealthInformationTypes(models.TextChoices): - PRESCRIPTION = "Prescription" - DIAGNOSTIC_REPORT = "DiagnosticReport" - OP_CONSULTATION = "OPConsultation" - DISCHARGE_SUMMARY = "DischargeSummary" - IMMUNIZATION_RECORD = "ImmunizationRecord" - RECORD_ARTIFACT = "HealthDocumentRecord" - WELLNESS_RECORD = "WellnessRecord" - - -class AccessMode(models.TextChoices): - VIEW = "VIEW" - STORE = "STORE" - QUERY = "QUERY" - STREAM = "STREAM" - - -class FrequencyUnit(models.TextChoices): - HOUR = "HOUR" - DAY = "DAY" - WEEK = "WEEK" - MONTH = "MONTH" - YEAR = "YEAR" diff --git a/care/abdm/models/consent.py b/care/abdm/models/consent.py deleted file mode 100644 index ee4b0f8264..0000000000 --- a/care/abdm/models/consent.py +++ /dev/null @@ -1,160 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.core.validators import MinValueValidator -from django.db import models -from django.utils import timezone - -from care.abdm.models import AbhaNumber -from care.abdm.models.base import ( - AccessMode, - FrequencyUnit, - HealthInformationTypes, - Purpose, - Status, -) -from care.abdm.models.json_schema import CARE_CONTEXTS -from care.abdm.utils.cipher import Cipher -from care.facility.models.file_upload import FileUpload -from care.users.models import User -from care.utils.models.base import BaseModel -from care.utils.models.validators import JSONFieldSchemaValidator - - -class Consent(BaseModel): - class Meta: - abstract = True - - def default_expiry(): - return timezone.now() + timezone.timedelta(days=30) - - def default_from_time(): - return timezone.now() - timezone.timedelta(days=30) - - def default_to_time(): - return timezone.now() - - consent_id = models.UUIDField(null=True, blank=True, unique=True) - - patient_abha = models.ForeignKey( - AbhaNumber, on_delete=models.PROTECT, to_field="health_id" - ) - - care_contexts = models.JSONField( - default=list, validators=[JSONFieldSchemaValidator(CARE_CONTEXTS)] - ) - - status = models.CharField(choices=Status, max_length=20, default=Status.REQUESTED) - purpose = models.CharField( - choices=Purpose, max_length=20, default=Purpose.CARE_MANAGEMENT - ) - hi_types = ArrayField( - models.CharField(choices=HealthInformationTypes, max_length=20), - default=list, - ) - - hip = models.CharField(max_length=50, null=True, blank=True) - hiu = models.CharField(max_length=50, null=True, blank=True) - - requester = models.ForeignKey( - User, on_delete=models.SET_NULL, null=True, blank=True - ) - - access_mode = models.CharField( - choices=AccessMode, max_length=20, default=AccessMode.VIEW - ) - from_time = models.DateTimeField(null=True, blank=True, default=default_from_time) - to_time = models.DateTimeField(null=True, blank=True, default=default_to_time) - expiry = models.DateTimeField(null=True, blank=True, default=default_expiry) - - frequency_unit = models.CharField( - choices=FrequencyUnit, max_length=20, default=FrequencyUnit.HOUR - ) - frequency_value = models.PositiveSmallIntegerField( - default=1, validators=[MinValueValidator(1)] - ) - frequency_repeats = models.PositiveSmallIntegerField(default=0) - - def consent_details_dict(self): - return { - "patient_abha": self.patient_abha, - "care_contexts": self.care_contexts, - "status": self.status, - "purpose": self.purpose, - "hi_types": self.hi_types, - "hip": self.hip, - "hiu": self.hiu, - "requester": self.requester, - "access_mode": self.access_mode, - "from_time": self.from_time, - "to_time": self.to_time, - "expiry": self.expiry, - "frequency_unit": self.frequency_unit, - "frequency_value": self.frequency_value, - "frequency_repeats": self.frequency_repeats, - } - - -class ConsentRequest(Consent): - @property - def request_id(self): - return self.consent_id - - -class ConsentArtefact(Consent): - @property - def artefact_id(self): - return self.external_id - - @property - def transaction_id(self): - return self.consent_id - - def save(self, *args, **kwargs): - if self.key_material_private_key is None: - cipher = Cipher("", "") - key_material = cipher.generate_key_pair() - - self.key_material_algorithm = "ECDH" - self.key_material_curve = "Curve25519" - self.key_material_public_key = key_material["publicKey"] - self.key_material_private_key = key_material["privateKey"] - self.key_material_nonce = key_material["nonce"] - - if self.status in [Status.REVOKED.value, Status.EXPIRED.value]: - file = FileUpload.objects.filter( - internal_name=f"{self.external_id}.json", - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - ).first() - - if file: - file.is_archived = True - file.archived_datetime = timezone.now() - file.archive_reason = self.status - file.save() - - return super().save(*args, **kwargs) - - consent_request = models.ForeignKey( - ConsentRequest, - on_delete=models.PROTECT, - to_field="consent_id", - null=True, - blank=True, - related_name="consent_artefacts", - ) - - cm = models.CharField(max_length=50, null=True, blank=True) - - key_material_algorithm = models.CharField( - max_length=20, - null=True, - blank=True, - default="ECDH", - ) - key_material_curve = models.CharField( - max_length=20, null=True, blank=True, default="Curve25519" - ) - key_material_public_key = models.CharField(max_length=100, null=True, blank=True) - key_material_private_key = models.CharField(max_length=200, null=True, blank=True) - key_material_nonce = models.CharField(max_length=100, null=True, blank=True) - - signature = models.TextField(null=True, blank=True) diff --git a/care/abdm/models/health_facility.py b/care/abdm/models/health_facility.py deleted file mode 100644 index a38bf1367c..0000000000 --- a/care/abdm/models/health_facility.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import models - -from care.abdm.models.permissions.health_facility import HealthFacilityPermissions -from care.utils.models.base import BaseModel - - -class HealthFacility(BaseModel, HealthFacilityPermissions): - hf_id = models.CharField(max_length=50, unique=True) - registered = models.BooleanField(default=False) - facility = models.OneToOneField( - "facility.Facility", on_delete=models.PROTECT, to_field="external_id" - ) - - def __str__(self): - return f"{self.hf_id} {self.facility}" diff --git a/care/abdm/models/json_schema.py b/care/abdm/models/json_schema.py deleted file mode 100644 index 081b65cc7c..0000000000 --- a/care/abdm/models/json_schema.py +++ /dev/null @@ -1,15 +0,0 @@ -CARE_CONTEXTS = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "content": [ - { - "type": "object", - "properties": { - "patientReference": {"type": "string"}, - "careContextReference": {"type": "string"}, - }, - "additionalProperties": False, - "required": ["patientReference", "careContextReference"], - } - ], -} diff --git a/care/abdm/models/permissions/health_facility.py b/care/abdm/models/permissions/health_facility.py deleted file mode 100644 index f1ccd0045e..0000000000 --- a/care/abdm/models/permissions/health_facility.py +++ /dev/null @@ -1,29 +0,0 @@ -from care.facility.models.mixins.permissions.base import BasePermissionMixin -from care.users.models import User - - -class HealthFacilityPermissions(BasePermissionMixin): - """ - Permissions for HealthFacilityViewSet - """ - - def has_object_read_permission(self, request): - return self.facility.has_object_read_permission(request) - - def has_object_write_permission(self, request): - allowed_user_types = [ - User.TYPE_VALUE_MAP["WardAdmin"], - User.TYPE_VALUE_MAP["LocalBodyAdmin"], - User.TYPE_VALUE_MAP["DistrictAdmin"], - User.TYPE_VALUE_MAP["StateAdmin"], - ] - return request.user.is_superuser or ( - request.user.user_type in allowed_user_types - and self.facility.has_object_write_permission(request) - ) - - def has_object_update_permission(self, request): - return self.has_object_write_permission(request) - - def has_object_destroy_permission(self, request): - return self.has_object_write_permission(request) diff --git a/care/abdm/receivers/consultation.py b/care/abdm/receivers/consultation.py deleted file mode 100644 index 82c9c3c847..0000000000 --- a/care/abdm/receivers/consultation.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from care.abdm.utils.api_call import AbdmGateway -from care.facility.models import PatientConsultation - - -@receiver(post_save, sender=PatientConsultation) -def create_care_context(sender, instance, created, **kwargs): - patient = instance.patient - - if created and getattr(patient, "abha_number", None) is not None: - abha_number = patient.abha_number - - try: - AbdmGateway().fetch_modes( - { - "healthId": abha_number.abha_number, - "name": abha_number.name, - "gender": abha_number.gender, - "dateOfBirth": str(abha_number.date_of_birth), - "consultationId": instance.external_id, - "purpose": "LINK", - } - ) - except Exception: - pass diff --git a/care/abdm/service/gateway.py b/care/abdm/service/gateway.py deleted file mode 100644 index 45f0c781e0..0000000000 --- a/care/abdm/service/gateway.py +++ /dev/null @@ -1,215 +0,0 @@ -import uuid -from datetime import UTC, datetime - -from django.conf import settings -from django.db.models import Q -from rest_framework.exceptions import ValidationError - -from care.abdm.models.abha_number import AbhaNumber -from care.abdm.models.base import Purpose -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.abdm.service.request import Request - - -class Gateway: - def __init__(self): - self.request = Request(settings.ABDM_URL + "/gateway") - - def consent_requests__init(self, consent: ConsentRequest): - data = { - "requestId": str(consent.external_id), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consent": { - "purpose": { - "text": Purpose(consent.purpose).label, - "code": Purpose(consent.purpose).value, - }, - "patient": {"id": consent.patient_abha.health_id}, - "hiu": { - "id": self.get_hf_id_by_health_id(consent.patient_abha.health_id) - }, - "requester": { - "name": f"{consent.requester.REVERSE_TYPE_MAP[consent.requester.user_type]}, {consent.requester.first_name} {consent.requester.last_name}", - "identifier": { - "type": "Care Username", - "value": consent.requester.username, - "system": settings.CURRENT_DOMAIN, - }, - }, - "hiTypes": consent.hi_types, - "permission": { - "accessMode": consent.access_mode, - "dateRange": { - "from": consent.from_time.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "to": consent.to_time.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - }, - "dataEraseAt": consent.expiry.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "frequency": { - "unit": consent.frequency_unit, - "value": consent.frequency_value, - "repeats": consent.frequency_repeats, - }, - }, - }, - } - - path = "/v0.5/consent-requests/init" - return self.request.post(path, data, headers={"X-CM-ID": settings.X_CM_ID}) - - def consent_requests__status(self, consent_request_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consentRequestId": consent_request_id, - } - - return self.request.post( - "/v0.5/consent-requests/status", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def consents__hiu__on_notify(self, consent: ConsentRequest, request_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "resp": {"requestId": request_id}, - } - - if len(consent.consent_artefacts.all()): - data["acknowledgement"] = [] - - for aretefact in consent.consent_artefacts.all(): - data["acknowledgement"].append( - { - "consentId": str(aretefact.artefact_id), - "status": "OK", - } - ) - - return self.request.post( - "/v0.5/consents/hiu/on-notify", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def consents__fetch(self, consent_artefact_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consentId": consent_artefact_id, - } - - return self.request.post( - "/v0.5/consents/fetch", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def health_information__cm__request(self, artefact: ConsentArtefact): - request_id = str(uuid.uuid4()) - artefact.consent_id = request_id - artefact.save() - - data = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "hiRequest": { - "consent": {"id": str(artefact.artefact_id)}, - "dataPushUrl": settings.BACKEND_DOMAIN - + "/v0.5/health-information/transfer", - "keyMaterial": { - "cryptoAlg": artefact.key_material_algorithm, - "curve": artefact.key_material_curve, - "dhPublicKey": { - "expiry": artefact.expiry.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "parameters": f"{artefact.key_material_curve}/{artefact.key_material_algorithm}", - "keyValue": artefact.key_material_public_key, - }, - "nonce": artefact.key_material_nonce, - }, - "dateRange": { - "from": artefact.from_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), - "to": artefact.to_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), - }, - }, - } - - return self.request.post( - "/v0.5/health-information/cm/request", - data, - headers={"X-CM-ID": settings.X_CM_ID}, - ) - - def get_hf_id_by_health_id(self, health_id): - abha_number = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - if not abha_number: - raise ValidationError(detail="No ABHA Number found") - - patient_facility = abha_number.patient.last_consultation.facility - if not hasattr(patient_facility, "healthfacility"): - raise ValidationError(detail="Health Facility not linked") - - return patient_facility.healthfacility.hf_id - - def health_information__notify(self, artefact: ConsentArtefact): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "consentId": str(artefact.artefact_id), - "transactionId": str(artefact.transaction_id), - "doneAt": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notifier": { - "type": "HIU", - "id": self.get_hf_id_by_health_id(artefact.patient_abha.health_id), - }, - "statusNotification": { - "sessionStatus": "TRANSFERRED", - "hipId": artefact.hip, - }, - }, - } - - return self.request.post( - "/v0.5/health-information/notify", - data, - headers={"X-CM-ID": settings.X_CM_ID}, - ) - - def patients__find(self, abha_number: AbhaNumber): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "patient": {"id": abha_number.health_id}, - "requester": { - "type": "HIU", - "id": self.get_hf_id_by_health_id(abha_number.health_id), - }, - }, - } - - return self.request.post( - "/v0.5/patients/find", data, headers={"X-CM-ID": settings.X_CM_ID} - ) diff --git a/care/abdm/service/request.py b/care/abdm/service/request.py deleted file mode 100644 index 55b9f12127..0000000000 --- a/care/abdm/service/request.py +++ /dev/null @@ -1,106 +0,0 @@ -import json -import logging - -import requests -from django.conf import settings -from django.core.cache import cache - -ABDM_GATEWAY_URL = settings.ABDM_URL + "/gateway" -ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" -ABDM_TOKEN_CACHE_KEY = "abdm_token" - -logger = logging.getLogger(__name__) - - -class Request: - def __init__(self, base_url): - self.url = base_url - - def user_header(self, user_token): - if not user_token: - return {} - return {"X-Token": "Bearer " + user_token} - - def auth_header(self): - token = cache.get(ABDM_TOKEN_CACHE_KEY) - if not token: - data = json.dumps( - { - "clientId": settings.ABDM_CLIENT_ID, - "clientSecret": settings.ABDM_CLIENT_SECRET, - } - ) - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - - logger.info("No Token in Cache") - response = requests.post(ABDM_TOKEN_URL, data=data, headers=headers) - - if response.status_code < 300: - if response.headers["Content-Type"] != "application/json": - logger.info( - f"Unsupported Content-Type: {response.headers['Content-Type']}" - ) - logger.info(f"Response: {response.text}") - - return None - else: - data = response.json() - token = data["accessToken"] - expires_in = data["expiresIn"] - - logger.info(f"New Token: {token}") - logger.info(f"Expires in: {expires_in}") - - cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) - else: - logger.info(f"Bad Response: {response.text}") - return None - - return {"Authorization": f"Bearer {token}"} - - def headers(self, additional_headers=None, auth=None): - return { - "Content-Type": "application/json", - "Accept": "*/*", - **(additional_headers or {}), - **(self.user_header(auth) or {}), - **(self.auth_header() or {}), - } - - def get(self, path, params=None, headers=None, auth=None): - url = self.url + path - headers = self.headers(headers, auth) - - logger.info(f"GET: {url}") - response = requests.get(url, headers=headers, params=params) - logger.info(f"{response.status_code} Response: {response.text}") - - return self._handle_response(response) - - def post(self, path, data=None, headers=None, auth=None): - url = self.url + path - payload = json.dumps(data) - headers = self.headers(headers, auth) - - logger.info(f"POST: {url}, {headers}, {data}") - response = requests.post(url, data=payload, headers=headers) - logger.info(f"{response.status_code} Response: {response.text}") - - return self._handle_response(response) - - def _handle_response(self, response: requests.Response): - def custom_json(): - try: - return response.json() - except json.JSONDecodeError as json_err: - logger.error(f"JSON Decode error: {json_err}") - return {"error": response.text} - except Exception as err: - logger.error(f"Unknown error while decoding json: {err}") - return {} - - response.json = custom_json - return response diff --git a/care/abdm/tests.py b/care/abdm/tests.py deleted file mode 100644 index a79ca8be56..0000000000 --- a/care/abdm/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/care/abdm/urls.py b/care/abdm/urls.py deleted file mode 100644 index bf4eb8a5d9..0000000000 --- a/care/abdm/urls.py +++ /dev/null @@ -1,141 +0,0 @@ -from django.urls import path -from rest_framework.routers import SimpleRouter - -from care.abdm.api.viewsets.auth import ( - AuthNotifyView, - DiscoverView, - LinkConfirmView, - LinkInitView, - NotifyView, - OnAddContextsView, - OnConfirmView, - OnFetchView, - OnInitView, - RequestDataView, -) -from care.abdm.api.viewsets.consent import ConsentCallbackViewSet -from care.abdm.api.viewsets.health_information import HealthInformationCallbackViewSet -from care.abdm.api.viewsets.hip import HipViewSet -from care.abdm.api.viewsets.monitoring import HeartbeatView -from care.abdm.api.viewsets.patients import PatientsCallbackViewSet -from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView -from care.abdm.api.viewsets.status import SMSOnNotifyView - - -class OptionalSlashRouter(SimpleRouter): - def __init__(self): - super().__init__() - self.trailing_slash = "/?" - - -abdm_router = OptionalSlashRouter() - -abdm_router.register("profile/v1.0/patients/", HipViewSet, basename="hip") - -abdm_urlpatterns = [ - *abdm_router.urls, - path( - "v0.5/consent-requests/on-init", - ConsentCallbackViewSet.as_view({"post": "consent_request__on_init"}), - name="abdm__consent_request__on_init", - ), - path( - "v0.5/consent-requests/on-status", - ConsentCallbackViewSet.as_view({"post": "consent_request__on_status"}), - name="abdm__consent_request__on_status", - ), - path( - "v0.5/consents/hiu/notify", - ConsentCallbackViewSet.as_view({"post": "consents__hiu__notify"}), - name="abdm__consents__hiu__notify", - ), - path( - "v0.5/consents/on-fetch", - ConsentCallbackViewSet.as_view({"post": "consents__on_fetch"}), - name="abdm__consents__on_fetch", - ), - path( - "v0.5/health-information/hiu/on-request", - HealthInformationCallbackViewSet.as_view( - {"post": "health_information__hiu__on_request"} - ), - name="abdm__health_information__hiu__on_request", - ), - path( - "v0.5/health-information/transfer", - HealthInformationCallbackViewSet.as_view( - {"post": "health_information__transfer"} - ), - name="abdm__health_information__transfer", - ), - path( - "v0.5/patients/on-find", - PatientsCallbackViewSet.as_view({"post": "patients__on_find"}), - name="abdm__patients__on_find", - ), - path( - "v0.5/users/auth/on-fetch-modes", - OnFetchView.as_view(), - name="abdm_on_fetch_modes_view", - ), - path( - "v0.5/users/auth/on-init", - OnInitView.as_view(), - name="abdm_on_init_view", - ), - path( - "v0.5/users/auth/on-confirm", - OnConfirmView.as_view(), - name="abdm_on_confirm_view", - ), - path( - "v0.5/users/auth/notify", - AuthNotifyView.as_view(), - name="abdm_auth_notify_view", - ), - path( - "v0.5/links/link/on-add-contexts", - OnAddContextsView.as_view(), - name="abdm_on_add_context_view", - ), - path( - "v0.5/care-contexts/discover", - DiscoverView.as_view(), - name="abdm_discover_view", - ), - path( - "v0.5/links/link/init", - LinkInitView.as_view(), - name="abdm_link_init_view", - ), - path( - "v0.5/links/link/confirm", - LinkConfirmView.as_view(), - name="abdm_link_confirm_view", - ), - path( - "v0.5/consents/hip/notify", - NotifyView.as_view(), - name="abdm_notify_view", - ), - path( - "v0.5/health-information/hip/request", - RequestDataView.as_view(), - name="abdm_request_data_view", - ), - path( - "v0.5/patients/status/notify", - PatientStatusNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/patients/sms/on-notify", - SMSOnNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/heartbeat", - HeartbeatView.as_view(), - name="abdm_monitoring_heartbeat_view", - ), -] diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py deleted file mode 100644 index e357a6f781..0000000000 --- a/care/abdm/utils/api_call.py +++ /dev/null @@ -1,812 +0,0 @@ -import json -import logging -import uuid -from base64 import b64encode -from datetime import UTC, datetime, timedelta - -import requests -from Crypto.Cipher import PKCS1_v1_5 -from Crypto.PublicKey import RSA -from django.conf import settings -from django.core.cache import cache -from django.db.models import Q -from rest_framework.exceptions import ValidationError - -from care.abdm.models import AbhaNumber -from care.abdm.service.request import Request -from care.facility.models.patient_consultation import PatientConsultation - -GATEWAY_API_URL = settings.ABDM_URL -HEALTH_SERVICE_API_URL = settings.HEALTH_SERVICE_API_URL -ABDM_DEVSERVICE_URL = GATEWAY_API_URL + "/devservice" -ABDM_GATEWAY_URL = GATEWAY_API_URL + "/gateway" -ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" -ABDM_TOKEN_CACHE_KEY = "abdm_token" -ABDM_FACILITY_URL = settings.ABDM_FACILITY_URL - -# TODO: Exception handling for all api calls, need to gracefully handle known exceptions - -logger = logging.getLogger(__name__) - - -def encrypt_with_public_key(a_message): - rsa_public_key = RSA.importKey( - requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert").text.strip() - ) - rsa_public_key = PKCS1_v1_5.new(rsa_public_key) - encrypted_text = rsa_public_key.encrypt(a_message.encode()) - return b64encode(encrypted_text).decode() - - -class APIGateway: - def __init__(self, gateway, token): - if gateway == "health": - self.url = HEALTH_SERVICE_API_URL - elif gateway == "abdm": - self.url = GATEWAY_API_URL - elif gateway == "abdm_gateway": - self.url = ABDM_GATEWAY_URL - elif gateway == "abdm_devservice": - self.url = ABDM_DEVSERVICE_URL - elif gateway == "facility": - self.url = ABDM_FACILITY_URL - else: - self.url = GATEWAY_API_URL - self.token = token - - # def encrypt(self, data): - # cert = cache.get("abdm_cert") - # if not cert: - # cert = requests.get(settings.ABDM_CERT_URL).text - # cache.set("abdm_cert", cert, 3600) - - def add_user_header(self, headers, user_token): - headers.update( - { - "X-Token": "Bearer " + user_token, - } - ) - return headers - - def add_auth_header(self, headers): - token = cache.get(ABDM_TOKEN_CACHE_KEY) - if not token: - logger.info("No Token in Cache") - data = { - "clientId": settings.ABDM_CLIENT_ID, - "clientSecret": settings.ABDM_CLIENT_SECRET, - } - auth_headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - resp = requests.post( - ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers - ) - logger.info(f"Token Response Status: {resp.status_code}") - if resp.status_code < 300: - # Checking if Content-Type is application/json - if resp.headers["Content-Type"] != "application/json": - logger.info( - "Unsupported Content-Type: {}".format( - resp.headers["Content-Type"] - ) - ) - logger.info(f"Response: {resp.text}") - return None - else: - data = resp.json() - token = data["accessToken"] - expires_in = data["expiresIn"] - logger.info(f"New Token: {token}") - logger.info(f"Expires in: {expires_in}") - cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) - else: - logger.info(f"Bad Response: {resp.text}") - return None - # logger.info("Returning Authorization Header: Bearer {}".format(token)) - logger.info("Adding Authorization Header") - auth_header = {"Authorization": f"Bearer {token}"} - return {**headers, **auth_header} - - def add_additional_headers(self, headers, additional_headers): - return {**headers, **additional_headers} - - def get(self, path, params=None, auth=None): - url = self.url + path - headers = {} - headers = self.add_auth_header(headers) - if auth: - headers = self.add_user_header(headers, auth) - logger.info(f"Making GET Request to: {url}") - response = requests.get(url, headers=headers, params=params) - logger.info(f"{response.status_code} Response: {response.text}") - return response - - def post(self, path, data=None, auth=None, additional_headers=None, method="POST"): - url = self.url + path - headers = { - "Content-Type": "application/json", - "accept": "*/*", - "Accept-Language": "en-US", - } - headers = self.add_auth_header(headers) - if auth: - headers = self.add_user_header(headers, auth) - if additional_headers: - headers = self.add_additional_headers(headers, additional_headers) - # headers_string = " ".join( - # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] - # ) - data_json = json.dumps(data) - # logger.info("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) - logger.info(f"Posting Request to: {url}") - response = requests.request(method, url, headers=headers, data=data_json) - logger.info(f"{response.status_code} Response: {response.text}") - return response - - -class HealthIdGateway: - def __init__(self): - self.api = APIGateway("health", None) - - def generate_aadhaar_otp(self, data): - path = "/v1/registration/aadhaar/generateOtp" - response = self.api.post(path, data) - logger.info(f"{response.status_code} Response: {response.text}") - return response.json() - - def resend_aadhaar_otp(self, data): - path = "/v1/registration/aadhaar/resendAadhaarOtp" - response = self.api.post(path, data) - return response.json() - - def verify_aadhaar_otp(self, data): - path = "/v1/registration/aadhaar/verifyOTP" - response = self.api.post(path, data) - return response.json() - - def check_and_generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP" - response = self.api.post(path, data) - return response.json() - - def generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/generateMobileOTP" - response = self.api.post(path, data) - return response.json() - - # /v1/registration/aadhaar/verifyMobileOTP - def verify_mobile_otp(self, data): - path = "/v1/registration/aadhaar/verifyMobileOTP" - response = self.api.post(path, data) - return response.json() - - # /v1/registration/aadhaar/createHealthIdWithPreVerified - def create_health_id(self, data): - path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" - logger.info(f"Creating Health ID with data: {data}") - # data.pop("healthId", None) - response = self.api.post(path, data) - return response.json() - - # /v1/search/existsByHealthId - # API checks if ABHA Address/ABHA Number is reserved/used which includes permanently deleted ABHA Addresses - # Return { status: true } - def exists_by_health_id(self, data): - path = "/v1/search/existsByHealthId" - response = self.api.post(path, data) - return response.json() - - # /v1/search/searchByHealthId - # API returns only Active or Deactive ABHA Number/ Address (Never returns Permanently Deleted ABHA Number/Address) - # Returns { - # "authMethods": [ - # "AADHAAR_OTP" - # ], - # "healthId": "deepakndhm", - # "healthIdNumber": "43-4221-5105-6749", - # "name": "kishan kumar singh", - # "status": "ACTIVE" - # } - def search_by_health_id(self, data): - path = "/v1/search/searchByHealthId" - response = self.api.post(path, data) - return response.json() - - # /v1/search/searchByMobile - def search_by_mobile(self, data): - path = "/v1/search/searchByMobile" - response = self.api.post(path, data) - return response.json() - - # Auth APIs - - # /v1/auth/init - def auth_init(self, data): - path = "/v1/auth/init" - response = self.api.post(path, data) - return response.json() - - # /v1/auth/confirmWithAadhaarOtp - def confirm_with_aadhaar_otp(self, data): - path = "/v1/auth/confirmWithAadhaarOtp" - response = self.api.post(path, data) - return response.json() - - # /v1/auth/confirmWithMobileOTP - def confirm_with_mobile_otp(self, data): - path = "/v1/auth/confirmWithMobileOTP" - response = self.api.post(path, data) - return response.json() - - # /v1/auth/confirmWithDemographics - def confirm_with_demographics(self, data): - path = "/v1/auth/confirmWithDemographics" - response = self.api.post(path, data) - return response.json() - - def verify_demographics(self, health_id, name, gender, year_of_birth): - auth_init_response = HealthIdGateway().auth_init( - {"authMethod": "DEMOGRAPHICS", "healthid": health_id} - ) - if "txnId" in auth_init_response: - demographics_response = HealthIdGateway().confirm_with_demographics( - { - "txnId": auth_init_response["txnId"], - "name": name, - "gender": gender, - "yearOfBirth": year_of_birth, - } - ) - return "status" in demographics_response and demographics_response["status"] - - return False - - # /v1/auth/generate/access-token - def generate_access_token(self, data): - if "access_token" in data: - return data["access_token"] - elif "accessToken" in data: - return data["accessToken"] - elif "token" in data: - return data["token"] - - if "refreshToken" in data: - refreshToken = data["refreshToken"] - elif "refresh_token" in data: - refreshToken = data["refresh_token"] - else: - return None - path = "/v1/auth/generate/access-token" - response = self.api.post(path, {"refreshToken": refreshToken}) - return response.json()["accessToken"] - - # Account APIs - - # /v1/account/profile - def get_profile(self, data): - path = "/v1/account/profile" - access_token = self.generate_access_token(data) - response = self.api.get(path, {}, access_token) - return response.json() - - # /v1/account/getPngCard - def get_abha_card_png(self, data): - path = "/v1/account/getPngCard" - access_token = self.generate_access_token(data) - response = self.api.get(path, {}, access_token) - - return b64encode(response.content) - - def get_abha_card_pdf(self, data): - path = "/v1/account/getCard" - access_token = self.generate_access_token(data) - response = self.api.get(path, {}, access_token) - - return b64encode(response.content) - - # /v1/account/qrCode - def get_qr_code(self, data, auth): - path = "/v1/account/qrCode" - access_token = self.generate_access_token(data) - logger.info(f"Getting QR Code for: {data}") - response = self.api.get(path, {}, access_token) - logger.info(f"QR Code Response: {response.text}") - return response.json() - - -class HealthIdGatewayV2: - def __init__(self): - self.api = APIGateway("health", None) - - # V2 APIs - def generate_aadhaar_otp(self, data): - path = "/v2/registration/aadhaar/generateOtp" - data["aadhaar"] = encrypt_with_public_key(data["aadhaar"]) - data.pop("cancelToken", {}) - response = self.api.post(path, data) - return response.json() - - def generate_document_mobile_otp(self, data): - path = "/v2/document/generate/mobile/otp" - data["mobile"] = "ENTER MOBILE NUMBER HERE" # Hard Coding for test - data.pop("cancelToken", {}) - response = self.api.post(path, data) - return response.json() - - def verify_document_mobile_otp(self, data): - path = "/v2/document/verify/mobile/otp" - data["otp"] = encrypt_with_public_key(data["otp"]) - data.pop("cancelToken", {}) - response = self.api.post(path, data) - return response.json() - - -class AbdmGateway: - # TODO: replace this with in-memory db (redis) - temp_memory = {} - - def __init__(self): - self.api = APIGateway("abdm_gateway", None) - - def get_hip_id_by_health_id(self, health_id): - abha_number = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - if not abha_number: - raise ValidationError(detail="No ABHA Number found") - - patient_facility = abha_number.patient.last_consultation.facility - if not getattr(patient_facility, "healthfacility", None): - raise ValidationError(detail="Health Facility not linked") - - return patient_facility.healthfacility.hf_id - - def add_care_context(self, access_token, request_id): - if request_id not in self.temp_memory: - return None - - data = self.temp_memory[request_id] - - if "consultationId" in data: - consultation = PatientConsultation.objects.get( - external_id=data["consultationId"] - ) - - response = self.add_contexts( - { - "access_token": access_token, - "patient_id": str(consultation.patient.external_id), - "patient_name": consultation.patient.name, - "context_id": str(consultation.external_id), - "context_name": f"Encounter: {consultation.created_date.date()!s}", - } - ) - - return response - - return False - - def save_linking_token(self, patient, access_token, request_id): - if request_id not in self.temp_memory: - return None - - data = self.temp_memory[request_id] - health_id = patient and patient["id"] or data["healthId"] - - abha_object = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - - if abha_object: - abha_object.access_token = access_token - abha_object.save() - return True - - return False - - # /v0.5/users/auth/fetch-modes - def fetch_modes(self, data): - path = "/v0.5/users/auth/fetch-modes" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - request_id = str(uuid.uuid4()) - - self.temp_memory[request_id] = data - if "authMode" in data and data["authMode"] == "DIRECT": - self.init(request_id) - return None - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "id": data["healthId"], - "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "requester": { - "type": "HIP", - "id": self.get_hip_id_by_health_id(data["healthId"]), - }, - }, - } - response = self.api.post(path, payload, None, additional_headers) - return response - - # "/v0.5/users/auth/init" - def init(self, prev_request_id): - if prev_request_id not in self.temp_memory: - return None - - path = "/v0.5/users/auth/init" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - data = self.temp_memory[prev_request_id] - self.temp_memory[request_id] = data - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "id": data["healthId"], - "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "authMode": data["authMode"] if "authMode" in data else "DEMOGRAPHICS", - "requester": { - "type": "HIP", - "id": self.get_hip_id_by_health_id(data["healthId"]), - }, - }, - } - response = self.api.post(path, payload, None, additional_headers) - return response - - # "/v0.5/users/auth/confirm" - def confirm(self, transaction_id, prev_request_id): - if prev_request_id not in self.temp_memory: - return None - - path = "/v0.5/users/auth/confirm" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - data = self.temp_memory[prev_request_id] - self.temp_memory[request_id] = data - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "transactionId": transaction_id, - "credential": { - "demographic": { - "name": data["name"], - "gender": data["gender"], - "dateOfBirth": data["dateOfBirth"], - }, - "authCode": "", - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def auth_on_notify(self, data): - path = "/v0.5/links/link/on-init" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "acknowledgement": {"status": "OK"}, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - # TODO: make it dynamic and call it at discharge (call it from on_confirm) - def add_contexts(self, data): - path = "/v0.5/links/link/add-contexts" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "link": { - "accessToken": data["access_token"], - "patient": { - "referenceNumber": data["patient_id"], - "display": data["patient_name"], - "careContexts": [ - { - "referenceNumber": data["context_id"], - "display": data["context_name"], - } - ], - }, - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_discover(self, data): - path = "/v0.5/care-contexts/on-discover" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "transactionId": data["transaction_id"], - "patient": { - "referenceNumber": data["patient_id"], - "display": data["patient_name"], - "careContexts": list( - map( - lambda context: { - "referenceNumber": context["id"], - "display": context["name"], - }, - data["care_contexts"], - ) - ), - "matchedBy": data["matched_by"], - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_link_init(self, data): - path = "/v0.5/links/link/on-init" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "transactionId": data["transaction_id"], - "link": { - "referenceNumber": data["patient_id"], - "authenticationType": "DIRECT", - "meta": { - "communicationMedium": "MOBILE", - "communicationHint": data["phone"], - "communicationExpiry": str( - (datetime.now() + timedelta(minutes=15)).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ) - ), - }, - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_link_confirm(self, data): - path = "/v0.5/links/link/on-confirm" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "patient": { - "referenceNumber": data["patient_id"], - "display": data["patient_name"], - "careContexts": list( - map( - lambda context: { - "referenceNumber": context["id"], - "display": context["name"], - }, - data["care_contexts"], - ) - ), - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_notify(self, data): - path = "/v0.5/consents/hip/on-notify" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "acknowledgement": {"status": "OK", "consentId": data["consent_id"]}, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_data_request(self, data): - path = "/v0.5/health-information/hip/on-request" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "hiRequest": { - "transactionId": data["transaction_id"], - "sessionStatus": "ACKNOWLEDGED", - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def data_transfer(self, data): - auth_header = Request("").auth_header() - - if not auth_header: - return None - - headers = { - "Content-Type": "application/json", - **auth_header, - } - - payload = { - "pageNumber": 1, - "pageCount": 1, - "transactionId": data["transaction_id"], - "entries": list( - map( - lambda context: { - "content": context["data"], - "media": "application/fhir+json", - "checksum": "string", - "careContextReference": context["consultation_id"], - }, - data["care_contexts"], - ) - ), - "keyMaterial": data["key_material"], - } - - response = requests.post( - data["data_push_url"], data=json.dumps(payload), headers=headers - ) - return response - - def data_notify(self, data): - path = "/v0.5/health-information/notify" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "consentId": data["consent_id"], - "transactionId": data["transaction_id"], - "doneAt": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "statusNotification": { - "sessionStatus": data["session_status"], - "hipId": self.get_hip_id_by_health_id(data["health_id"]), - "statusResponses": list( - map( - lambda context: { - "careContextReference": context["id"], - "hiStatus": "OK", - "description": "success", # not sure what to put - }, - data["care_contexts"], - ) - ), - }, - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def patient_status_on_notify(self, data): - path = "/v0.5/patients/status/on-notify" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "acknowledgement": {"status": "OK"}, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def patient_sms_notify(self, data): - path = "/v0.5/patients/sms/notify2" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "phoneNo": f"+91-{data['phone']}", - "hip": {"id": self.get_hip_id_by_health_id(data["healthId"])}, - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - # /v1.0/patients/profile/on-share - def on_share(self, data): - path = "/v1.0/patients/profile/on-share" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - response = self.api.post(path, data, None, additional_headers) - return response - - -class Bridge: - def __init__(self): - self.api = APIGateway("abdm_devservice", None) - - def add_update_service(self, data): - path = "/v1/bridges/addUpdateServices" - response = self.api.post(path, data, method="PUT") - return response - - -class Facility: - def __init__(self) -> None: - self.api = APIGateway("facility", None) - - def add_update_service(self, data): - path = "/v1/bridges/MutipleHRPAddUpdateServices" - response = self.api.post(path, data, method="POST") - return response diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py deleted file mode 100644 index 5a48430956..0000000000 --- a/care/abdm/utils/cipher.py +++ /dev/null @@ -1,95 +0,0 @@ -import json - -import requests -from django.conf import settings - - -class Cipher: - server_url = settings.FIDELIUS_URL - - def __init__( - self, - external_public_key, - external_nonce, - internal_private_key=None, - internal_public_key=None, - internal_nonce=None, - ): - self.external_public_key = external_public_key - self.external_nonce = external_nonce - - self.internal_private_key = internal_private_key - self.internal_public_key = internal_public_key - self.internal_nonce = internal_nonce - - self.key_to_share = None - - def generate_key_pair(self): - response = requests.get(f"{self.server_url}/keys/generate") - - if response.status_code == 200: - key_material = response.json() - - self.internal_private_key = key_material["privateKey"] - self.internal_public_key = key_material["publicKey"] - self.internal_nonce = key_material["nonce"] - - return key_material - - return None - - def encrypt(self, paylaod): - if not self.internal_private_key: - key_material = self.generate_key_pair() - - if not key_material: - return None - - response = requests.post( - f"{self.server_url}/encrypt", - headers={"Content-Type": "application/json"}, - data=json.dumps( - { - "receiverPublicKey": self.external_public_key, - "receiverNonce": self.external_nonce, - "senderPrivateKey": self.internal_private_key, - "senderPublicKey": self.internal_public_key, - "senderNonce": self.internal_nonce, - "plainTextData": paylaod, - } - ), - ) - - if response.status_code == 200: - data = response.json() - self.key_to_share = data["keyToShare"] - - return { - "public_key": self.key_to_share, - "data": data["encryptedData"], - "nonce": self.internal_nonce, - } - - return None - - def decrypt(self, paylaod): - response = requests.post( - f"{self.server_url}/decrypt", - headers={"Content-Type": "application/json"}, - data=json.dumps( - { - "receiverPrivateKey": self.internal_private_key, - "receiverNonce": self.internal_nonce, - "senderPublicKey": self.external_public_key, - "senderNonce": self.external_nonce, - "encryptedData": paylaod, - } - ), - ) - - if response.status_code == 200: - data = response.json() - - return data["decryptedData"] - - return None diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py deleted file mode 100644 index eb9db58f2e..0000000000 --- a/care/abdm/utils/fhir.py +++ /dev/null @@ -1,1220 +0,0 @@ -import base64 -from datetime import UTC, datetime -from uuid import uuid4 as uuid - -from fhir.resources.address import Address -from fhir.resources.annotation import Annotation -from fhir.resources.attachment import Attachment -from fhir.resources.bundle import Bundle, BundleEntry -from fhir.resources.careplan import CarePlan -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.coding import Coding -from fhir.resources.composition import Composition, CompositionSection -from fhir.resources.condition import Condition -from fhir.resources.contactpoint import ContactPoint -from fhir.resources.diagnosticreport import DiagnosticReport -from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent -from fhir.resources.dosage import Dosage -from fhir.resources.encounter import Encounter, EncounterDiagnosis -from fhir.resources.humanname import HumanName -from fhir.resources.identifier import Identifier -from fhir.resources.immunization import Immunization, ImmunizationProtocolApplied -from fhir.resources.medication import Medication -from fhir.resources.medicationrequest import MedicationRequest -from fhir.resources.meta import Meta -from fhir.resources.observation import Observation, ObservationComponent -from fhir.resources.organization import Organization -from fhir.resources.patient import Patient -from fhir.resources.period import Period -from fhir.resources.practitioner import Practitioner -from fhir.resources.procedure import Procedure -from fhir.resources.quantity import Quantity -from fhir.resources.reference import Reference - -from care.facility.models.file_upload import FileUpload -from care.facility.models.icd11_diagnosis import REVERSE_CONDITION_VERIFICATION_STATUSES -from care.facility.models.patient_investigation import InvestigationValue -from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id - - -class Fhir: - def __init__(self, consultation): - self.consultation = consultation - - self._patient_profile = None - self._practitioner_profile = None - self._organization_profile = None - self._encounter_profile = None - self._careplan_profile = None - self._diagnostic_report_profile = None - self._immunization_profile = None - self._medication_profiles = [] - self._medication_request_profiles = [] - self._observation_profiles = [] - self._document_reference_profiles = [] - self._condition_profiles = [] - self._procedure_profiles = [] - - def _reference_url(self, resource=None): - if resource is None: - return "" - - return f"{resource.resource_type}/{resource.id}" - - def _reference(self, resource=None): - if resource is None: - return None - - return Reference(reference=self._reference_url(resource)) - - def _patient(self): - if self._patient_profile is not None: - return self._patient_profile - - id = str(self.consultation.patient.external_id) - name = self.consultation.patient.name - gender = self.consultation.patient.gender - self._patient_profile = Patient( - id=id, - identifier=[Identifier(value=id)], - name=[HumanName(text=name)], - gender="male" if gender == 1 else "female" if gender == 2 else "other", - ) - - return self._patient_profile - - def _practioner(self): - if self._practitioner_profile is not None: - return self._practitioner_profile - - id = str(uuid()) - name = ( - ( - self.consultation.treating_physician - and f"{self.consultation.treating_physician.first_name} {self.consultation.treating_physician.last_name}" - ) - or self.consultation.deprecated_verified_by - or f"{self.consultation.created_by.first_name} {self.consultation.created_by.last_name}" - ) - self._practitioner_profile = Practitioner( - id=id, - identifier=[Identifier(value=id)], - name=[HumanName(text=name)], - ) - - return self._practitioner_profile - - def _organization(self): - if self._organization_profile is not None: - return self._organization_profile - - id = str(self.consultation.facility.external_id) - hip_id = "IN3210000017" # TODO: make it dynamic - name = self.consultation.facility.name - phone = self.consultation.facility.phone_number - address = self.consultation.facility.address - local_body = self.consultation.facility.local_body.name - district = self.consultation.facility.district.name - state = self.consultation.facility.state.name - pincode = self.consultation.facility.pincode - self._organization_profile = Organization( - id=id, - identifier=[ - Identifier(system="https://facilitysbx.ndhm.gov.in", value=hip_id) - ], - name=name, - telecom=[ContactPoint(system="phone", value=phone)], - address=[ - Address( - line=[address, local_body], - district=district, - state=state, - postalCode=pincode, - country="INDIA", - ) - ], - ) - - return self._organization_profile - - def _condition(self, diagnosis_id, verification_status): - diagnosis = get_icd11_diagnosis_object_by_id(diagnosis_id) - [code, label] = diagnosis.label.split(" ", 1) - condition_profile = Condition( - id=diagnosis_id, - identifier=[Identifier(value=diagnosis_id)], - category=[ - CodeableConcept( - coding=[ - Coding( - system="http://terminology.hl7.org/CodeSystem/condition-category", - code="encounter-diagnosis", - display="Encounter Diagnosis", - ) - ], - text="Encounter Diagnosis", - ) - ], - verificationStatus=CodeableConcept( - coding=[ - Coding( - system="http://terminology.hl7.org/CodeSystem/condition-ver-status", - code=verification_status, - display=REVERSE_CONDITION_VERIFICATION_STATUSES[ - verification_status - ], - ) - ] - ), - code=CodeableConcept( - coding=[ - Coding( - system="http://id.who.int/icd/release/11/mms", - code=code, - display=label, - ) - ], - text=diagnosis.label, - ), - subject=self._reference(self._patient()), - ) - - self._condition_profiles.append(condition_profile) - return condition_profile - - def _procedure(self, procedure): - procedure_profile = Procedure( - id=str(uuid()), - status="completed", - code=CodeableConcept( - text=procedure["procedure"], - ), - subject=self._reference(self._patient()), - performedDateTime=( - f"{procedure['time']}:00+05:30" if not procedure["repetitive"] else None - ), - performedString=( - f"Every {procedure['frequency']}" if procedure["repetitive"] else None - ), - ) - - self._procedure_profiles.append(procedure_profile) - return procedure_profile - - def _careplan(self): - if self._careplan_profile: - return self._careplan_profile - - self._careplan_profile = CarePlan( - id=str(uuid()), - status="completed", - intent="plan", - title="Care Plan", - description="This includes Treatment Summary, Prescribed Medication, General Notes and Special Instructions", - period=Period( - start=self.consultation.encounter_date.isoformat(), - end=( - self.consultation.discharge_date.isoformat() - if self.consultation.discharge_date - else None - ), - ), - note=[ - Annotation(text=self.consultation.treatment_plan), - Annotation(text=self.consultation.consultation_notes), - Annotation(text=self.consultation.special_instruction), - ], - subject=self._reference(self._patient()), - ) - - return self._careplan_profile - - def _diagnostic_report(self): - if self._diagnostic_report_profile: - return self._diagnostic_report_profile - - self._diagnostic_report_profile = DiagnosticReport( - id=str(uuid()), - status="final", - code=CodeableConcept(text="Investigation/Test Results"), - result=list( - map( - lambda investigation: self._reference( - self._observation( - title=investigation.investigation.name, - value={ - "value": investigation.value, - "unit": investigation.investigation.unit, - }, - id=str(investigation.external_id), - date=investigation.created_date.isoformat(), - ) - ), - InvestigationValue.objects.filter(consultation=self.consultation), - ) - ), - subject=self._reference(self._patient()), - performer=[self._reference(self._organization())], - resultsInterpreter=[self._reference(self._organization())], - conclusion="Refer to Doctor. To be correlated with further study.", - ) - - return self._diagnostic_report_profile - - def _observation(self, title, value, id, date): - if not value or (isinstance(value, dict) and not value["value"]): - return None - - return Observation( - id=( - f"{id}.{title.replace(' ', '').replace('_', '-')}" - if id and title - else str(uuid()) - ), - status="final", - effectiveDateTime=date if date else None, - code=CodeableConcept(text=title), - valueQuantity=( - Quantity(value=str(value["value"]), unit=value["unit"]) - if isinstance(value, dict) - else None - ), - valueString=value if isinstance(value, str) else None, - component=( - list( - map( - lambda component: ObservationComponent( - code=CodeableConcept(text=component["title"]), - valueQuantity=( - Quantity( - value=component["value"], unit=component["unit"] - ) - if isinstance(component, dict) - else None - ), - valueString=( - component if isinstance(component, str) else None - ), - ), - value, - ) - ) - if isinstance(value, list) - else None - ), - ) - - def _observations_from_daily_round(self, daily_round): - id = str(daily_round.external_id) - date = daily_round.created_date.isoformat() - observation_profiles = [ - self._observation( - "Temperature", - {"value": daily_round.temperature, "unit": "F"}, - id, - date, - ), - self._observation( - "SpO2", - {"value": daily_round.ventilator_spo2, "unit": "%"}, - id, - date, - ), - self._observation( - "Pulse", - {"value": daily_round.pulse, "unit": "bpm"}, - id, - date, - ), - self._observation( - "Resp", - {"value": daily_round.resp, "unit": "bpm"}, - id, - date, - ), - self._observation( - "Blood Pressure", - ( - [ - { - "title": "Systolic Blood Pressure", - "value": daily_round.bp["systolic"], - "unit": "mmHg", - }, - { - "title": "Diastolic Blood Pressure", - "value": daily_round.bp["diastolic"], - "unit": "mmHg", - }, - ] - if "systolic" in daily_round.bp and "diastolic" in daily_round.bp - else None - ), - id, - date, - ), - ] - - # TODO: do it for other fields like bp, pulse, spo2, ... - - observation_profiles = list( - filter(lambda profile: profile is not None, observation_profiles) - ) - self._observation_profiles.extend(observation_profiles) - return observation_profiles - - def _encounter(self, include_diagnosis=False): - if self._encounter_profile is not None: - return self._encounter_profile - - id = str(self.consultation.external_id) - status = "finished" if self.consultation.discharge_date else "in-progress" - period_start = self.consultation.encounter_date.isoformat() - period_end = ( - self.consultation.discharge_date.isoformat() - if self.consultation.discharge_date - else None - ) - self._encounter_profile = Encounter( - **{ - "id": id, - "identifier": [Identifier(value=id)], - "status": status, - "class": Coding(code="IMP", display="Inpatient Encounter"), - "subject": self._reference(self._patient()), - "period": Period(start=period_start, end=period_end), - "diagnosis": ( - list( - map( - lambda consultation_diagnosis: EncounterDiagnosis( - condition=self._reference( - self._condition( - consultation_diagnosis.diagnosis_id, - consultation_diagnosis.verification_status, - ), - ) - ), - self.consultation.diagnoses.all(), - ) - ) - if include_diagnosis - else None - ), - } - ) - - return self._encounter_profile - - def _immunization(self): - if self._immunization_profile: - return self._immunization_profile - - if not self.consultation.patient.is_vaccinated: - return None - - self._immunization_profile = Immunization( - id=str(uuid()), - status="completed", - identifier=[ - Identifier( - type=CodeableConcept(text="Covin Id"), - value=self.consultation.patient.covin_id, - ) - ], - vaccineCode=CodeableConcept( - coding=[ - Coding( - system="http://snomed.info/sct", - code="1119305005", - display="COVID-19 antigen vaccine", - ) - ], - text=self.consultation.patient.vaccine_name, - ), - patient=self._reference(self._patient()), - route=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="47625008", - display="Intravenous route", - ) - ] - ), - occurrenceDateTime=self.consultation.patient.last_vaccinated_date.isoformat(), - protocolApplied=[ - ImmunizationProtocolApplied( - doseNumberPositiveInt=self.consultation.patient.number_of_doses - ) - ], - ) - - def _document_reference(self, file): - id = str(file.external_id) - content_type, content = file.file_contents() - document_reference_profile = DocumentReference( - id=id, - identifier=[Identifier(value=id)], - status="current", - type=CodeableConcept(text=file.internal_name.split(".")[0]), - content=[ - DocumentReferenceContent( - attachment=Attachment( - contentType=content_type, data=base64.b64encode(content) - ) - ) - ], - author=[self._reference(self._organization())], - ) - - self._document_reference_profiles.append(document_reference_profile) - return document_reference_profile - - def _medication(self, name): - medication_profile = Medication(id=str(uuid()), code=CodeableConcept(text=name)) - - self._medication_profiles.append(medication_profile) - return medication_profile - - def _medication_request(self, medicine): - id = str(uuid()) - prescription_date = ( - self.consultation.encounter_date.isoformat() - ) # TODO: change to the time of prescription - status = "unknown" # TODO: get correct status active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown - dosage_text = f"{medicine['dosage_new']} / {medicine['dosage']} for {medicine['days']} days" - - medication_profile = self._medication(medicine["medicine"]) - medication_request_profile = MedicationRequest( - id=id, - identifier=[Identifier(value=id)], - status=status, - intent="order", - authoredOn=prescription_date, - dosageInstruction=[Dosage(text=dosage_text)], - medicationReference=self._reference(medication_profile), - subject=self._reference(self._patient()), - requester=self._reference(self._practioner()), - ) - - self._medication_request_profiles.append(medication_request_profile) - return medication_profile, medication_request_profile - - def _prescription_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription record", - ) - ] - ), - title="Prescription", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="In Patient Prescriptions", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription record", - ) - ] - ), - entry=list( - map( - lambda medicine: self._reference( - self._medication_request(medicine)[1] - ), - self.consultation.discharge_advice, - ) - ), - ) - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _health_document_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record artifact", - ) - ] - ), - title="Health Document Record", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Health Document Record", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record artifact", - ) - ] - ), - entry=list( - map( - lambda file: self._reference( - self._document_reference(file) - ), - FileUpload.objects.filter( - associating_id=self.consultation.id - ), - ) - ), - ) - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _wellness_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - title="Wellness Record", - date=datetime.now(UTC).isoformat(), - section=list( - map( - lambda daily_round: CompositionSection( - title=f"Daily Round - {daily_round.created_date}", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - entry=list( - map( - lambda observation_profile: self._reference( - observation_profile - ), - self._observations_from_daily_round(daily_round), - ) - ), - ), - self.consultation.daily_rounds.all(), - ) - ), - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _immunization_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="41000179103", - display="Immunization Record", - ), - ], - ), - title="Immunization", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="IPD Immunization", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="41000179103", - display="Immunization Record", - ), - ], - ), - entry=[ - *( - [self._reference(self._immunization())] - if self._immunization() - else [] - ) - ], - emptyReason=( - None - if self._immunization() - else CodeableConcept( - coding=[Coding(code="notasked", display="Not Asked")] - ) - ), - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _diagnostic_report_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="721981007", - display="Diagnostic Report", - ), - ], - ), - title="Diagnostic Report", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Investigation Report", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="721981007", - display="Diagnostic Report", - ), - ], - ), - entry=[self._reference(self._diagnostic_report())], - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _discharge_summary_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="373942005", - display="Discharge Summary Record", - ) - ] - ), - title="Discharge Summary Document", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Prescribed medications", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription", - ) - ] - ), - entry=list( - map( - lambda medicine: self._reference( - self._medication_request(medicine)[1] - ), - self.consultation.discharge_advice, - ) - ), - ), - CompositionSection( - title="Health Documents", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record", - ) - ] - ), - entry=list( - map( - lambda file: self._reference( - self._document_reference(file) - ), - FileUpload.objects.filter( - associating_id=self.consultation.id - ), - ) - ), - ), - *list( - map( - lambda daily_round: CompositionSection( - title=f"Daily Round - {daily_round.created_date}", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - entry=list( - map( - lambda observation_profile: self._reference( - observation_profile - ), - self._observations_from_daily_round(daily_round), - ) - ), - ), - self.consultation.daily_rounds.all(), - ) - ), - CompositionSection( - title="Procedures", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="371525003", - display="Clinical procedure report", - ) - ] - ), - entry=list( - map( - lambda procedure: self._reference( - self._procedure(procedure) - ), - self.consultation.procedure, - ) - ), - ), - CompositionSection( - title="Care Plan", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="734163000", - display="Care Plan", - ) - ] - ), - entry=[self._reference(self._careplan())], - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter(include_diagnosis=True)), - author=[self._reference(self._organization())], - ) - - def _op_consultation_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="371530004", - display="Clinical consultation report", - ) - ] - ), - title="OP Consultation Document", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Prescribed medications", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription", - ) - ] - ), - entry=list( - map( - lambda medicine: self._reference( - self._medication_request(medicine)[1] - ), - self.consultation.discharge_advice, - ) - ), - ), - CompositionSection( - title="Health Documents", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record", - ) - ] - ), - entry=list( - map( - lambda file: self._reference( - self._document_reference(file) - ), - FileUpload.objects.filter( - associating_id=self.consultation.id - ), - ) - ), - ), - *list( - map( - lambda daily_round: CompositionSection( - title=f"Daily Round - {daily_round.created_date}", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - entry=list( - map( - lambda observation_profile: self._reference( - observation_profile - ), - self._observations_from_daily_round(daily_round), - ) - ), - ), - self.consultation.daily_rounds.all(), - ) - ), - CompositionSection( - title="Procedures", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="371525003", - display="Clinical procedure report", - ) - ] - ), - entry=list( - map( - lambda procedure: self._reference( - self._procedure(procedure) - ), - self.consultation.procedure, - ) - ), - ), - CompositionSection( - title="Care Plan", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="734163000", - display="Care Plan", - ) - ] - ), - entry=[self._reference(self._careplan())], - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter(include_diagnosis=True)), - author=[self._reference(self._organization())], - ) - - def _bundle_entry(self, resource): - return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) - - def create_prescription_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._prescription_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_request_profiles, - ) - ), - ], - ).json() - - def create_wellness_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._wellness_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_immunization_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._immunization_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - self._bundle_entry(self._immunization()), - ], - ).json() - - def create_diagnostic_report_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._diagnostic_report_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_health_document_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._health_document_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._document_reference_profiles, - ) - ), - ], - ).json() - - def create_discharge_summary_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._discharge_summary_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - self._bundle_entry(self._careplan()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_request_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._condition_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._procedure_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._document_reference_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_op_consultation_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._op_consultation_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - self._bundle_entry(self._careplan()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_request_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._condition_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._procedure_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._document_reference_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_record(self, record_type): - if record_type == "Prescription": - return self.create_prescription_record() - if record_type == "WellnessRecord": - return self.create_wellness_record() - if record_type == "ImmunizationRecord": - return self.create_immunization_record() - if record_type == "HealthDocumentRecord": - return self.create_health_document_record() - if record_type == "DiagnosticReport": - return self.create_diagnostic_report_record() - if record_type == "DischargeSummary": - return self.create_discharge_summary_record() - if record_type == "OPConsultation": - return self.create_op_consultation_record() - return self.create_discharge_summary_record() diff --git a/care/abdm/views.py b/care/abdm/views.py deleted file mode 100644 index 60f00ef0ef..0000000000 --- a/care/abdm/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. diff --git a/care/facility/management/commands/load_dummy_data.py b/care/facility/management/commands/load_dummy_data.py index 4a0633801c..ea57bf17a6 100644 --- a/care/facility/management/commands/load_dummy_data.py +++ b/care/facility/management/commands/load_dummy_data.py @@ -2,6 +2,13 @@ from django.core import management from django.core.management import BaseCommand, CommandError +from django.db.models.signals import ( + m2m_changed, + post_delete, + post_save, + pre_delete, + pre_save, +) class Command(BaseCommand): @@ -20,6 +27,20 @@ def handle(self, *args, **options): msg = "This command is not intended to be run in production environment." raise CommandError(msg) + # Disconnecting signals temporarily to avoid conflicts + signals_to_disconnect = [ + post_save, + post_delete, + pre_save, + pre_delete, + m2m_changed, + ] + original_receivers = {} + + for signal in signals_to_disconnect: + original_receivers[signal] = signal.receivers + signal.receivers = [] + try: management.call_command("loaddata", self.BASE_URL + "states.json") management.call_command("load_skill_data") @@ -32,3 +53,7 @@ def handle(self, *args, **options): management.call_command("populate_investigations") except Exception as e: raise CommandError(e) from e + finally: + # Reconnect original signals + for signal in signals_to_disconnect: + signal.receivers = original_receivers[signal] diff --git a/config/api_router.py b/config/api_router.py index b4a3aa0f4a..7e00754a00 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -3,12 +3,6 @@ from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_nested.routers import NestedSimpleRouter -from care.abdm.api.viewsets.abha_number import AbhaNumberViewSet -from care.abdm.api.viewsets.consent import ConsentViewSet -from care.abdm.api.viewsets.health_facility import HealthFacilityViewSet -from care.abdm.api.viewsets.health_information import HealthInformationViewSet -from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet -from care.abdm.api.viewsets.patients import PatientsViewSet from care.facility.api.viewsets.ambulance import AmbulanceViewSet from care.facility.api.viewsets.asset import ( AssetLocationViewSet, @@ -321,23 +315,6 @@ router.register("public/asset", AssetPublicViewSet, basename="public-asset") router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") -# ABDM endpoints -if settings.ENABLE_ABDM: - router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") - router.register("abdm/consent", ConsentViewSet, basename="abdm-consent") - router.register( - "abdm/health_information", - HealthInformationViewSet, - basename="abdm-healthinformation", - ) - router.register("abdm/patients", PatientsViewSet, basename="abdm-patients") - router.register("abdm/abha_numbers", AbhaNumberViewSet, basename="abdm-abhanumber") - -router.register( - "abdm/health_facility", HealthFacilityViewSet, basename="abdm-healthfacility" -) - - app_name = "api" urlpatterns = [ path("", include(router.urls)), diff --git a/config/authentication.py b/config/authentication.py index 086348b1bc..619ae9ab22 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -1,9 +1,7 @@ -import json import logging import jwt import requests -from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.cache import cache from django.core.exceptions import ValidationError @@ -199,57 +197,6 @@ def get_user(self, validated_token, facility): return asset_user -class ABDMAuthentication(JWTAuthentication): - def open_id_authenticate(self, url, token): - public_key = requests.get(url, timeout=OPENID_REQUEST_TIMEOUT) - jwk = public_key.json()["keys"][0] - public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) - return jwt.decode( - token, key=public_key, audience="account", algorithms=["RS256"] - ) - - def authenticate_header(self, request): - return "Bearer" - - def authenticate(self, request): - jwt_token = self.get_header(request) - if jwt_token is None: - return None - jwt_token = self.get_jwt_token(jwt_token) - - abdm_cert_url = f"{settings.ABDM_URL}/gateway/v0.5/certs" - validated_token = self.get_validated_token(abdm_cert_url, jwt_token) - - return self.get_user(validated_token), validated_token - - def get_jwt_token(self, token): - return token.replace("Bearer", "").replace(" ", "") - - def get_validated_token(self, url, token): - try: - return self.open_id_authenticate(url, token) - except Exception as e: - logger.info(e, "Token: ", token) - raise InvalidToken({"detail": "Invalid Authorization token"}) from e - - def get_user(self, validated_token): - user = User.objects.filter(username=settings.ABDM_USERNAME).first() - if not user: - password = User.objects.make_random_password() - user = User( - username=settings.ABDM_USERNAME, - email="hcx@ohc.network", - password=f"{password}xyz", - gender=3, - phone_number="917777777777", - user_type=User.TYPE_VALUE_MAP["Volunteer"], - verified=True, - date_of_birth=timezone.now().date(), - ) - user.save() - return user - - class CustomJWTAuthenticationScheme(OpenApiAuthenticationExtension): target_class = "config.authentication.CustomJWTAuthentication" name = "jwtAuth" diff --git a/config/settings/base.py b/config/settings/base.py index 5bf8ddd3b6..df20ebb0cf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -125,7 +125,6 @@ ] LOCAL_APPS = [ "care.facility", - "care.abdm", "care.users", "care.audit_log", ] @@ -633,21 +632,6 @@ APP_VERSION = env("APP_VERSION", default="unknown") -# ABDM -ENABLE_ABDM = env.bool("ENABLE_ABDM", default=False) -ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") -ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") -ABDM_URL = env("ABDM_URL", default="https://dev.abdm.gov.in") -HEALTH_SERVICE_API_URL = env( - "HEALTH_SERVICE_API_URL", default="https://healthidsbx.abdm.gov.in/api" -) -ABDM_FACILITY_URL = env("ABDM_FACILITY_URL", default="https://facilitysbx.abdm.gov.in") -HIP_NAME_PREFIX = env("HIP_NAME_PREFIX", default="") -HIP_NAME_SUFFIX = env("HIP_NAME_SUFFIX", default="") -ABDM_USERNAME = env("ABDM_USERNAME", default="abdm_user_internal") -X_CM_ID = env("X_CM_ID", default="sbx") -FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090") - IS_PRODUCTION = False PLAUSIBLE_HOST = env("PLAUSIBLE_HOST", default="") diff --git a/config/urls.py b/config/urls.py index c90bd2adc2..904af4b3f3 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,7 +9,6 @@ SpectacularSwaggerView, ) -from care.abdm.urls import abdm_urlpatterns from care.facility.api.viewsets.open_id import PublicJWKsView from care.facility.api.viewsets.patient_consultation import ( dev_preview_discharge_summary, @@ -34,7 +33,7 @@ path("ping/", ping, name="ping"), path("app_version/", app_version, name="app_version"), # Django Admin, use {% url 'admin:index' %} - path(f"{settings.ADMIN_URL.rstrip("/")}/", admin.site.urls), + path(f"{settings.ADMIN_URL.rstrip('/')}/", admin.site.urls), # Rest API path("api/v1/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path( @@ -75,9 +74,6 @@ *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), ] -if settings.ENABLE_ABDM: - urlpatterns += abdm_urlpatterns - if settings.DEBUG: # This allows the error pages to be debugged during development, just visit # these url in browser to see how these error pages look like. diff --git a/docker-compose.yaml b/docker-compose.yaml index 166297a383..b361cbdd8b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -37,12 +37,6 @@ services: ports: - "4566:4566" - fidelius: - image: khavinshankar/fidelius:latest - restart: unless-stopped - ports: - - "8092:8090" - volumes: postgres-data: redis-data: diff --git a/docker/.local.env b/docker/.local.env index b00327fc9b..83b5dadb33 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -26,3 +26,11 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem + +# ABDM envs: added to avoid test failures +ABDM_CLIENT_ID=SBX_001 +ABDM_CLIENT_SECRET=xxxx +ABDM_GATEWAY_URL=https://dev.abdm.gov.in +ABDM_ABHA_URL=https://abhasbx.abdm.gov.in +ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in +ABDM_CM_ID=sbx diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index f83f059d2a..0405816f59 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -5,7 +5,7 @@ ARG TYPST_VERSION=0.11.0 ENV PATH=/venv/bin:$PATH RUN apt-get update && apt-get install --no-install-recommends -y \ - build-essential libjpeg-dev zlib1g-dev \ + build-essential libjpeg-dev zlib1g-dev libgmp-dev \ libpq-dev gettext wget curl gnupg git \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* diff --git a/plug_config.py b/plug_config.py index c177c62cde..27be9de162 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,6 +1,13 @@ from plugs.manager import PlugManager from plugs.plug import Plug +abdm_plugin = Plug( + name="abdm", + package_name="git+https://github.com/ohcnetwork/care_abdm.git", + version="@main", + configs={}, +) + hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", @@ -8,6 +15,6 @@ configs={}, ) -plugs = [hcx_plugin] +plugs = [hcx_plugin, abdm_plugin] manager = PlugManager(plugs) From 7471c65360d7b352d152b2d57e3cb4149bbaaab5 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:33:51 +0530 Subject: [PATCH 10/27] added missing mandatory env to .local.env and .prebuild.env --- docker/.local.env | 2 ++ docker/.prebuilt.env | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/docker/.local.env b/docker/.local.env index 83b5dadb33..cfbd0d888a 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -34,3 +34,5 @@ ABDM_GATEWAY_URL=https://dev.abdm.gov.in ABDM_ABHA_URL=https://abhasbx.abdm.gov.in ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in ABDM_CM_ID=sbx +CURRENT_DOMAIN=https://care.ohc.network +BACKEND_DOMAIN=https://careapi.ohc.network diff --git a/docker/.prebuilt.env b/docker/.prebuilt.env index 8bcc36312e..18a9422988 100644 --- a/docker/.prebuilt.env +++ b/docker/.prebuilt.env @@ -39,3 +39,13 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem + +# ABDM envs: added to avoid test failures +ABDM_CLIENT_ID=SBX_001 +ABDM_CLIENT_SECRET=xxxx +ABDM_GATEWAY_URL=https://dev.abdm.gov.in +ABDM_ABHA_URL=https://abhasbx.abdm.gov.in +ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in +ABDM_CM_ID=sbx +CURRENT_DOMAIN=https://care.ohc.network +BACKEND_DOMAIN=https://careapi.ohc.network From f1c2f772f5efc922f2e2ae9831fd2658c8abeb0e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:55:01 +0530 Subject: [PATCH 11/27] removed abdm config from plug_config --- docker/.local.env | 10 ---------- docker/.prebuilt.env | 10 ---------- plug_config.py | 9 +-------- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/docker/.local.env b/docker/.local.env index cfbd0d888a..b00327fc9b 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -26,13 +26,3 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem - -# ABDM envs: added to avoid test failures -ABDM_CLIENT_ID=SBX_001 -ABDM_CLIENT_SECRET=xxxx -ABDM_GATEWAY_URL=https://dev.abdm.gov.in -ABDM_ABHA_URL=https://abhasbx.abdm.gov.in -ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in -ABDM_CM_ID=sbx -CURRENT_DOMAIN=https://care.ohc.network -BACKEND_DOMAIN=https://careapi.ohc.network diff --git a/docker/.prebuilt.env b/docker/.prebuilt.env index 18a9422988..8bcc36312e 100644 --- a/docker/.prebuilt.env +++ b/docker/.prebuilt.env @@ -39,13 +39,3 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem - -# ABDM envs: added to avoid test failures -ABDM_CLIENT_ID=SBX_001 -ABDM_CLIENT_SECRET=xxxx -ABDM_GATEWAY_URL=https://dev.abdm.gov.in -ABDM_ABHA_URL=https://abhasbx.abdm.gov.in -ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in -ABDM_CM_ID=sbx -CURRENT_DOMAIN=https://care.ohc.network -BACKEND_DOMAIN=https://careapi.ohc.network diff --git a/plug_config.py b/plug_config.py index 27be9de162..c177c62cde 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,13 +1,6 @@ from plugs.manager import PlugManager from plugs.plug import Plug -abdm_plugin = Plug( - name="abdm", - package_name="git+https://github.com/ohcnetwork/care_abdm.git", - version="@main", - configs={}, -) - hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", @@ -15,6 +8,6 @@ configs={}, ) -plugs = [hcx_plugin, abdm_plugin] +plugs = [hcx_plugin] manager = PlugManager(plugs) From ee5caf9bcad844d3943761b1660d2702ff97353f Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sat, 19 Oct 2024 02:01:50 +0530 Subject: [PATCH 12/27] Discard changes to plug_config.py --- plug_config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plug_config.py b/plug_config.py index c177c62cde..27be9de162 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,6 +1,13 @@ from plugs.manager import PlugManager from plugs.plug import Plug +abdm_plugin = Plug( + name="abdm", + package_name="git+https://github.com/ohcnetwork/care_abdm.git", + version="@main", + configs={}, +) + hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", @@ -8,6 +15,6 @@ configs={}, ) -plugs = [hcx_plugin] +plugs = [hcx_plugin, abdm_plugin] manager = PlugManager(plugs) From 2363119498da7b9663f58ae2707b0730deebadc9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 11:47:20 +0530 Subject: [PATCH 13/27] Fixed prod deploy: install libgmp-dev in prod (#2548) * fixed prod deploy: install libgmp-dev in prod * added libgmp-dev to runtime step in prod dockerfile --- docker/prod.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 877478c3a9..067b0498d6 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -20,7 +20,7 @@ WORKDIR $APP_HOME FROM base AS builder RUN apt-get update && apt-get install --no-install-recommends -y \ - build-essential libjpeg-dev zlib1g-dev libpq-dev git wget \ + build-essential libjpeg-dev zlib1g-dev libgmp-dev libpq-dev git wget \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* @@ -54,7 +54,7 @@ RUN python3 $APP_HOME/install_plugins.py FROM base AS runtime RUN apt-get update && apt-get install --no-install-recommends -y \ - libpq-dev gettext wget curl gnupg \ + libpq-dev libgmp-dev gettext wget curl gnupg \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* From 00cd5f9a3d9860d55fed44fcc78bc62ef1047186 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sat, 19 Oct 2024 12:46:31 +0530 Subject: [PATCH 14/27] Move code owners to backend admins --- .github/CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 04cb82a8da..e6d222c56c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1 @@ -* @ohcnetwork/care-developers -*.yml @tomahawk-pilot +* @ohcnetwork/care-backend-admins From fa48de84e787762355ff77fd178aa64433de2074 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:22:46 +0530 Subject: [PATCH 15/27] Bump newrelic from 10.0.0 to 10.1.0 (#2541) Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 10.0.0 to 10.1.0. - [Release notes](https://github.com/newrelic/newrelic-python-agent/releases) - [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v10.0.0...v10.1.0) --- updated-dependencies: - dependency-name: newrelic dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- Pipfile | 2 +- Pipfile.lock | 58 ++++++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/Pipfile b/Pipfile index 16f5923139..c95fa9289a 100644 --- a/Pipfile +++ b/Pipfile @@ -29,7 +29,7 @@ gunicorn = "==23.0.0" healthy-django = "==0.1.0" jsonschema = "==4.23.0" jwcrypto = "==1.5.6" -newrelic = "==10.0.0" +newrelic = "==10.1.0" pillow = "==10.4.0" psycopg = { extras = ["c"], version = "==3.2.2" } pycryptodome = "==3.20.0" diff --git a/Pipfile.lock b/Pipfile.lock index dda896d1fc..ee060a0463 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "82b40d895920ac109f51b1e331c891ba2c9e3ebe5e7feded2cd8cc01bbd948d0" + "sha256": "1c90ca755b86427eedcf4f48ff4aa792fd98b9ad9e0462f8463ba8e7283f9949" }, "pipfile-spec": 6, "requires": { @@ -1005,35 +1005,39 @@ }, "newrelic": { "hashes": [ - "sha256:002e21527c77c0c9640402c152d40a114b4cc821e7de93cf445fffaef160f1aa", - "sha256:01e68cf6826a3d456aaa0a4c88a7b864403428369b855c3d9c5c27958ef48adb", - "sha256:03a5068d68f22d80797a048a4018d673b8cdd646bc5f9fb63328b53b08bc6de7", - "sha256:0d1f0c1c54a301ee8f7c4372a8905a18cd36d9a2f9b6550898dd7bac147480d3", - "sha256:14e675e0a73e52fde94df9de89201de945cc3a2a046b4fdfe5ba1717b15cad78", - "sha256:1f4cd5ca11f08badd4b1cdd746053cfb30a09d5d9b9c1f5d911718d2870b4493", - "sha256:27d2f34bf714ef9d7ff8a68265a2094b87a4bdc7b1bbbd0a1421cf5cf8f33311", - "sha256:34b60d16d6e8fbc3e65a7d5171718999ecf7bc369cf8baae1bee3c6317972e18", - "sha256:4d09af04f86d40c534d3753bdc1e45e9ca76ce85cea7ea87994c77b3d0677381", - "sha256:548b538c3e95b589a30565bff668285ca74bb64069eb1d6f643bde9768944f53", - "sha256:6257413c9e261e8256be5cadb488945dbb3830dcc6091805fa3a5c70992a03a6", - "sha256:78bc57206c7747f7096ed081d828719def7c0952ea7834c7769d383bd7ba0aa6", - "sha256:8716867245ebe97656017e7a6ef17ebccb730e59062531e3e7b9ce9ffc7b4e4b", - "sha256:94c94a0a05e2995dff812f4fb85113227bcc5a24635539031842af9c1ddc4368", - "sha256:a8a16dfac53914dd0b930a2c087df701585d4f372b2c138466418e78d067b50f", - "sha256:a8c1b480bbe5c3e2e156f8de86182aa207430dbb32e0e5dc523ba8c3731328fc", - "sha256:b1352b6d357e82899ff102acb6971fb9c2cebe70c783081f11c3e53fddfedc4e", - "sha256:b60407fdb9798eee54488130bf87dfe542c3f04475c0c6b8c14e84274db5b1eb", - "sha256:bd3c73bbfac0a48402583aada21bf026161df8b73c6552cb8654f4a93f409860", - "sha256:c633972d88d89a2a17471b834961a889709f4970016e9641e3ddc0234669aadf", - "sha256:d6e09a66088431356c6c1a75bd1cdac2e425d547b47026138b254ac51d5df23d", - "sha256:d8bfbbb50ccc39a51a3449cdfb61970d7e61be0eb93336e3725857b7c1d17ff7", - "sha256:d90d41d78bd72d7fab7ed1cf34bf3dc519ab2d8c6820554061b8708eb7951374", - "sha256:f1aac4a5fe1d0cbe2bb9e2c52152604fb872a6bce28e129febd29d1d307df1f4", - "sha256:f446bf0943220e114e861bbc96761733e0877684ee860cf61755abe2d9805367" + "sha256:09cd9a92c0cc54ec10d0954518ee490168215c13d8b24dc2398d7f539a422442", + "sha256:0acfc2fbc41c25902ad90b30a623c7cd6663c720ff754b051d39e85d179adc7a", + "sha256:24ab3764904c093fe0b3ba2d5e951c887311180b0a039ccb3cfcc6e720abdd04", + "sha256:2f0691c056c2d0df0f268dc1d39ed9fd1bdac36250316d0e4fe4547adad9de20", + "sha256:315b6675a28611409536a9fcc8cc2a463e0c662fa5f5a385267d3e672da1ae88", + "sha256:35f88e14f6448d7fb45c9a2e9685e8a51b42c859b9075df7934c84a1244db738", + "sha256:379a4e815cd48106c4b9080be7d331088fefe8ae2568eb68975fa7f617619d52", + "sha256:38f4dc5800173636eedd607a6a4c1a7a228953ac86c92ac80a6191fb050ed2c0", + "sha256:55633a0655327cb769b2d0672861cea42cd535d0e5ef73bba0cbfd3a91603a0a", + "sha256:5fefb4de5fda9273e40d6242c88d7b3775fe096940e140123e0da21829b0720c", + "sha256:7c0317ebe7f2cd61fe7131e43c5dc7965ed4bfc5f64ee631fb304e9c86b8e1e0", + "sha256:80327c8c06f2e7fa7a58efc2dd7e00d480310f81055cc9970cd356d2de1ad655", + "sha256:99968ce04e5eae99a4ab7b58baf85556dbd665f947a8f816cc6e3bacad25785a", + "sha256:a9b3ebfbd63ade4d340feea77824f62f15a8d7d1ecba55c6437e9b32106a33d2", + "sha256:ad3e5ef7181fc1d35b59041f280e55a3195d6adc0b9c0e708e30b948c2ee0848", + "sha256:afcdcbc2be3c8f7926ddcd4559186f9fb4c37f0eee9c5a480b84870381e63f34", + "sha256:affdb3ee375d47bf1995ee4d17c8a817232ed2d88bb80438e6c5f13912b18b60", + "sha256:b1db3ec4f72a4691e9fd828f1665d0f823a077b479d0a6e690b8e9c31a34bc17", + "sha256:b77f6fbfb57b0b1fdcb5cc970a4fcac60b33c1500d4611061d8009515bf07783", + "sha256:ba231e7d45b6b12a5df233a2d33677fbf9fc77f45d5332a75e11bf190fac3152", + "sha256:c2c516a1a44c2c6ba9f2bc8e1a76f7b499c1fc62307f72733b1121a3de15a11b", + "sha256:d0f0fff322dc66658fe45d945268918dc89cc33d685246ea2062aac6c9cff821", + "sha256:d4845761aca14048118d7914ca765374a967b0665f84c2603609d259c93c82ba", + "sha256:d51db5922fc549b69e7b4fae70e9a91011a079e99bb380e8cc06933e6c4d26f7", + "sha256:dd139bb4c36658007ddb17b53b8e46c6a81e6e814806b79275b9cc0d3bd626a8", + "sha256:e44f0b8e938197d13e2ef23028882018e2ecbb1c51a974b34d5329482a4584f7", + "sha256:e7a0f80519aff6f52276075b4b6222eff6dbc6814073fc017034f4869c12c93b", + "sha256:f5920bf78a844e7c5bb267b4a44978e22f125618aafa32459211ab9208da3ab5", + "sha256:f6a082558163f5e36f6ef62081aaeb3528c3dbe2489bce7ecccdbea28363c02a" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==10.0.0" + "version": "==10.1.0" }, "packaging": { "hashes": [ From 8bac44a73e272fafb736ec59ca6cda330ffe1c04 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:26:04 +0530 Subject: [PATCH 16/27] added check for email provider check before sending reset password email (#2544) * added check for email provider check before sending email * improving test to check if the email is sent successfully --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/users/reset_password_views.py | 14 ++++++++++ care/users/tests/test_auth.py | 45 +++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/care/users/reset_password_views.py b/care/users/reset_password_views.py index 89f67ae087..e204ab0719 100644 --- a/care/users/reset_password_views.py +++ b/care/users/reset_password_views.py @@ -208,6 +208,20 @@ def post(self, request, *args, **kwargs): status=status.HTTP_429_TOO_MANY_REQUESTS, ) + if settings.IS_PRODUCTION and ( + not settings.EMAIL_HOST + or not settings.EMAIL_HOST_USER + or not settings.EMAIL_HOST_PASSWORD + ): + raise exceptions.ValidationError( + { + "detail": [ + _( + "There was a problem resetting your password. Please contact the administrator." + ) + ] + } + ) # before we continue, delete all existing expired tokens password_reset_token_validation_time = get_password_reset_token_expiry_time() diff --git a/care/users/tests/test_auth.py b/care/users/tests/test_auth.py index 695e105564..912e5da010 100644 --- a/care/users/tests/test_auth.py +++ b/care/users/tests/test_auth.py @@ -1,5 +1,6 @@ from datetime import timedelta +from django.core import mail from django.test import override_settings from django.utils.timezone import now from django_rest_passwordreset.models import ResetPasswordToken @@ -99,7 +100,7 @@ def test_auth_verify_with_invalid_token(self): self.assertEqual(response.data["detail"], "Token is invalid or expired") -@override_settings(DISABLE_RATELIMIT=True) +@override_settings(DISABLE_RATELIMIT=True, IS_PRODUCTION=False) class TestPasswordReset(TestUtils, APITestCase): @classmethod def setUpTestData(cls) -> None: @@ -118,13 +119,55 @@ def create_reset_password_token( token.save() return token + @override_settings( + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + ) def test_forgot_password_with_valid_input(self): + mail.outbox = [] + response = self.client.post( + "/api/v1/password_reset/", + {"username": self.user.username}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("Password Reset for Care", mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [self.user.email]) + self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) + self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) + + @override_settings(IS_PRODUCTION=True) + def test_forgot_password_without_email_configration(self): + response = self.client.post( + "/api/v1/password_reset/", + {"username": self.user.username}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.json()["detail"][0], + "There was a problem resetting your password. Please contact the administrator.", + ) + + @override_settings( + IS_PRODUCTION=True, + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + EMAIL_HOST="dummy.smtp.server", + EMAIL_HOST_USER="dummy-email@example.com", + EMAIL_HOST_PASSWORD="dummy-password", + ) + def test_forgot_password_with_email_configuration(self): + mail.outbox = [] + response = self.client.post( "/api/v1/password_reset/", {"username": self.user.username}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("Password Reset for Care", mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [self.user.email]) self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) def test_forgot_password_with_missing_fields(self): From 3bd5e4e048dd3b7cc7fba53d9b903dfca345ef54 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 23 Oct 2024 20:02:21 +0530 Subject: [PATCH 17/27] fixes validation preventing linking multiple cameras to a bed (#2559) --- care/facility/api/serializers/bed.py | 17 ++++++----- care/facility/tests/test_asset_bed_api.py | 36 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 508c2f9619..031d2a68c1 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -111,6 +111,10 @@ def validate(self, attrs): not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): raise PermissionError + if AssetBed.objects.filter(asset=asset, bed=bed).exists(): + raise ValidationError( + {"non_field_errors": "Asset is already linked to bed"} + ) if asset.asset_class not in [ AssetClasses.HL7MONITOR.name, AssetClasses.ONVIF.name, @@ -123,18 +127,15 @@ def validate(self, attrs): {"asset": "Should be in the same facility as the bed"} ) if ( - asset.asset_class - in [ - AssetClasses.HL7MONITOR.name, - AssetClasses.ONVIF.name, - ] + asset.asset_class == AssetClasses.HL7MONITOR.name + and AssetBed.objects.filter( + bed=bed, asset__asset_class=asset.asset_class + ).exists() ) and AssetBed.objects.filter( bed=bed, asset__asset_class=asset.asset_class ).exists(): raise ValidationError( - { - "asset": "Bed is already in use by another asset of the same class" - } + {"asset": "Another HL7 Monitor is already linked to this bed."} ) else: raise ValidationError( diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py index d22aae9bfd..4ed81a36b8 100644 --- a/care/facility/tests/test_asset_bed_api.py +++ b/care/facility/tests/test_asset_bed_api.py @@ -21,9 +21,21 @@ def setUpTestData(cls): ) cls.asset_location = cls.create_asset_location(cls.facility) cls.asset = cls.create_asset(cls.asset_location) + cls.monitor_asset_1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.monitor_asset_2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name + ) cls.camera_asset = cls.create_asset( cls.asset_location, asset_class=AssetClasses.ONVIF.name ) + cls.camera_asset_1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 1" + ) + cls.camera_asset_2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 2" + ) cls.bed = cls.create_bed(cls.facility, cls.asset_location) def test_link_disallowed_asset_class_asset_to_bed(self): @@ -49,6 +61,30 @@ def test_link_asset_to_bed_and_attempt_duplicate_linking(self): self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.data["count"], 1) + def test_linking_multiple_cameras_to_a_bed(self): + data = { + "asset": self.camera_asset_1.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + # Attempt linking another camera to same bed. + data["asset"] = self.camera_asset_2.external_id + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_linking_multiple_hl7_monitors_to_a_bed(self): + data = { + "asset": self.monitor_asset_1.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + # Attempt linking another hl7 monitor to same bed. + data["asset"] = self.monitor_asset_2.external_id + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): @classmethod From 5b735dcc08386daba5de0afbb8dddf87561b4858 Mon Sep 17 00:00:00 2001 From: Vysakh Premkumar <84713473+tellmeY18@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:13:00 +0530 Subject: [PATCH 18/27] Update deploy.yml (#2562) --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cccc03b16c..34b73cff0a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -102,7 +102,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} build-args: | APP_VERSION=${{ github.sha }} - ADDITIONAL_PLUGS=${{ secrets.ADDITIONAL_PLUGS }} + ADDITIONAL_PLUGS=${{ env.ADDITIONAL_PLUGS }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max From 69c2b831457ba1e47b68ada0eae9582d3c32b865 Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan <40040905+Jacobjeevan@users.noreply.github.com> Date: Thu, 24 Oct 2024 21:27:23 +0530 Subject: [PATCH 19/27] Asset location route: modifications for CNS (#2545) * CNS location - Add a parameter to filter for locations with no monitors - Also filter for monitors without paitents * Added new test cases for asset locations * using asset class instead of string * fix formatting --- care/facility/api/viewsets/asset.py | 25 ++++++++++- .../facility/tests/test_asset_location_api.py | 43 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index fc66eff4bf..8b24bebb51 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -58,6 +58,7 @@ AvailabilityRecord, StatusChoices, ) +from care.facility.models.bed import AssetBed, ConsultationBed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.cache.cache_allowed_facilities import get_accessible_facilities @@ -84,6 +85,27 @@ def delete_asset_cache(sender, instance, created, **kwargs): cache.delete("asset:qr:" + str(instance.id)) +class AssetLocationFilter(filters.FilterSet): + bed_is_occupied = filters.BooleanFilter(method="filter_bed_is_occupied") + + def filter_bed_is_occupied(self, queryset, name, value): + asset_locations = ( + AssetBed.objects.select_related("asset", "bed") + .filter(asset__asset_class=AssetClasses.HL7MONITOR.name) + .values_list("bed__location_id", "bed__id") + ) + if value: + asset_locations = asset_locations.filter( + bed__id__in=Subquery( + ConsultationBed.objects.filter( + bed__id=OuterRef("bed__id"), end_date__isnull=value + ).values("bed__id") + ) + ) + asset_locations = asset_locations.values_list("bed__location_id", flat=True) + return queryset.filter(id__in=asset_locations) + + class AssetLocationViewSet( ListModelMixin, RetrieveModelMixin, @@ -101,8 +123,9 @@ class AssetLocationViewSet( ) serializer_class = AssetLocationSerializer lookup_field = "external_id" - filter_backends = (drf_filters.SearchFilter,) + filter_backends = (filters.DjangoFilterBackend, drf_filters.SearchFilter) search_fields = ["name"] + filterset_class = AssetLocationFilter def get_serializer_context(self): facility = self.get_facility() diff --git a/care/facility/tests/test_asset_location_api.py b/care/facility/tests/test_asset_location_api.py index c2f95b8940..9e8280d617 100644 --- a/care/facility/tests/test_asset_location_api.py +++ b/care/facility/tests/test_asset_location_api.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase +from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.tests.test_utils import TestUtils @@ -15,8 +16,12 @@ def setUpTestData(cls) -> None: cls.asset_location = cls.create_asset_location(cls.facility) cls.asset_location_with_linked_bed = cls.create_asset_location(cls.facility) cls.asset_location_with_linked_asset = cls.create_asset_location(cls.facility) - cls.asset = cls.create_asset(cls.asset_location_with_linked_asset) + cls.asset = cls.create_asset( + cls.asset_location_with_linked_asset, + asset_class=AssetClasses.HL7MONITOR.name, + ) cls.bed = cls.create_bed(cls.facility, cls.asset_location_with_linked_bed) + cls.asset_bed = cls.create_asset_bed(cls.asset, cls.bed) cls.patient = cls.create_patient(cls.district, cls.facility) cls.consultation = cls.create_consultation(cls.patient, cls.facility) cls.consultation_bed = cls.create_consultation_bed(cls.consultation, cls.bed) @@ -24,6 +29,16 @@ def setUpTestData(cls) -> None: cls.deleted_asset = cls.create_asset(cls.asset_location) cls.deleted_asset.deleted = True cls.deleted_asset.save() + cls.asset_second_location = cls.create_asset_location( + cls.facility, name="asset2 location" + ) + cls.asset_second = cls.create_asset( + cls.asset_second_location, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.asset_bed_second = cls.create_bed(cls.facility, cls.asset_second_location) + cls.assetbed_second = cls.create_asset_bed( + cls.asset_second, cls.asset_bed_second + ) def test_list_asset_locations(self): response = self.client.get( @@ -31,6 +46,32 @@ def test_list_asset_locations(self): ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertContains(response, self.asset_location.external_id) + self.assertContains(response, self.asset_second_location.external_id) + + def test_asset_locations_get_monitors_all(self): + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=false" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_location_with_linked_bed.external_id) + self.assertContains(response, self.asset_second_location.external_id) + + def test_asset_locations_get_monitors_only_consultation_bed(self): + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=true" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_location_with_linked_bed.external_id) + + def test_asset_locations_get_only_monitors(self): + self.asset.asset_class = AssetClasses.VENTILATOR.name + self.asset.save() + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=false" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_second_location.external_id) + self.assertEqual(len(response.data["results"]), 1) def test_retrieve_asset_location(self): response = self.client.get( From 28f969950fe1286bdc922883a22562a9639adaf5 Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan <40040905+Jacobjeevan@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:16:59 +0530 Subject: [PATCH 20/27] Fix Immunologist doctor type Id (#2568) * fix immunologist doc type id * migration --- .../0467_alter_hospitaldoctors_area.py | 18 ++++++++++++++++++ care/facility/models/facility.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 care/facility/migrations/0467_alter_hospitaldoctors_area.py diff --git a/care/facility/migrations/0467_alter_hospitaldoctors_area.py b/care/facility/migrations/0467_alter_hospitaldoctors_area.py new file mode 100644 index 0000000000..d1507f0334 --- /dev/null +++ b/care/facility/migrations/0467_alter_hospitaldoctors_area.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.1 on 2024-10-28 13:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0466_camera_presets'), + ] + + operations = [ + migrations.AlterField( + model_name='hospitaldoctors', + name='area', + field=models.IntegerField(choices=[(1, 'General Medicine'), (2, 'Pulmonology'), (3, 'Intensivist'), (4, 'Pediatrician'), (5, 'Others'), (6, 'Anesthesiologist'), (7, 'Cardiac Surgeon'), (8, 'Cardiologist'), (9, 'Dentist'), (10, 'Dermatologist'), (11, 'Diabetologist'), (12, 'Emergency Medicine Physician'), (13, 'Endocrinologist'), (14, 'Family Physician'), (15, 'Gastroenterologist'), (16, 'General Surgeon'), (17, 'Geriatrician'), (18, 'Hematologist'), (19, 'Immunologist'), (20, 'Infectious Disease Specialist'), (21, 'MBBS doctor'), (22, 'Medical Officer'), (23, 'Nephrologist'), (24, 'Neuro Surgeon'), (25, 'Neurologist'), (26, 'Obstetrician/Gynecologist (OB/GYN)'), (27, 'Oncologist'), (28, 'Oncology Surgeon'), (29, 'Ophthalmologist'), (30, 'Oral and Maxillofacial Surgeon'), (31, 'Orthopedic'), (32, 'Orthopedic Surgeon'), (33, 'Otolaryngologist (ENT)'), (34, 'Palliative care Physician'), (35, 'Pathologist'), (36, 'Pediatric Surgeon'), (37, 'Physician'), (38, 'Plastic Surgeon'), (39, 'Psychiatrist'), (40, 'Pulmonologist'), (41, 'Radio technician'), (42, 'Radiologist'), (43, 'Rheumatologist'), (44, 'Sports Medicine Specialist'), (45, 'Thoraco-Vascular Surgeon'), (46, 'Transfusion Medicine Specialist'), (47, 'Urologist'), (48, 'Nurse'), (49, 'Allergist/Immunologist'), (50, 'Cardiothoracic Surgeon'), (51, 'Gynecologic Oncologist'), (52, 'Hepatologist'), (53, 'Internist'), (54, 'Neonatologist'), (55, 'Pain Management Specialist'), (56, 'Physiatrist (Physical Medicine and Rehabilitation)'), (57, 'Podiatrist'), (58, 'Preventive Medicine Specialist'), (59, 'Radiation Oncologist'), (60, 'Sleep Medicine Specialist'), (61, 'Transplant Surgeon'), (62, 'Trauma Surgeon'), (63, 'Vascular Surgeon'), (64, 'Critical Care Physician')]), + ), + ] diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index e3593871b7..01c0102b10 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -148,7 +148,7 @@ class FacilityFeature(models.IntegerChoices): (16, "General Surgeon"), (17, "Geriatrician"), (18, "Hematologist"), - (29, "Immunologist"), + (19, "Immunologist"), (20, "Infectious Disease Specialist"), (21, "MBBS doctor"), (22, "Medical Officer"), From 0a4b3381984eb14ae3059f2335861f163a26cb08 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 30 Oct 2024 12:20:58 +0530 Subject: [PATCH 21/27] Update dependencies (#2560) Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .pre-commit-config.yaml | 4 +- Pipfile | 38 +- Pipfile.lock | 2310 +++++++++++++++++++----------------- docker/dev.Dockerfile | 2 +- docker/prod.Dockerfile | 2 +- 6 files changed, 1229 insertions(+), 1129 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4bceff1ce5..019ec021aa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" cache: 'pipenv' - name: Install pipenv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cad6717e2..2969bbc4da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_stages: [commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: no-commit-to-branch args: [--branch, develop, --branch, staging, --branch, production] @@ -17,7 +17,7 @@ repos: - id: check-toml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.7 + rev: v0.7.0 hooks: - id: ruff args: [ --fix ] diff --git a/Pipfile b/Pipfile index c95fa9289a..b18cab9908 100644 --- a/Pipfile +++ b/Pipfile @@ -6,17 +6,17 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.3.2" -boto3 = "==1.35.29" +boto3 = "==1.35.49" celery = "==5.4.0" -django = "==5.1.1" +django = "==5.1.2" django-environ = "==0.11.2" -django-cors-headers = "==4.4.0" +django-cors-headers = "==4.5.0" django-filter = "==24.3" django-maintenance-mode = "==0.21.1" django-queryset-csv = "==1.1.0" django-ratelimit = "==4.1.0" django-redis = "==5.4.0" -django-rest-passwordreset = "==1.4.1" +django-rest-passwordreset = "==1.4.2" django-simple-history = "==3.7.0" djangoql = "==0.18.1" djangorestframework = "==3.15.2" @@ -28,39 +28,37 @@ drf-spectacular = "==0.27.2" gunicorn = "==23.0.0" healthy-django = "==0.1.0" jsonschema = "==4.23.0" -jwcrypto = "==1.5.6" -newrelic = "==10.1.0" -pillow = "==10.4.0" -psycopg = { extras = ["c"], version = "==3.2.2" } -pycryptodome = "==3.20.0" +newrelic = "==10.2.0" +pillow = "==11.0.0" +psycopg = { extras = ["c"], version = "==3.2.3" } pydantic = "==1.10.18" # fix for fhir.resources < 7.0.2 pyjwt = "==2.9.0" python-slugify = "==8.0.4" -pywebpush = "==2.0.0" +pywebpush = "==2.0.1" redis = { extras = ["hiredis"], version = "==5.0.8" } # constraint for redis-om redis-om = "==0.3.1" # > 0.3.1 broken with pydantic < 2 requests = "==2.32.3" -sentry-sdk = "==2.14.0" +sentry-sdk = "==2.17.0" whitenoise = "==6.7.0" [dev-packages] -boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.29" } -coverage = "==7.6.1" -debugpy = "==1.8.6" +boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.49" } +coverage = "==7.6.4" +debugpy = "==1.8.7" django-coverage-plugin = "==3.1.0" django-extensions = "==3.2.3" django-silk = "==5.2.0" djangorestframework-stubs = "==3.15.1" factory-boy = "==3.3.1" freezegun = "==1.5.1" -ipython = "==8.27.0" -mypy = "==1.11.2" -pre-commit = "==3.8.0" +ipython = "==8.28.0" +mypy = "==1.12.1" +pre-commit = "==4.0.1" requests-mock = "==1.12.1" tblib = "==3.0.0" watchdog = "==5.0.3" -werkzeug = "==3.0.4" -ruff = "==0.6.8" +werkzeug = "==3.0.6" +ruff = "==0.7.0" [docs] furo = "==2024.8.6" @@ -68,4 +66,4 @@ sphinx = "==8.0.2" myst-parser = "==4.0.0" [requires] -python_version = "3.12" +python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index ee060a0463..94a791a397 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "1c90ca755b86427eedcf4f48ff4aa792fd98b9ad9e0462f8463ba8e7283f9949" + "sha256": "e1007c202923fb8d82dc686ed4d9f3ff7e05afa8cbd6e81ea857baf3c0397a75" }, "pipfile-spec": 6, "requires": { - "python_version": "3.12" + "python_version": "3.13" }, "sources": [ { @@ -26,100 +26,100 @@ }, "aiohttp": { "hashes": [ - "sha256:10c7932337285a6bfa3a5fe1fd4da90b66ebfd9d0cbd1544402e1202eb9a8c3e", - "sha256:177126e971782769b34933e94fddd1089cef0fe6b82fee8a885e539f5b0f0c6a", - "sha256:1ce46dfb49cfbf9e92818be4b761d4042230b1f0e05ffec0aad15b3eb162b905", - "sha256:1e7a6af57091056a79a35104d6ec29d98ec7f1fb7270ad9c6fff871b678d1ff8", - "sha256:21a72f4a9c69a8567a0aca12042f12bba25d3139fd5dd8eeb9931f4d9e8599cd", - "sha256:21c1925541ca84f7b5e0df361c0a813a7d6a56d3b0030ebd4b220b8d232015f9", - "sha256:21f8225f7dc187018e8433c9326be01477fb2810721e048b33ac49091b19fb4a", - "sha256:22cdeb684d8552490dd2697a5138c4ecb46f844892df437aaf94f7eea99af879", - "sha256:270e653b5a4b557476a1ed40e6b6ce82f331aab669620d7c95c658ef976c9c5e", - "sha256:2df786c96c57cd6b87156ba4c5f166af7b88f3fc05f9d592252fdc83d8615a3c", - "sha256:32710d6b3b6c09c60c794d84ca887a3a2890131c0b02b3cefdcc6709a2260a7c", - "sha256:33a68011a38020ed4ff41ae0dbf4a96a202562ecf2024bdd8f65385f1d07f6ef", - "sha256:365783e1b7c40b59ed4ce2b5a7491bae48f41cd2c30d52647a5b1ee8604c68ad", - "sha256:3a95d2686bc4794d66bd8de654e41b5339fab542b2bca9238aa63ed5f4f2ce82", - "sha256:3b2036479b6b94afaaca7d07b8a68dc0e67b0caf5f6293bb6a5a1825f5923000", - "sha256:3c7f270f4ca92760f98a42c45a58674fff488e23b144ec80b1cc6fa2effed377", - "sha256:3f6d47e392c27206701565c8df4cac6ebed28fdf6dcaea5b1eea7a4631d8e6db", - "sha256:40d2d719c3c36a7a65ed26400e2b45b2d9ed7edf498f4df38b2ae130f25a0d01", - "sha256:4618f0d2bf523043866a9ff8458900d8eb0a6d4018f251dae98e5f1fb699f3a8", - "sha256:471a8c47344b9cc309558b3fcc469bd2c12b49322b4b31eb386c4a2b2d44e44a", - "sha256:4954e6b06dd0be97e1a5751fc606be1f9edbdc553c5d9b57d72406a8fbd17f9d", - "sha256:497a7d20caea8855c5429db3cdb829385467217d7feb86952a6107e033e031b9", - "sha256:4b91f4f62ad39a8a42d511d66269b46cb2fb7dea9564c21ab6c56a642d28bff5", - "sha256:4dbf252ac19860e0ab56cd480d2805498f47c5a2d04f5995d8d8a6effd04b48c", - "sha256:4e10b04542d27e21538e670156e88766543692a0a883f243ba8fad9ddea82e53", - "sha256:5284997e3d88d0dfb874c43e51ae8f4a6f4ca5b90dcf22995035187253d430db", - "sha256:57359785f27394a8bcab0da6dcd46706d087dfebf59a8d0ad2e64a4bc2f6f94f", - "sha256:597128cb7bc5f068181b49a732961f46cb89f85686206289d6ccb5e27cb5fbe2", - "sha256:5aa1a073514cf59c81ad49a4ed9b5d72b2433638cd53160fd2f3a9cfa94718db", - "sha256:680dbcff5adc7f696ccf8bf671d38366a1f620b5616a1d333d0cb33956065395", - "sha256:6984dda9d79064361ab58d03f6c1e793ea845c6cfa89ffe1a7b9bb400dfd56bd", - "sha256:69de056022e7abf69cb9fec795515973cc3eeaff51e3ea8d72a77aa933a91c52", - "sha256:6c7efa6616a95e3bd73b8a69691012d2ef1f95f9ea0189e42f338fae080c2fc6", - "sha256:6d1ad868624f6cea77341ef2877ad4e71f7116834a6cd7ec36ec5c32f94ee6ae", - "sha256:713dff3f87ceec3bde4f3f484861464e722cf7533f9fa6b824ec82bb5a9010a7", - "sha256:71462f8eeca477cbc0c9700a9464e3f75f59068aed5e9d4a521a103692da72dc", - "sha256:7c38cfd355fd86c39b2d54651bd6ed7d63d4fe3b5553f364bae3306e2445f847", - "sha256:8296edd99d0dd9d0eb8b9e25b3b3506eef55c1854e9cc230f0b3f885f680410b", - "sha256:85431c9131a9a0f65260dc7a65c800ca5eae78c4c9931618f18c8e0933a0e0c1", - "sha256:85e4d7bd05d18e4b348441e7584c681eff646e3bf38f68b2626807f3add21aa2", - "sha256:8885ca09d3a9317219c0831276bfe26984b17b2c37b7bf70dd478d17092a4772", - "sha256:8960fabc20bfe4fafb941067cda8e23c8c17c98c121aa31c7bf0cdab11b07842", - "sha256:9443d9ebc5167ce1fbb552faf2d666fb22ef5716a8750be67efd140a7733738c", - "sha256:9721554bfa9e15f6e462da304374c2f1baede3cb06008c36c47fa37ea32f1dc4", - "sha256:98a4eb60e27033dee9593814ca320ee8c199489fbc6b2699d0f710584db7feb7", - "sha256:98fae99d5c2146f254b7806001498e6f9ffb0e330de55a35e72feb7cb2fa399b", - "sha256:9a281cba03bdaa341c70b7551b2256a88d45eead149f48b75a96d41128c240b3", - "sha256:a087c84b4992160ffef7afd98ef24177c8bd4ad61c53607145a8377457385100", - "sha256:a1ba7bc139592339ddeb62c06486d0fa0f4ca61216e14137a40d626c81faf10c", - "sha256:a3081246bab4d419697ee45e555cef5cd1def7ac193dff6f50be761d2e44f194", - "sha256:a72f89aea712c619b2ca32c6f4335c77125ede27530ad9705f4f349357833695", - "sha256:a78ba86d5a08207d1d1ad10b97aed6ea48b374b3f6831d02d0b06545ac0f181e", - "sha256:a961ee6f2cdd1a2be4735333ab284691180d40bad48f97bb598841bfcbfb94ec", - "sha256:ab1546fc8e00676febc81c548a876c7bde32f881b8334b77f84719ab2c7d28dc", - "sha256:ab2d6523575fc98896c80f49ac99e849c0b0e69cc80bf864eed6af2ae728a52b", - "sha256:aff048793d05e1ce05b62e49dccf81fe52719a13f4861530706619506224992b", - "sha256:b1a012677b8e0a39e181e218de47d6741c5922202e3b0b65e412e2ce47c39337", - "sha256:b667e2a03407d79a76c618dc30cedebd48f082d85880d0c9c4ec2faa3e10f43e", - "sha256:b91557ee0893da52794b25660d4f57bb519bcad8b7df301acd3898f7197c5d81", - "sha256:badb51d851358cd7535b647bb67af4854b64f3c85f0d089c737f75504d5910ec", - "sha256:c36074b26f3263879ba8e4dbd33db2b79874a3392f403a70b772701363148b9f", - "sha256:c4916070e12ae140110aa598031876c1bf8676a36a750716ea0aa5bd694aa2e7", - "sha256:c6769d71bfb1ed60321363a9bc05e94dcf05e38295ef41d46ac08919e5b00d19", - "sha256:c887019dbcb4af58a091a45ccf376fffe800b5531b45c1efccda4bedf87747ea", - "sha256:cd9716ef0224fe0d0336997eb242f40619f9f8c5c57e66b525a1ebf9f1d8cebe", - "sha256:ceacea31f8a55cdba02bc72c93eb2e1b77160e91f8abd605969c168502fd71eb", - "sha256:d088ca05381fd409793571d8e34eca06daf41c8c50a05aeed358d2d340c7af81", - "sha256:d3a79200a9d5e621c4623081ddb25380b713c8cf5233cd11c1aabad990bb9381", - "sha256:d82404a0e7b10e0d7f022cf44031b78af8a4f99bd01561ac68f7c24772fed021", - "sha256:d95ae4420669c871667aad92ba8cce6251d61d79c1a38504621094143f94a8b4", - "sha256:da57af0c54a302b7c655fa1ccd5b1817a53739afa39924ef1816e7b7c8a07ccb", - "sha256:ddb9b9764cfb4459acf01c02d2a59d3e5066b06a846a364fd1749aa168efa2be", - "sha256:de23085cf90911600ace512e909114385026b16324fa203cc74c81f21fd3276a", - "sha256:e1f0f7b27171b2956a27bd8f899751d0866ddabdd05cbddf3520f945130a908c", - "sha256:e32148b4a745e70a255a1d44b5664de1f2e24fcefb98a75b60c83b9e260ddb5b", - "sha256:e45fdfcb2d5bcad83373e4808825b7512953146d147488114575780640665027", - "sha256:e56bb7e31c4bc79956b866163170bc89fd619e0581ce813330d4ea46921a4881", - "sha256:e860985f30f3a015979e63e7ba1a391526cdac1b22b7b332579df7867848e255", - "sha256:ee3587506898d4a404b33bd19689286ccf226c3d44d7a73670c8498cd688e42c", - "sha256:ee97c4e54f457c366e1f76fbbf3e8effee9de57dae671084a161c00f481106ce", - "sha256:ef9b484604af05ca745b6108ca1aaa22ae1919037ae4f93aaf9a37ba42e0b835", - "sha256:f21e8f2abed9a44afc3d15bba22e0dfc71e5fa859bea916e42354c16102b036f", - "sha256:f23a6c1d09de5de89a33c9e9b229106cb70dcfdd55e81a3a3580eaadaa32bc92", - "sha256:f5d5d5401744dda50b943d8764508d0e60cc2d3305ac1e6420935861a9d544bc", - "sha256:f78e2a78432c537ae876a93013b7bc0027ba5b93ad7b3463624c4b6906489332", - "sha256:f8179855a4e4f3b931cb1764ec87673d3fbdcca2af496c8d30567d7b034a13db", - "sha256:fc0e7f91705445d79beafba9bb3057dd50830e40fe5417017a76a214af54e122", - "sha256:fe285a697c851734285369614443451462ce78aac2b77db23567507484b1dc6f", - "sha256:fe3d79d6af839ffa46fdc5d2cf34295390894471e9875050eafa584cb781508d", - "sha256:fecd55e7418fabd297fd836e65cbd6371aa4035a264998a091bbf13f94d9c44d", - "sha256:ffef3d763e4c8fc97e740da5b4d0f080b78630a3914f4e772a122bbfa608c1db" - ], - "markers": "python_version >= '3.8'", - "version": "==3.10.8" + "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", + "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c", + "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24", + "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480", + "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2", + "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5", + "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", + "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", + "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", + "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", + "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486", + "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", + "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", + "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", + "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68", + "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1", + "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d", + "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd", + "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", + "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8", + "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7", + "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", + "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7", + "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", + "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", + "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", + "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", + "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", + "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8", + "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", + "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", + "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", + "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", + "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce", + "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b", + "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8", + "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", + "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", + "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a", + "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", + "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", + "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab", + "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", + "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", + "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9", + "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572", + "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554", + "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d", + "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257", + "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", + "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b", + "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", + "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090", + "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6", + "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc", + "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", + "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", + "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", + "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", + "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", + "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", + "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026", + "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb", + "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", + "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", + "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", + "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f", + "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983", + "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", + "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", + "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa", + "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c", + "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2", + "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", + "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", + "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762", + "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a", + "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8", + "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", + "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", + "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc", + "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91", + "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23", + "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527", + "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", + "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", + "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7", + "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f", + "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a", + "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", + "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414" + ], + "markers": "python_version >= '3.8'", + "version": "==3.10.10" }, "aiosignal": { "hashes": [ @@ -208,20 +208,20 @@ }, "boto3": { "hashes": [ - "sha256:2244044cdfa8ac345d7400536dc15a4824835e7ec5c55bc267e118af66bb27db", - "sha256:7bbb1ee649e09e956952285782cfdebd7e81fc78384f48dfab3d66c6eaf3f63f" + "sha256:b660c649a27a6b47a34f6f858f5bd7c3b0a798a16dec8dda7cbebeee80fd1f60", + "sha256:ddecb27f5699ca9f97711c52b6c0652c2e63bf6c2bfbc13b819b4f523b4d30ff" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.29" + "version": "==1.35.49" }, "botocore": { "hashes": [ - "sha256:4cee814875bc78656aef4011d3d6b2231e96f53ea3661ee428201afb579d5c31", - "sha256:f7bfa910cf2cbcc8c2307c1cf7b93495d614c2d699883417893e0a337fe4eb63" + "sha256:07d0c1325fdbfa49a4a054413dbdeab0a6030449b2aa66099241af2dac48afd8", + "sha256:aed4d3643afd702920792b68fbe712a8c3847993820d1048cd238a6469354da1" ], "markers": "python_version >= '3.8'", - "version": "==1.35.31" + "version": "==1.35.49" }, "celery": { "hashes": [ @@ -315,99 +315,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "click": { "hashes": [ @@ -442,54 +457,54 @@ }, "cryptography": { "hashes": [ - "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", - "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", - "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", - "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", - "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", - "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", - "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", - "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", - "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84", - "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", - "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", - "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", - "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2", - "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", - "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", - "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365", - "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", - "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", - "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", - "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", - "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", - "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", - "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", - "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", - "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", - "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", - "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" + "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", + "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", + "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", + "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", + "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", + "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", + "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", + "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", + "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", + "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", + "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", + "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", + "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", + "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", + "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", + "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", + "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", + "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", + "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", + "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", + "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" ], "markers": "python_version >= '3.7'", - "version": "==43.0.1" + "version": "==43.0.3" }, "django": { "hashes": [ - "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", - "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" + "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", + "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.1" + "version": "==5.1.2" }, "django-cors-headers": { "hashes": [ - "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6", - "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2" + "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6", + "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.4.0" + "markers": "python_version >= '3.9'", + "version": "==4.5.0" }, "django-environ": { "hashes": [ @@ -544,11 +559,11 @@ }, "django-rest-passwordreset": { "hashes": [ - "sha256:701f26804b6317f8ddbb1f9b2159176b65d8281e980b90db32dbd60407fb518a", - "sha256:94ea2aa717d2a6c50898541a1177dca4ae8b74bb67460aae3fd6ae02c992ce52" + "sha256:52e0a5ce0729102a9f691153ce5b554dc63660d9375932f7bc59e7ec242f2575", + "sha256:b81bd309bfdc3f01355e70c8e9c09f188f84590ad9ed3fd0bdcb075a76193006" ], "index": "pypi", - "version": "==1.4.1" + "version": "==1.4.2" }, "django-simple-history": { "hashes": [ @@ -586,11 +601,11 @@ }, "dnspython": { "hashes": [ - "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", - "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc" + "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", + "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1" ], - "markers": "python_version >= '3.8'", - "version": "==2.6.1" + "markers": "python_version >= '3.9'", + "version": "==2.7.0" }, "drf-nested-routers": { "hashes": [ @@ -635,86 +650,101 @@ }, "frozenlist": { "hashes": [ - "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", - "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", - "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", - "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", - "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", - "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", - "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", - "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", - "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", - "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", - "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", - "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", - "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", - "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", - "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", - "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", - "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", - "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", - "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", - "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", - "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", - "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", - "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", - "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", - "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", - "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", - "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", - "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", - "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", - "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", - "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", - "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", - "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", - "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", - "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", - "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", - "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", - "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", - "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", - "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", - "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", - "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", - "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", - "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", - "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", - "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", - "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", - "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", - "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", - "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", - "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", - "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", - "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", - "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", - "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", - "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", - "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", - "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", - "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", - "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", - "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", - "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", - "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", - "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", - "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", - "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", - "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", - "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", - "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", - "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", - "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", - "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", - "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", - "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", - "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", - "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", - "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" - ], - "markers": "python_version >= '3.8'", - "version": "==1.4.1" + "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", + "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", + "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", + "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", + "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", + "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", + "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", + "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", + "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", + "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", + "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", + "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", + "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c", + "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", + "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", + "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", + "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", + "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", + "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10", + "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", + "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", + "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", + "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", + "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10", + "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", + "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", + "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", + "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", + "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", + "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923", + "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", + "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", + "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", + "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", + "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", + "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", + "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", + "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", + "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", + "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", + "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", + "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", + "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", + "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", + "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", + "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604", + "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", + "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", + "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", + "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", + "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", + "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", + "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", + "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", + "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3", + "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", + "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", + "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", + "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf", + "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", + "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", + "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171", + "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", + "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", + "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", + "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", + "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", + "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", + "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9", + "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", + "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723", + "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", + "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", + "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99", + "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e", + "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", + "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", + "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", + "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", + "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", + "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca", + "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", + "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", + "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", + "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", + "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307", + "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e", + "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", + "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", + "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", + "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", + "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" }, "gunicorn": { "hashes": [ @@ -874,20 +904,11 @@ }, "jsonschema-specifications": { "hashes": [ - "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", - "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" + "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", + "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" ], - "markers": "python_version >= '3.8'", - "version": "==2023.12.1" - }, - "jwcrypto": { - "hashes": [ - "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", - "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.5.6" + "markers": "python_version >= '3.9'", + "version": "==2024.10.1" }, "kombu": { "hashes": [ @@ -1005,39 +1026,39 @@ }, "newrelic": { "hashes": [ - "sha256:09cd9a92c0cc54ec10d0954518ee490168215c13d8b24dc2398d7f539a422442", - "sha256:0acfc2fbc41c25902ad90b30a623c7cd6663c720ff754b051d39e85d179adc7a", - "sha256:24ab3764904c093fe0b3ba2d5e951c887311180b0a039ccb3cfcc6e720abdd04", - "sha256:2f0691c056c2d0df0f268dc1d39ed9fd1bdac36250316d0e4fe4547adad9de20", - "sha256:315b6675a28611409536a9fcc8cc2a463e0c662fa5f5a385267d3e672da1ae88", - "sha256:35f88e14f6448d7fb45c9a2e9685e8a51b42c859b9075df7934c84a1244db738", - "sha256:379a4e815cd48106c4b9080be7d331088fefe8ae2568eb68975fa7f617619d52", - "sha256:38f4dc5800173636eedd607a6a4c1a7a228953ac86c92ac80a6191fb050ed2c0", - "sha256:55633a0655327cb769b2d0672861cea42cd535d0e5ef73bba0cbfd3a91603a0a", - "sha256:5fefb4de5fda9273e40d6242c88d7b3775fe096940e140123e0da21829b0720c", - "sha256:7c0317ebe7f2cd61fe7131e43c5dc7965ed4bfc5f64ee631fb304e9c86b8e1e0", - "sha256:80327c8c06f2e7fa7a58efc2dd7e00d480310f81055cc9970cd356d2de1ad655", - "sha256:99968ce04e5eae99a4ab7b58baf85556dbd665f947a8f816cc6e3bacad25785a", - "sha256:a9b3ebfbd63ade4d340feea77824f62f15a8d7d1ecba55c6437e9b32106a33d2", - "sha256:ad3e5ef7181fc1d35b59041f280e55a3195d6adc0b9c0e708e30b948c2ee0848", - "sha256:afcdcbc2be3c8f7926ddcd4559186f9fb4c37f0eee9c5a480b84870381e63f34", - "sha256:affdb3ee375d47bf1995ee4d17c8a817232ed2d88bb80438e6c5f13912b18b60", - "sha256:b1db3ec4f72a4691e9fd828f1665d0f823a077b479d0a6e690b8e9c31a34bc17", - "sha256:b77f6fbfb57b0b1fdcb5cc970a4fcac60b33c1500d4611061d8009515bf07783", - "sha256:ba231e7d45b6b12a5df233a2d33677fbf9fc77f45d5332a75e11bf190fac3152", - "sha256:c2c516a1a44c2c6ba9f2bc8e1a76f7b499c1fc62307f72733b1121a3de15a11b", - "sha256:d0f0fff322dc66658fe45d945268918dc89cc33d685246ea2062aac6c9cff821", - "sha256:d4845761aca14048118d7914ca765374a967b0665f84c2603609d259c93c82ba", - "sha256:d51db5922fc549b69e7b4fae70e9a91011a079e99bb380e8cc06933e6c4d26f7", - "sha256:dd139bb4c36658007ddb17b53b8e46c6a81e6e814806b79275b9cc0d3bd626a8", - "sha256:e44f0b8e938197d13e2ef23028882018e2ecbb1c51a974b34d5329482a4584f7", - "sha256:e7a0f80519aff6f52276075b4b6222eff6dbc6814073fc017034f4869c12c93b", - "sha256:f5920bf78a844e7c5bb267b4a44978e22f125618aafa32459211ab9208da3ab5", - "sha256:f6a082558163f5e36f6ef62081aaeb3528c3dbe2489bce7ecccdbea28363c02a" + "sha256:00df1aa613294cb592a52157f789e75166dbf439cfa9e6cf59f6cf4a265dada9", + "sha256:03ab987eae0452aeb5aed8571c100d1735613a3a227387f99fe54ed38f1ae0e9", + "sha256:23400846dad2283693eade90b6d3c3462301a4b7735c7f76009b1fa445660aeb", + "sha256:32bd34e4cd73c2435472c0b67869fd2db914d6c99d3e1e404e09affe61a8551e", + "sha256:3415b1c7cab5e586e72cca467dd80cd0507f23a3139c02911cf75892fdbb48a6", + "sha256:3521d646c0032db53b7320fe6b6859eebd863f1b47d7c7dd480073727091e50e", + "sha256:36a2218c9e79897d9b5671cdeac30c467d7fbac10cda4f2d79062f2bd0fcaed8", + "sha256:3bee0b9ce1eccf6ac63e51113781743853b1b84c98ae48ed17d0410c352ccb4d", + "sha256:3cd5aeade6462519328fc42f4e98948a45571f3d22360a0559e19a6525c723a6", + "sha256:3d9c8297ba158ce4570fc48cfea7bdf3678b2054baaf0cad4debcbca33c2af3a", + "sha256:3f15a940b6794b4008ab983e7ac3b4d179efe609e040ee96ed5744723fc580c8", + "sha256:501cc575b3fd702a21542a0f5dac59b83f47e2806f5b7c0f4e4510b5474ed77f", + "sha256:60d01303807228718c4099d8550f72d21ee8b61a33555d8974800f6868f2144a", + "sha256:62d521a5d7269c8a5c5838c4ca3b757ef63a13257302c901223c75510cc6f9f5", + "sha256:69aa68cae47c595bdeb95f275d78693ec27a9fd9353bf81257e21f8607134db6", + "sha256:7b449546ebb89feaadfd36fda7735ce06023fc90979b838e244f98369aba5ccb", + "sha256:7f021eac4c2e3b14eab90c608d8bd25b4e3c6b0b0d40796ec1c1260cc47b5e83", + "sha256:a15df23effd09bb1d1f5c38866b75cc5f380b6aa953efbca9e95e79b72744db4", + "sha256:a6d4094d19db924c51ca35da603344907bcbca030822f7a78d5d9c6ad361d419", + "sha256:a6ff022c7556b61b067e8e6b729fe60f437a8356f319ae3b8342858792f3930d", + "sha256:b4220b97669d214e75d2039fea9e0505fde5bc450832210abbc76b9a635785ed", + "sha256:bc693e0db87ab4cf6623847c3949debbcc991554edfb4dd8c02c136e0770b367", + "sha256:c57e79d37ed87e2790c5e66253f9a5d91ed8cc218f160d5a4d062fc759791a78", + "sha256:c6aa9cf936b16d13b65c1b7aa6c722a76a0702469f91bcfc3c5b39f3293181eb", + "sha256:e5809b4111ef3b1d0b5fa66ad06a81de512842370707863d44888c9439f16c4c", + "sha256:e6f822e6a43151af13a748fb2de6ff298aeb6eee03bf6512afba6aaa79211172", + "sha256:ef5d27001d3b5ca53f19d150c60b570c1b0c774d082ab9bf8349f4430ed85b48", + "sha256:f6333aa7051544ddc7f8a85f344bc3f401ddd8635540878da34de7bfd91f5d95", + "sha256:fc3d34db12133b481636384663f45b9ccd7f0f41554a59c15ec37aeb4f77227d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==10.1.0" + "version": "==10.2.0" }, "packaging": { "hashes": [ @@ -1049,90 +1070,85 @@ }, "pillow": { "hashes": [ - "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", - "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", - "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", - "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", - "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", - "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", - "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", - "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", - "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", - "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", - "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", - "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", - "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", - "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", - "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", - "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", - "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", - "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", - "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", - "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", - "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", - "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", - "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", - "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", - "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", - "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", - "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", - "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", - "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", - "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", - "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", - "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", - "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", - "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", - "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", - "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", - "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", - "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", - "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", - "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", - "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", - "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", - "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", - "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", - "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", - "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", - "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", - "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", - "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", - "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", - "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", - "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", - "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", - "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", - "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", - "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", - "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", - "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", - "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", - "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", - "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", - "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", - "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", - "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", - "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", - "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", - "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", - "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", - "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", - "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", - "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", - "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", - "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", - "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", - "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", - "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", - "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", - "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", - "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", - "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" + "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", + "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", + "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", + "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", + "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", + "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", + "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", + "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", + "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", + "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", + "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", + "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", + "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", + "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", + "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", + "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", + "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", + "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", + "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", + "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", + "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", + "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", + "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", + "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", + "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", + "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", + "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", + "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", + "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", + "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", + "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", + "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", + "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", + "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", + "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", + "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", + "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", + "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", + "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", + "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", + "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", + "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", + "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", + "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", + "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", + "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", + "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", + "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", + "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", + "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", + "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", + "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", + "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", + "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", + "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", + "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", + "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", + "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", + "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", + "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", + "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", + "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", + "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", + "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", + "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", + "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", + "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", + "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", + "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", + "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", + "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", + "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", + "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", + "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", + "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==10.4.0" + "markers": "python_version >= '3.9'", + "version": "==11.0.0" }, "ply": { "hashes": [ @@ -1149,22 +1165,126 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.0.48" }, + "propcache": { + "hashes": [ + "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", + "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", + "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", + "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", + "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", + "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", + "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", + "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", + "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", + "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", + "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", + "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", + "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", + "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", + "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", + "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", + "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", + "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", + "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", + "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", + "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", + "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", + "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", + "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", + "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", + "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", + "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", + "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", + "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", + "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", + "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", + "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", + "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", + "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", + "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", + "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", + "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", + "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", + "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", + "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", + "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", + "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", + "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", + "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", + "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", + "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", + "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", + "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", + "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", + "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", + "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", + "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", + "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", + "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", + "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", + "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", + "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", + "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", + "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", + "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", + "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", + "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", + "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", + "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", + "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", + "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", + "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", + "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", + "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", + "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", + "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", + "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", + "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", + "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", + "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", + "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", + "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", + "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", + "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", + "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", + "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", + "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", + "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", + "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", + "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", + "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", + "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", + "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", + "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", + "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", + "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", + "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", + "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", + "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", + "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", + "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", + "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", + "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504" + ], + "markers": "python_version >= '3.8'", + "version": "==0.2.0" + }, "psycopg": { "extras": [ "c" ], "hashes": [ - "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0", - "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2" + "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", + "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2" ], "markers": "python_version >= '3.8'", - "version": "==3.2.2" + "version": "==3.2.3" }, "psycopg-c": { "hashes": [ - "sha256:de8cac75bc6640ef0f54ad9187b81e07c430206a83c566b73d4cca41ecccb7c8" + "sha256:06ae7db8eaec1a3845960fa7f997f4ccdb1a7a7ab8dc593a680bcc74e1359671" ], - "version": "==3.2.2" + "version": "==3.2.3" }, "py-vapid": { "hashes": [ @@ -1180,45 +1300,6 @@ "markers": "python_version >= '3.8'", "version": "==2.22" }, - "pycryptodome": { - "hashes": [ - "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690", - "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7", - "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4", - "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd", - "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5", - "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc", - "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818", - "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab", - "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d", - "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a", - "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25", - "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091", - "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea", - "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a", - "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c", - "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72", - "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9", - "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6", - "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044", - "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04", - "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c", - "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e", - "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f", - "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b", - "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4", - "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33", - "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f", - "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e", - "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a", - "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2", - "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3", - "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128" - ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.20.0" - }, "pydantic": { "extras": [ "email" @@ -1286,7 +1367,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-fsutil": { @@ -1315,10 +1396,10 @@ }, "pywebpush": { "hashes": [ - "sha256:03ccc3e975b60374b7634c495595616be523bf2c7da0d976e84fda9ac8c63301" + "sha256:72e3586aec89a06373c0585e6f97c6fd0b81519e41f32896a3639909ecf45db0" ], "index": "pypi", - "version": "==2.0.0" + "version": "==2.0.1" }, "pyyaml": { "hashes": [ @@ -1527,20 +1608,20 @@ }, "s3transfer": { "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "sentry-sdk": { "hashes": [ - "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d", - "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4" + "sha256:625955884b862cc58748920f9e21efdfb8e0d4f98cca4ab0d3918576d5b606ad", + "sha256:dd0a05352b78ffeacced73a94e86f38b32e2eae15fff5f30ca5abb568a72eacf" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.14.0" + "version": "==2.17.0" }, "setuptools": { "hashes": [ @@ -1555,7 +1636,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sqlparse": { @@ -1591,26 +1672,26 @@ }, "types-redis": { "hashes": [ - "sha256:0e7537e5c085fe96b7d468d5edae0cf667b4ba4b62c6e4a5dfc340bd3b868c23", - "sha256:4bab1a378dbf23c2c95c370dfdb89a8f033957c4fd1a53fee71b529c182fe008" + "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", + "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed" ], "markers": "python_version >= '3.8'", - "version": "==4.6.0.20240903" + "version": "==4.6.0.20241004" }, "types-setuptools": { "hashes": [ - "sha256:06f78307e68d1bbde6938072c57b81cf8a99bc84bd6dc7e4c5014730b097dc0c", - "sha256:12f12a165e7ed383f31def705e5c0fa1c26215dd466b0af34bd042f7d5331f55" + "sha256:2949913a518d5285ce00a3b7d88961c80a6e72ffb8f3da0a3f5650ea533bd45e", + "sha256:6721ac0f1a620321e2ccd87a9a747c4a383dc381f78d894ce37f2455b45fcf1c" ], "markers": "python_version >= '3.8'", - "version": "==75.1.0.20240917" + "version": "==75.2.0.20241025" }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version < '3.13'", + "markers": "python_version >= '3.8'", "version": "==4.12.2" }, "tzdata": { @@ -1669,101 +1750,91 @@ }, "yarl": { "hashes": [ - "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b", - "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424", - "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644", - "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465", - "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88", - "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8", - "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da", - "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154", - "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51", - "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f", - "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc", - "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d", - "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8", - "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4", - "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c", - "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc", - "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2", - "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606", - "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6", - "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c", - "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734", - "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae", - "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220", - "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e", - "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d", - "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c", - "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177", - "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da", - "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23", - "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485", - "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85", - "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05", - "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09", - "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99", - "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9", - "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e", - "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5", - "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71", - "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0", - "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8", - "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10", - "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246", - "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049", - "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d", - "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2", - "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad", - "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323", - "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7", - "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2", - "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3", - "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a", - "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851", - "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206", - "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b", - "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550", - "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f", - "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1", - "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b", - "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe", - "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74", - "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5", - "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495", - "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c", - "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813", - "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a", - "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2", - "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d", - "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57", - "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc", - "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320", - "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43", - "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e", - "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac", - "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26", - "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c", - "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2", - "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799", - "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d", - "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735", - "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419", - "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e", - "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4", - "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0", - "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c", - "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3", - "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8", - "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9", - "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224", - "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38", - "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf", - "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7", - "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093" - ], - "markers": "python_version >= '3.8'", - "version": "==1.13.1" + "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9", + "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36", + "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240", + "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2", + "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581", + "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929", + "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3", + "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6", + "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552", + "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472", + "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2", + "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb", + "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7", + "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b", + "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b", + "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058", + "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a", + "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656", + "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71", + "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3", + "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837", + "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6", + "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0", + "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104", + "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca", + "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb", + "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7", + "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07", + "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b", + "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202", + "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d", + "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532", + "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f", + "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5", + "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3", + "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724", + "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2", + "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09", + "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732", + "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2", + "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120", + "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4", + "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027", + "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e", + "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d", + "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b", + "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16", + "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120", + "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5", + "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97", + "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84", + "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00", + "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596", + "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d", + "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56", + "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7", + "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283", + "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67", + "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c", + "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968", + "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916", + "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae", + "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8", + "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604", + "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4", + "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af", + "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f", + "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a", + "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428", + "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9", + "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b", + "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059", + "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3", + "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49", + "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3", + "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade", + "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3", + "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c", + "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7", + "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349", + "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243", + "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7" + ], + "markers": "python_version >= '3.9'", + "version": "==1.16.0" } }, "develop": { @@ -1792,12 +1863,12 @@ }, "boto3": { "hashes": [ - "sha256:2244044cdfa8ac345d7400536dc15a4824835e7ec5c55bc267e118af66bb27db", - "sha256:7bbb1ee649e09e956952285782cfdebd7e81fc78384f48dfab3d66c6eaf3f63f" + "sha256:b660c649a27a6b47a34f6f858f5bd7c3b0a798a16dec8dda7cbebeee80fd1f60", + "sha256:ddecb27f5699ca9f97711c52b6c0652c2e63bf6c2bfbc13b819b4f523b4d30ff" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.29" + "version": "==1.35.49" }, "boto3-stubs": { "extras": [ @@ -1805,27 +1876,27 @@ "s3" ], "hashes": [ - "sha256:048e664389c3fb53e8ab0f810eb280ba02c2f8213a63dc5d3da075ffd54b4504", - "sha256:6e5f082f7cd028bdf3bfc57c9db3b784e0f6ec2232b10482859a919d6cd6bfc9" + "sha256:2a2e08ba2383df6f478127f9754a02a590131249b40c59d7c6ca9fce76906785", + "sha256:daad87dcff906f7c09dde4ef3c252e2c47b6e1e8e669f5a8311658ac0d1182c0" ], "markers": "python_version >= '3.8'", - "version": "==1.35.29" + "version": "==1.35.49" }, "botocore": { "hashes": [ - "sha256:4cee814875bc78656aef4011d3d6b2231e96f53ea3661ee428201afb579d5c31", - "sha256:f7bfa910cf2cbcc8c2307c1cf7b93495d614c2d699883417893e0a337fe4eb63" + "sha256:07d0c1325fdbfa49a4a054413dbdeab0a6030449b2aa66099241af2dac48afd8", + "sha256:aed4d3643afd702920792b68fbe712a8c3847993820d1048cd238a6469354da1" ], "markers": "python_version >= '3.8'", - "version": "==1.35.31" + "version": "==1.35.49" }, "botocore-stubs": { "hashes": [ - "sha256:b26f79253d8f2460aa8f2d49ae933c3f352fe38be72ea033651c15cd47a822a9", - "sha256:b7289c27b759dad40759c1421519cbd7ec65b79f7aa8be5dfc76004a61aef42b" + "sha256:367ce067e003de7e9b76320f551ba4fc8369a4b7ef10210f6071d3593fea2605", + "sha256:c5006e31d77e290eca215e6a71292ea7b029b54900310ed0f87da8e844f1db38" ], "markers": "python_version >= '3.8'", - "version": "==1.35.31" + "version": "==1.35.49" }, "certifi": { "hashes": [ @@ -1845,207 +1916,216 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "coverage": { "hashes": [ - "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", - "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", - "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", - "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", - "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", - "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", - "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", - "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", - "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", - "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", - "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", - "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", - "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", - "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", - "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", - "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", - "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", - "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", - "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", - "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", - "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", - "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", - "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", - "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", - "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", - "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", - "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", - "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", - "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", - "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", - "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", - "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", - "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", - "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", - "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", - "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", - "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", - "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", - "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", - "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", - "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", - "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", - "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", - "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", - "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", - "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", - "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", - "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", - "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", - "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", - "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", - "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", - "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", - "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", - "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", - "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", - "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", - "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", - "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", - "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", - "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", - "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", - "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", - "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", - "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", - "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", - "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", - "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", - "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", - "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", - "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", - "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc" + "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376", + "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", + "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111", + "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", + "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", + "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", + "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", + "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", + "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", + "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c", + "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", + "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", + "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", + "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0", + "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", + "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", + "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", + "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", + "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", + "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", + "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", + "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", + "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", + "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", + "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", + "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", + "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", + "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", + "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", + "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901", + "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", + "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", + "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", + "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", + "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", + "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", + "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", + "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", + "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3", + "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", + "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076", + "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", + "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", + "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", + "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", + "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", + "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", + "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09", + "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", + "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", + "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f", + "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", + "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", + "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", + "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", + "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", + "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", + "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", + "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", + "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", + "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", + "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==7.6.1" + "markers": "python_version >= '3.9'", + "version": "==7.6.4" }, "debugpy": { "hashes": [ - "sha256:0a85707c6a84b0c5b3db92a2df685b5230dd8fb8c108298ba4f11dba157a615a", - "sha256:22140bc02c66cda6053b6eb56dfe01bbe22a4447846581ba1dd6df2c9f97982d", - "sha256:30f467c5345d9dfdcc0afdb10e018e47f092e383447500f125b4e013236bf14b", - "sha256:3358aa619a073b620cd0d51d8a6176590af24abcc3fe2e479929a154bf591b51", - "sha256:43996632bee7435583952155c06881074b9a742a86cee74e701d87ca532fe833", - "sha256:538c6cdcdcdad310bbefd96d7850be1cd46e703079cc9e67d42a9ca776cdc8a8", - "sha256:567419081ff67da766c898ccf21e79f1adad0e321381b0dfc7a9c8f7a9347972", - "sha256:5d73d8c52614432f4215d0fe79a7e595d0dd162b5c15233762565be2f014803b", - "sha256:67479a94cf5fd2c2d88f9615e087fcb4fec169ec780464a3f2ba4a9a2bb79955", - "sha256:9fb8653f6cbf1dd0a305ac1aa66ec246002145074ea57933978346ea5afdf70b", - "sha256:b48892df4d810eff21d3ef37274f4c60d32cdcafc462ad5647239036b0f0649f", - "sha256:c1cef65cffbc96e7b392d9178dbfd524ab0750da6c0023c027ddcac968fd1caa", - "sha256:c931a9371a86784cee25dec8d65bc2dc7a21f3f1552e3833d9ef8f919d22280a", - "sha256:c9834dfd701a1f6bf0f7f0b8b1573970ae99ebbeee68314116e0ccc5c78eea3c", - "sha256:cdaf0b9691879da2d13fa39b61c01887c34558d1ff6e5c30e2eb698f5384cd43", - "sha256:db891b141fc6ee4b5fc6d1cc8035ec329cabc64bdd2ae672b4550c87d4ecb128", - "sha256:df5dc9eb4ca050273b8e374a4cd967c43be1327eeb42bfe2f58b3cdfe7c68dcb", - "sha256:e3a82da039cfe717b6fb1886cbbe5c4a3f15d7df4765af857f4307585121c2dd", - "sha256:e3e182cd98eac20ee23a00653503315085b29ab44ed66269482349d307b08df9", - "sha256:e4ce0570aa4aca87137890d23b86faeadf184924ad892d20c54237bcaab75d8f", - "sha256:f1e60bd06bb3cc5c0e957df748d1fab501e01416c43a7bdc756d2a992ea1b881", - "sha256:f7158252803d0752ed5398d291dee4c553bb12d14547c0e1843ab74ee9c31123" + "sha256:11ad72eb9ddb436afb8337891a986302e14944f0f755fd94e90d0d71e9100bba", + "sha256:171899588bcd412151e593bd40d9907133a7622cd6ecdbdb75f89d1551df13c2", + "sha256:18b8f731ed3e2e1df8e9cdaa23fb1fc9c24e570cd0081625308ec51c82efe42e", + "sha256:29e1571c276d643757ea126d014abda081eb5ea4c851628b33de0c2b6245b037", + "sha256:2efb84d6789352d7950b03d7f866e6d180284bc02c7e12cb37b489b7083d81aa", + "sha256:2f729228430ef191c1e4df72a75ac94e9bf77413ce5f3f900018712c9da0aaca", + "sha256:45c30aaefb3e1975e8a0258f5bbd26cd40cde9bfe71e9e5a7ac82e79bad64e39", + "sha256:4b908291a1d051ef3331484de8e959ef3e66f12b5e610c203b5b75d2725613a7", + "sha256:4d27d842311353ede0ad572600c62e4bcd74f458ee01ab0dd3a1a4457e7e3706", + "sha256:57b00de1c8d2c84a61b90880f7e5b6deaf4c312ecbde3a0e8912f2a56c4ac9ae", + "sha256:628a11f4b295ffb4141d8242a9bb52b77ad4a63a2ad19217a93be0f77f2c28c9", + "sha256:6a9d9d6d31846d8e34f52987ee0f1a904c7baa4912bf4843ab39dadf9b8f3e0d", + "sha256:6e1c4ffb0c79f66e89dfd97944f335880f0d50ad29525dc792785384923e2211", + "sha256:703c1fd62ae0356e194f3e7b7a92acd931f71fe81c4b3be2c17a7b8a4b546ec2", + "sha256:85ce9c1d0eebf622f86cc68618ad64bf66c4fc3197d88f74bb695a416837dd55", + "sha256:90d93e4f2db442f8222dec5ec55ccfc8005821028982f1968ebf551d32b28907", + "sha256:93176e7672551cb5281577cdb62c63aadc87ec036f0c6a486f0ded337c504596", + "sha256:95fe04a573b8b22896c404365e03f4eda0ce0ba135b7667a1e57bd079793b96b", + "sha256:a6cf2510740e0c0b4a40330640e4b454f928c7b99b0c9dbf48b11efba08a8cda", + "sha256:b12515e04720e9e5c2216cc7086d0edadf25d7ab7e3564ec8b4521cf111b4f8c", + "sha256:b6db2a370e2700557a976eaadb16243ec9c91bd46f1b3bb15376d7aaa7632c81", + "sha256:caf528ff9e7308b74a1749c183d6808ffbedbb9fb6af78b033c28974d9b8831f", + "sha256:cba1d078cf2e1e0b8402e6bda528bf8fda7ccd158c3dba6c012b7897747c41a0", + "sha256:d050a1ec7e925f514f0f6594a1e522580317da31fbda1af71d1530d6ea1f2b40", + "sha256:da8df5b89a41f1fd31503b179d0a84a5fdb752dddd5b5388dbd1ae23cda31ce9", + "sha256:f2f4349a28e3228a42958f8ddaa6333d6f8282d5edaea456070e48609c5983b7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.8.6" + "version": "==1.8.7" }, "decorator": { "hashes": [ @@ -2057,19 +2137,19 @@ }, "distlib": { "hashes": [ - "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", - "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" + "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", + "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" ], - "version": "==0.3.8" + "version": "==0.3.9" }, "django": { "hashes": [ - "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", - "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" + "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", + "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.1" + "version": "==5.1.2" }, "django-coverage-plugin": { "hashes": [ @@ -2099,19 +2179,19 @@ }, "django-stubs": { "hashes": [ - "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5", - "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40" + "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b", + "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "django-stubs-ext": { "hashes": [ - "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d", - "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926" + "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c", + "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "djangorestframework-stubs": { "hashes": [ @@ -2141,11 +2221,11 @@ }, "faker": { "hashes": [ - "sha256:dbf81295c948270a9e96cd48a9a3ebec73acac9a153d0c854fbbd0294557609f", - "sha256:e0593931bd7be9a9ea984b5d8c302ef1cec19392585d1e90d444199271d0a94d" + "sha256:4f7f133560b9d4d2a915581f4ba86f9a6a83421b89e911f36c4c96cff58135a5", + "sha256:93e8b70813f76d05d98951154681180cb795cfbcff3eced7680d963bcc0da2a9" ], "markers": "python_version >= '3.8'", - "version": "==30.1.0" + "version": "==30.8.1" }, "filelock": { "hashes": [ @@ -2190,12 +2270,12 @@ }, "ipython": { "hashes": [ - "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e", - "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c" + "sha256:0d0d15ca1e01faeb868ef56bc7ee5a0de5bd66885735682e8a322ae289a13d1a", + "sha256:530ef1e7bb693724d3cdc37287c80b07ad9b25986c007a53aa1857272dac3f35" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==8.27.0" + "version": "==8.28.0" }, "jedi": { "hashes": [ @@ -2215,69 +2295,70 @@ }, "markupsafe": { "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" }, "matplotlib-inline": { "hashes": [ @@ -2289,44 +2370,49 @@ }, "mypy": { "hashes": [ - "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", - "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", - "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", - "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", - "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", - "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", - "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", - "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", - "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", - "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", - "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", - "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", - "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", - "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", - "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", - "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", - "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", - "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", - "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", - "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", - "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", - "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", - "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", - "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", - "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", - "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", - "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d" + "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", + "sha256:0dcc1e843d58f444fce19da4cce5bd35c282d4bde232acdeca8279523087088a", + "sha256:0e6fe449223fa59fbee351db32283838a8fee8059e0028e9e6494a03802b4004", + "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", + "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", + "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", + "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", + "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", + "sha256:3d7d4371829184e22fda4015278fbfdef0327a4b955a483012bd2d423a788801", + "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", + "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", + "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", + "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", + "sha256:8135ffec02121a75f75dc97c81af7c14aa4ae0dda277132cfcd6abcd21551bfd", + "sha256:843826966f1d65925e8b50d2b483065c51fc16dc5d72647e0236aae51dc8d77e", + "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", + "sha256:96af62050971c5241afb4701c15189ea9507db89ad07794a4ee7b4e092dc0627", + "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", + "sha256:9fe20f89da41a95e14c34b1ddb09c80262edcc295ad891f22cc4b60013e8f78d", + "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", + "sha256:a7b76fa83260824300cc4834a3ab93180db19876bce59af921467fd03e692810", + "sha256:b16fe09f9c741d85a2e3b14a5257a27a4f4886c171d562bc5a5e90d8591906b8", + "sha256:b947097fae68004b8328c55161ac9db7d3566abfef72d9d41b47a021c2fba6b1", + "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", + "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", + "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", + "sha256:d90da248f4c2dba6c44ddcfea94bb361e491962f05f41990ff24dbd09969ce20", + "sha256:dc6e2a2195a290a7fd5bac3e60b586d77fc88e986eba7feced8b778c373f9afe", + "sha256:de5b2a8988b4e1269a98beaf0e7cc71b510d050dce80c343b53b4955fff45f19", + "sha256:e10ba7de5c616e44ad21005fa13450cd0de7caaa303a626147d45307492e4f2d", + "sha256:f59f1dfbf497d473201356966e353ef09d4daec48caeacc0254db8ef633a28a5", + "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.11.2" + "version": "==1.12.1" }, "mypy-boto3-s3": { "hashes": [ - "sha256:5b5aec56e16ef48766ae81b1807263127eebc4e6bfbeeef3f2f2cb7c30c06d03", - "sha256:9f64e1196ffecc2c6ab7bee95e848692b5f464a1df14211361ea6bbbc2038387" + "sha256:34d19dfba400f5b9bd6b64f09eb8f8eedef60545b410a3753fe99fec0c41ba78", + "sha256:f0087a3765d103b2db565cd8065ebc2b0f70f2dd4e92c132f64b8945dd869940" ], - "version": "==1.35.22" + "version": "==1.35.46" }, "mypy-extensions": { "hashes": [ @@ -2370,12 +2456,12 @@ }, "pre-commit": { "hashes": [ - "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", - "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f" + "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", + "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.8.0" + "version": "==4.0.1" }, "prompt-toolkit": { "hashes": [ @@ -2420,7 +2506,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pyyaml": { @@ -2502,43 +2588,43 @@ }, "ruff": { "hashes": [ - "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750", - "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa", - "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c", - "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0", - "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f", - "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098", - "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0", - "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f", - "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44", - "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2", - "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a", - "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", - "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb", - "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18", - "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5", - "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce", - "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263", - "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87" + "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628", + "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e", + "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495", + "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9", + "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa", + "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06", + "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b", + "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737", + "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11", + "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be", + "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598", + "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e", + "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4", + "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914", + "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9", + "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d", + "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec", + "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.6.8" + "version": "==0.7.0" }, "s3transfer": { "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sqlparse": { @@ -2575,11 +2661,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:67a660c90bad360c339f6a79310cc17094d12472042c7ca5a41450aaf5fc9a54", - "sha256:b2c196bbd3226bab42d80fae13c34548de9ddc195f5a366d79c15d18e5897aa9" + "sha256:3fd1edeac923d1956c0e907c973fb83bda465beae7f054716b371b293f9b5fdc", + "sha256:517d9d06f19cf58d778ca90ad01e52e0489466bf70dcf78c7f47f74fdf151a60" ], "markers": "python_version >= '3.8'", - "version": "==0.22.0" + "version": "==0.23.0" }, "types-pyyaml": { "hashes": [ @@ -2591,26 +2677,26 @@ }, "types-requests": { "hashes": [ - "sha256:2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405", - "sha256:59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310" + "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", + "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747" ], "markers": "python_version >= '3.8'", - "version": "==2.32.0.20240914" + "version": "==2.32.0.20241016" }, "types-s3transfer": { "hashes": [ - "sha256:60167a3bfb5c536ec6cdb5818f7f9a28edca9dc3e0b5ff85ae374526fc5e576e", - "sha256:7a3fec8cd632e2b5efb665a355ef93c2a87fdd5a45b74a949f95a9e628a86356" + "sha256:d34c5a82f531af95bb550927136ff5b737a1ed3087f90a59d545591dfde5b4cc", + "sha256:f761b2876ac4c208e6c6b75cdf5f6939009768be9950c545b11b0225e7703ee7" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version < '3.13'", + "markers": "python_version >= '3.8'", "version": "==4.12.2" }, "urllib3": { @@ -2623,11 +2709,11 @@ }, "virtualenv": { "hashes": [ - "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", - "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2" + "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2", + "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655" ], - "markers": "python_version >= '3.7'", - "version": "==20.26.6" + "markers": "python_version >= '3.8'", + "version": "==20.27.0" }, "watchdog": { "hashes": [ @@ -2675,12 +2761,12 @@ }, "werkzeug": { "hashes": [ - "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", - "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306" + "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", + "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.0.4" + "version": "==3.0.6" } }, "docs": { @@ -2718,99 +2804,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "docutils": { "hashes": [ @@ -2863,69 +2964,70 @@ }, "markupsafe": { "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" }, "mdit-py-plugins": { "hashes": [ diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index 0405816f59..473737e801 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim-bookworm +FROM python:3.13-slim-bookworm ARG TYPST_VERSION=0.11.0 diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 067b0498d6..8e27664c36 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim-bookworm AS base +FROM python:3.13-slim-bookworm AS base ARG APP_HOME=/app ARG TYPST_VERSION=0.11.0 From fa80b2cee3de658f55e715dc42e354d243e6ebdc Mon Sep 17 00:00:00 2001 From: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:30:31 +0530 Subject: [PATCH 22/27] Add Plug Config API (#2574) Co-authored-by: Aakash Singh --- .github/workflows/linter.yml | 6 +++- care/users/api/serializers/plug_config.py | 9 +++++ care/users/api/viewsets/plug_config.py | 44 +++++++++++++++++++++++ care/users/migrations/0020_plugconfig.py | 21 +++++++++++ care/users/models.py | 8 +++++ config/api_router.py | 4 +++ docker/dev.Dockerfile | 2 +- docker/prod.Dockerfile | 2 +- 8 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 care/users/api/serializers/plug_config.py create mode 100644 care/users/api/viewsets/plug_config.py create mode 100644 care/users/migrations/0020_plugconfig.py diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index cbf322bd10..37d7c67fad 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -14,7 +14,11 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v3 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: pre-commit/action@v3.0.1 with: extra_args: --color=always --from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }} diff --git a/care/users/api/serializers/plug_config.py b/care/users/api/serializers/plug_config.py new file mode 100644 index 0000000000..6ac59adb21 --- /dev/null +++ b/care/users/api/serializers/plug_config.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from care.users.models import PlugConfig + + +class PLugConfigSerializer(serializers.ModelSerializer): + class Meta: + model = PlugConfig + exclude = ("id",) diff --git a/care/users/api/viewsets/plug_config.py b/care/users/api/viewsets/plug_config.py new file mode 100644 index 0000000000..40cae848a8 --- /dev/null +++ b/care/users/api/viewsets/plug_config.py @@ -0,0 +1,44 @@ +from django.core.cache import cache +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from care.users.api.serializers.plug_config import PLugConfigSerializer +from care.users.models import PlugConfig + + +class PlugConfigViewset( + ModelViewSet, + GenericViewSet, +): + lookup_field = "slug" + serializer_class = PLugConfigSerializer + queryset = PlugConfig.objects.all().order_by("slug") + cache_key = "care_plug_viewset_list" + authentication_classes = [] + + def list(self, request, *args, **kwargs): + # Cache data and return + response = cache.get(self.cache_key) + if not response: + serializer = self.get_serializer(self.queryset, many=True) + response = serializer.data + cache.set(self.cache_key, response) + return Response({"configs": [response]}) + + def perform_create(self, serializer): + cache.delete(self.cache_key) + serializer.save() + + def perform_update(self, serializer): + cache.delete(self.cache_key) + serializer.save() + + def perform_destroy(self, instance): + cache.delete(self.cache_key) + instance.delete() + + def get_permissions(self): + if self.action in ["list", "retrieve"]: + return [] + return [IsAdminUser()] diff --git a/care/users/migrations/0020_plugconfig.py b/care/users/migrations/0020_plugconfig.py new file mode 100644 index 0000000000..8f0b3977a1 --- /dev/null +++ b/care/users/migrations/0020_plugconfig.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.1 on 2024-10-29 19:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0019_rename_doctor_qualification_user_qualification'), + ] + + operations = [ + migrations.CreateModel( + name='PlugConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.CharField(max_length=255, unique=True)), + ('meta', models.JSONField(default=dict)), + ], + ), + ] diff --git a/care/users/models.py b/care/users/models.py index 5f214871e0..bf0d47c284 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -443,6 +443,14 @@ def __str__(self): return self.facility.name +class PlugConfig(models.Model): + slug = models.CharField(max_length=255, unique=True) + meta = models.JSONField(default=dict) + + def __str__(self): + return self.slug + + class UserFlag(BaseFlag): user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) diff --git a/config/api_router.py b/config/api_router.py index 7e00754a00..94b18f61de 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -99,6 +99,7 @@ StateViewSet, WardViewSet, ) +from care.users.api.viewsets.plug_config import PlugConfigViewset from care.users.api.viewsets.skill import SkillViewSet from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet @@ -106,6 +107,9 @@ router = DefaultRouter() if settings.DEBUG else SimpleRouter() router.register("users", UserViewSet, basename="users") + +router.register("plug_config", PlugConfigViewset, basename="plug_configs") + user_nested_router = NestedSimpleRouter(router, r"users", lookup="users") user_nested_router.register("skill", UserSkillViewSet, basename="users-skill") diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index 473737e801..198f40d824 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -28,7 +28,7 @@ RUN ARCH=$(dpkg --print-architecture) && \ # use pipenv to manage virtualenv RUN python -m venv /venv -RUN pip install pipenv +RUN pip install pipenv==2024.2.0 COPY Pipfile Pipfile.lock ./ RUN pipenv install --system --categories "packages dev-packages" diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 8e27664c36..2aef7be6d8 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -41,7 +41,7 @@ RUN ARCH=$(dpkg --print-architecture) && \ # use pipenv to manage virtualenv RUN python -m venv /venv -RUN pip install pipenv +RUN pip install pipenv==2024.2.0 COPY Pipfile Pipfile.lock $APP_HOME RUN pipenv sync --system --categories "packages" From 67c63d93b78b257fb861799d68193583e982067d Mon Sep 17 00:00:00 2001 From: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:04:20 +0530 Subject: [PATCH 23/27] New RBAC Implementation (#2515) RBAC Implementation --- care/security/__init__.py | 0 care/security/apps.py | 11 +++ care/security/authorization/__init__.py | 0 care/security/authorization/base.py | 88 +++++++++++++++++++ care/security/authorization/facility.py | 22 +++++ care/security/management/__init__.py | 0 care/security/management/commands/__init__.py | 0 .../commands/sync_permissions_roles.py | 65 ++++++++++++++ care/security/migrations/0001_initial.py | 88 +++++++++++++++++++ care/security/migrations/__init__.py | 0 care/security/models/__init__.py | 3 + care/security/models/permission.py | 18 ++++ .../security/models/permission_association.py | 22 +++++ care/security/models/role.py | 45 ++++++++++ care/security/permissions/__init__.py | 0 care/security/permissions/base.py | 83 +++++++++++++++++ care/security/permissions/facility.py | 19 ++++ care/security/roles/__init__.py | 0 care/security/roles/role.py | 69 +++++++++++++++ config/settings/base.py | 1 + 20 files changed, 534 insertions(+) create mode 100644 care/security/__init__.py create mode 100644 care/security/apps.py create mode 100644 care/security/authorization/__init__.py create mode 100644 care/security/authorization/base.py create mode 100644 care/security/authorization/facility.py create mode 100644 care/security/management/__init__.py create mode 100644 care/security/management/commands/__init__.py create mode 100644 care/security/management/commands/sync_permissions_roles.py create mode 100644 care/security/migrations/0001_initial.py create mode 100644 care/security/migrations/__init__.py create mode 100644 care/security/models/__init__.py create mode 100644 care/security/models/permission.py create mode 100644 care/security/models/permission_association.py create mode 100644 care/security/models/role.py create mode 100644 care/security/permissions/__init__.py create mode 100644 care/security/permissions/base.py create mode 100644 care/security/permissions/facility.py create mode 100644 care/security/roles/__init__.py create mode 100644 care/security/roles/role.py diff --git a/care/security/__init__.py b/care/security/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/apps.py b/care/security/apps.py new file mode 100644 index 0000000000..9f29b9bd7a --- /dev/null +++ b/care/security/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class SecurityConfig(AppConfig): + name = "care.security" + verbose_name = _("Security Management") + + def ready(self): + # import care.security.signals # noqa F401 + pass diff --git a/care/security/authorization/__init__.py b/care/security/authorization/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/authorization/base.py b/care/security/authorization/base.py new file mode 100644 index 0000000000..36db9c022e --- /dev/null +++ b/care/security/authorization/base.py @@ -0,0 +1,88 @@ +from care.security.permissions.base import PermissionController + + +class PermissionDeniedError(Exception): + pass + + +class AuthorizationHandler: + """ + This is the base class for Authorization Handlers + Authorization handler must define a list of actions that can be performed and define the methods that + actually perform the authorization action. + + All Authz methods would be of the signature ( user, obj , **kwargs ) + obj refers to the obj which the user is seeking permission to. obj can also be a string or any datatype as long + as the logic can handle the type. + + Queries are actions that return a queryset as the response. + """ + + actions = [] + queries = [] + + def check_permission(self, user, obj): + if not PermissionController.has_permission(user, obj): + raise PermissionDeniedError + + return PermissionController.has_permission(user, obj) + + +class AuthorizationController: + """ + This class abstracts all security related operations in care + This includes Checking if A has access to resource X, + Filtering query-sets for list based operations and so on. + Security Controller implicitly caches all cachable operations and expects it to be invalidated. + + SecurityController maintains a list of override Classes, When present, + The override classes are invoked first and then the predefined classes. + The overridden classes can choose to call the next function in the hierarchy if needed. + """ + + override_authz_controllers: list[ + AuthorizationHandler + ] = [] # The order is important + # Override Security Controllers will be defined from plugs + internal_authz_controllers: list[AuthorizationHandler] = [] + + cache = {} + + @classmethod + def build_cache(cls): + for controller in ( + cls.internal_authz_controllers + cls.override_authz_controllers + ): + for action in controller.actions: + if "actions" not in cls.cache: + cls.cache["actions"] = {} + cls.cache["actions"][action] = [ + *cls.cache["actions"].get(action, []), + controller, + ] + + @classmethod + def get_action_controllers(cls, action): + return cls.cache["actions"].get(action, []) + + @classmethod + def check_action_permission(cls, action, user, obj): + """ + TODO: Add Caching and capability to remove cache at both user and obj level + """ + if not cls.cache: + cls.build_cache() + controllers = cls.get_action_controllers(action) + for controller in controllers: + permission_fn = getattr(controller, action) + result, _continue = permission_fn(user, obj) + if not _continue: + return result + if not result: + return result + return True + + @classmethod + def register_internal_controller(cls, controller: AuthorizationHandler): + # TODO : Do some deduplication Logic + cls.internal_authz_controllers.append(controller) diff --git a/care/security/authorization/facility.py b/care/security/authorization/facility.py new file mode 100644 index 0000000000..11de91f2b7 --- /dev/null +++ b/care/security/authorization/facility.py @@ -0,0 +1,22 @@ +from care.abdm.utils.api_call import Facility +from care.facility.models import FacilityUser +from care.security.authorization.base import ( + AuthorizationHandler, + PermissionDeniedError, +) + + +class FacilityAccess(AuthorizationHandler): + actions = ["can_read_facility"] + queries = ["allowed_facilities"] + + def can_read_facility(self, user, facility_id): + self.check_permission(user, facility_id) + # Since the old method relied on a facility-user relationship, check that + # This can be removed when the migrations have been completed + if not FacilityUser.objects.filter(facility_id=facility_id, user=user).exists(): + raise PermissionDeniedError + return True, True + + def allowed_facilities(self, user): + return Facility.objects.filter(users__id__exact=user.id) diff --git a/care/security/management/__init__.py b/care/security/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/management/commands/__init__.py b/care/security/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/management/commands/sync_permissions_roles.py b/care/security/management/commands/sync_permissions_roles.py new file mode 100644 index 0000000000..5d5808e7e7 --- /dev/null +++ b/care/security/management/commands/sync_permissions_roles.py @@ -0,0 +1,65 @@ +from django.core.management import BaseCommand +from django.db import transaction + +from care.security.models import PermissionModel, RoleModel, RolePermission +from care.security.permissions.base import PermissionController +from care.security.roles.role import RoleController +from care.utils.lock import Lock + + +class Command(BaseCommand): + """ + This command syncs roles, permissions and role-permission mapping to the database. + This command should be run after all deployments and plug changes. + This command is idempotent, multiple instances running the same command is automatically blocked with redis. + """ + + help = "Syncs permissions and roles to database" + + def handle(self, *args, **options): + permissions = PermissionController.get_permissions() + roles = RoleController.get_roles() + with transaction.atomic(), Lock("sync_permissions_roles", 900): + # Create, update permissions and delete old permissions + PermissionModel.objects.all().update(temp_deleted=True) + for permission, metadata in permissions.items(): + permission_obj = PermissionModel.objects.filter(slug=permission).first() + if not permission_obj: + permission_obj = PermissionModel(slug=permission) + permission_obj.name = metadata.name + permission_obj.description = metadata.description + permission_obj.context = metadata.context.value + permission_obj.temp_deleted = False + permission_obj.save() + PermissionModel.objects.filter(temp_deleted=True).delete() + # Create, update roles and delete old roles + RoleModel.objects.all().update(temp_deleted=True) + for role in roles: + role_obj = RoleModel.objects.filter( + name=role.name, context=role.context.value + ).first() + if not role_obj: + role_obj = RoleModel(name=role.name, context=role.context.value) + role_obj.description = role.description + role_obj.is_system = True + role_obj.temp_deleted = False + role_obj.save() + RoleModel.objects.filter(temp_deleted=True).delete() + # Sync permissions to role + RolePermission.objects.all().update(temp_deleted=True) + role_cache = {} + for permission, metadata in permissions.items(): + permission_obj = PermissionModel.objects.filter(slug=permission).first() + for role in metadata.roles: + if role.name not in role_cache: + role_cache[role.name] = RoleModel.objects.get(name=role.name) + obj = RolePermission.objects.filter( + role=role_cache[role.name], permission=permission_obj + ).first() + if not obj: + obj = RolePermission( + role=role_cache[role.name], permission=permission_obj + ) + obj.temp_deleted = False + obj.save() + RolePermission.objects.filter(temp_deleted=True).delete() diff --git a/care/security/migrations/0001_initial.py b/care/security/migrations/0001_initial.py new file mode 100644 index 0000000000..37363e6084 --- /dev/null +++ b/care/security/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 5.1.2 on 2024-10-30 10:00 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PermissionModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('slug', models.CharField(db_index=True, max_length=1024, unique=True)), + ('name', models.CharField(max_length=1024)), + ('description', models.TextField(default='')), + ('context', models.CharField(max_length=1024)), + ('temp_deleted', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoleModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('name', models.CharField(max_length=1024)), + ('description', models.TextField(default='')), + ('context', models.CharField(max_length=1024)), + ('is_system', models.BooleanField(default=False)), + ('temp_deleted', models.BooleanField(default=False)), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('name', 'context'), name='unique_order')], + }, + ), + migrations.CreateModel( + name='RoleAssociation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('context', models.CharField(max_length=1024)), + ('context_id', models.BigIntegerField()), + ('expiry', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.rolemodel')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RolePermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('temp_deleted', models.BooleanField(default=False)), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.permissionmodel')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.rolemodel')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/security/migrations/__init__.py b/care/security/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/models/__init__.py b/care/security/models/__init__.py new file mode 100644 index 0000000000..edaf462c2c --- /dev/null +++ b/care/security/models/__init__.py @@ -0,0 +1,3 @@ +from .permission import * # noqa F403 +from .permission_association import * # noqa F403 +from .role import * # noqa F403 diff --git a/care/security/models/permission.py b/care/security/models/permission.py new file mode 100644 index 0000000000..0faed59c6d --- /dev/null +++ b/care/security/models/permission.py @@ -0,0 +1,18 @@ +from django.db import models + +from care.utils.models.base import BaseModel + + +class PermissionModel(BaseModel): + """ + This model represents a permission in the security system. + A permission allows a certain action to be performed by the user for a given context. + """ + + slug = models.CharField(max_length=1024, unique=True, db_index=True) + name = models.CharField(max_length=1024) + description = models.TextField(default="") + context = models.CharField( + max_length=1024 + ) # We can add choices here as well if needed + temp_deleted = models.BooleanField(default=False) diff --git a/care/security/models/permission_association.py b/care/security/models/permission_association.py new file mode 100644 index 0000000000..b8e85fd3b8 --- /dev/null +++ b/care/security/models/permission_association.py @@ -0,0 +1,22 @@ +from django.db import models + +from care.security.models.role import RoleModel +from care.users.models import User +from care.utils.models.base import BaseModel + + +class RoleAssociation(BaseModel): + """ + This model connects roles to users via contexts + Expiry can be used to expire the role allocation after a certain period + """ + + user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) + context = models.CharField(max_length=1024) + context_id = models.BigIntegerField() # Store integer id of the context here + role = models.ForeignKey( + RoleModel, on_delete=models.CASCADE, null=False, blank=False + ) + expiry = models.DateTimeField(null=True, blank=True) + + # TODO : Index user, context and context_id diff --git a/care/security/models/role.py b/care/security/models/role.py new file mode 100644 index 0000000000..471aa44d35 --- /dev/null +++ b/care/security/models/role.py @@ -0,0 +1,45 @@ +from django.db import models +from django.db.models import UniqueConstraint + +from care.security.models.permission import PermissionModel +from care.utils.models.base import BaseModel + + +class RoleModel(BaseModel): + """ + This model represents a role in the security system. + A role comprises multiple permissions on the same type. + A role can only be made for a single context. eg, A role can be FacilityAdmin with Facility related permission items + Another role is to be created for other contexts, eg. Asset Admin should only contain Asset related permission items + Roles can be created on the fly, System roles cannot be deleted, but user created roles can be deleted by users + with the permission to delete roles + """ + + name = models.CharField(max_length=1024) + description = models.TextField(default="") + context = models.CharField( + max_length=1024 + ) # We can add choices here as well if needed + is_system = models.BooleanField( + default=False + ) # Denotes if role was created on the fly + temp_deleted = models.BooleanField(default=False) + + class Meta: + constraints = [ + UniqueConstraint(name="unique_order", fields=["name", "context"]) + ] + + +class RolePermission(BaseModel): + """ + Connects a role to a list of permissions + """ + + role = models.ForeignKey( + RoleModel, on_delete=models.CASCADE, null=False, blank=False + ) + permission = models.ForeignKey( + PermissionModel, on_delete=models.CASCADE, null=False, blank=False + ) + temp_deleted = models.BooleanField(default=False) diff --git a/care/security/permissions/__init__.py b/care/security/permissions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/permissions/base.py b/care/security/permissions/base.py new file mode 100644 index 0000000000..00028e6b39 --- /dev/null +++ b/care/security/permissions/base.py @@ -0,0 +1,83 @@ +import enum +from dataclasses import dataclass + +from care.security.models import RoleAssociation, RolePermission + + +class PermissionContext(enum.Enum): + GENERIC = "GENERIC" + FACILITY = "FACILITY" + ASSET = "ASSET" + LOCATION = "LOCATION" + + +@dataclass +class Permission: + """ + This class abstracts a permission + """ + + name: str + description: str + context: PermissionContext + roles: list + + +class PermissionHandler: + pass + + +from care.security.permissions.facility import FacilityPermissions # noqa: E402 + + +class PermissionController: + """ + This class defines all permissions used within care. + This class is used to abstract all operations related to permissions + """ + + override_permission_handlers = [] + # Override Permission Controllers will be defined from plugs + internal_permission_handlers = [FacilityPermissions] + + cache = {} + + @classmethod + def build_cache(cls): + """ + Iterate through the entire permission library and create a list of permissions and associated Metadata + """ + for handler in ( + cls.internal_permission_handlers + cls.override_permission_handlers + ): + for permission in handler: + cls.cache[permission.name] = permission.value + + @classmethod + def has_permission(cls, user, permission, context, context_id): + # TODO : Cache permissions and invalidate when they change + # TODO : Fetch the user role from the previous role management implementation as well. + # Need to maintain some sort of mapping from previous generation to new generation of roles + from care.security.roles.role import RoleController + + mapped_role = RoleController.map_old_role_to_new(user.role) + permission_roles = RolePermission.objects.filter( + permission__slug=permission, permission__context=context + ).values("role_id") + if RoleAssociation.objects.filter( + context_id=context_id, context=context, role__in=permission_roles, user=user + ).exists(): + return True + # Check for old cases + return RolePermission.objects.filter( + permission__slug=permission, + permission__context=context, + role__name=mapped_role.name, + role__context=mapped_role.context.value, + ).exists() + + @classmethod + def get_permissions(cls): + if not cls.cache: + cls.build_cache() + return cls.cache diff --git a/care/security/permissions/facility.py b/care/security/permissions/facility.py new file mode 100644 index 0000000000..ee1cafbd0e --- /dev/null +++ b/care/security/permissions/facility.py @@ -0,0 +1,19 @@ +import enum + +from care.security.permissions.base import Permission, PermissionContext +from care.security.roles.role import DOCTOR_ROLE, STAFF_ROLE + + +class FacilityPermissions(enum.Enum): + can_read_facility = Permission( + "Can Read on Facility", + "Something Here", + PermissionContext.FACILITY, + [STAFF_ROLE, DOCTOR_ROLE], + ) + can_update_facility = Permission( + "Can Update on Facility", + "Something Here", + PermissionContext.FACILITY, + [STAFF_ROLE], + ) diff --git a/care/security/roles/__init__.py b/care/security/roles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/roles/role.py b/care/security/roles/role.py new file mode 100644 index 0000000000..dac2d8455e --- /dev/null +++ b/care/security/roles/role.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass + +from care.security.permissions.base import PermissionContext + + +@dataclass +class Role: + """ + This class can be inherited for role classes that are created by default + """ + + name: str + description: str + context: PermissionContext + + +DOCTOR_ROLE = Role( + name="Doctor", + description="Some Description Here", + context=PermissionContext.FACILITY, +) # TODO : Clean description +STAFF_ROLE = Role( + name="Staff", + description="Some Description Here", + context=PermissionContext.FACILITY, +) # TODO : Clean description +ADMIN_ROLE = Role( + name="Facility Admin", + description="Some Description Here", + context=PermissionContext.FACILITY, +) # TODO : Clean description + + +class RoleController: + override_roles = [] + # Override Permission Controllers will be defined from plugs + internal_roles = [DOCTOR_ROLE, STAFF_ROLE] + + @classmethod + def get_roles(cls): + return cls.internal_roles + cls.override_roles + + @classmethod + def map_old_role_to_new(cls, old_role): + mapping = { + "Transportation": STAFF_ROLE, + "Pharmacist": STAFF_ROLE, + "Volunteer": STAFF_ROLE, + "StaffReadOnly": STAFF_ROLE, + "Staff": STAFF_ROLE, + "NurseReadOnly": STAFF_ROLE, + "Nurse": STAFF_ROLE, + "Doctor": DOCTOR_ROLE, + "Reserved": DOCTOR_ROLE, + "WardAdmin": STAFF_ROLE, + "LocalBodyAdmin": ADMIN_ROLE, + "DistrictLabAdmin": ADMIN_ROLE, + "DistrictReadOnlyAdmin": ADMIN_ROLE, + "DistrictAdmin": ADMIN_ROLE, + "StateLabAdmin": ADMIN_ROLE, + "StateReadOnlyAdmin": ADMIN_ROLE, + "StateAdmin": ADMIN_ROLE, + } + return mapping[old_role] + + @classmethod + def register_role(cls, role: Role): + # TODO : Do some deduplication Logic + cls.override_roles.append(role) diff --git a/config/settings/base.py b/config/settings/base.py index df20ebb0cf..65e58d235d 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -124,6 +124,7 @@ "healthy_django", ] LOCAL_APPS = [ + "care.security", "care.facility", "care.users", "care.audit_log", From 6143667af11818d8d45efce20073bfb64899a4f9 Mon Sep 17 00:00:00 2001 From: John Lu <87673068+JohnLu2004@users.noreply.github.com> Date: Sat, 2 Nov 2024 02:14:20 -0400 Subject: [PATCH 24/27] Flushed care_static_data prefixed redis keys (#2572) Flushed care_static_data prefixed redis keys (#2572) --------- Co-authored-by: JohnLu2004 Co-authored-by: Aakash Singh --- care/facility/management/commands/load_redis_index.py | 11 +++++++++++ care/facility/tasks/redis_index.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/care/facility/management/commands/load_redis_index.py b/care/facility/management/commands/load_redis_index.py index 736f482836..e24175a8bf 100644 --- a/care/facility/management/commands/load_redis_index.py +++ b/care/facility/management/commands/load_redis_index.py @@ -17,6 +17,17 @@ class Command(BaseCommand): help = "Loads static data to redis" def handle(self, *args, **options): + try: + deleted_count = cache.delete_pattern("care_static_data*", itersize=25_000) + self.stdout.write( + f"Deleted {deleted_count} keys with prefix 'care_static_data'" + ) + except Exception as e: + self.stdout.write( + f"Failed to delete keys with prefix 'care_static_data': {e}" + ) + return + if cache.get("redis_index_loading"): self.stdout.write("Redis Index already loading, skipping") return diff --git a/care/facility/tasks/redis_index.py b/care/facility/tasks/redis_index.py index 306fc1352c..901c1be22d 100644 --- a/care/facility/tasks/redis_index.py +++ b/care/facility/tasks/redis_index.py @@ -15,6 +15,13 @@ @shared_task def load_redis_index(): + try: + deleted_count = cache.delete_pattern("care_static_data*", itersize=25_000) + logger.info("Deleted %s keys with prefix 'care_static_data'", deleted_count) + except Exception as e: + logger.error("Failed to delete keys with prefix 'care_static_data': %s", e) + return + if cache.get("redis_index_loading"): logger.info("Redis Index already loading, skipping") return @@ -37,9 +44,9 @@ def load_redis_index(): if load_static_data: load_static_data() except ModuleNotFoundError: - logger.info("Module %s not found", module_path) + logger.debug("Module %s not found", module_path) except Exception as e: - logger.info("Error loading static data for %s: %s", plug.name, e) + logger.error("Error loading static data for %s: %s", plug.name, e) cache.delete("redis_index_loading") logger.info("Redis Index Loaded") From 39c7aa41abcac031ad4d2c7d22cf6d0428455dc0 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sat, 2 Nov 2024 17:37:49 +0530 Subject: [PATCH 25/27] Fix bug in response --- care/users/api/viewsets/plug_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/users/api/viewsets/plug_config.py b/care/users/api/viewsets/plug_config.py index 40cae848a8..ddfe801353 100644 --- a/care/users/api/viewsets/plug_config.py +++ b/care/users/api/viewsets/plug_config.py @@ -24,7 +24,7 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(self.queryset, many=True) response = serializer.data cache.set(self.cache_key, response) - return Response({"configs": [response]}) + return Response({"configs": response}) def perform_create(self, serializer): cache.delete(self.cache_key) From 8d5e701eacbb5bc4b2a0e91cbc17b6f348a209b2 Mon Sep 17 00:00:00 2001 From: arinak1017 Date: Mon, 4 Nov 2024 05:31:04 -0500 Subject: [PATCH 26/27] Added a settings variable to allow configurable time periods for notification deletion (#2547) Added NOTIFICATION_RETENTION_DAYS settings variable to allow configurable time period with default of 30 days --- care/facility/tasks/cleanup.py | 7 +++++-- .../facility/tests/test_delete_older_notifications_task.py | 7 +++++-- config/settings/base.py | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/care/facility/tasks/cleanup.py b/care/facility/tasks/cleanup.py index 3f913142cc..4fca52eddc 100644 --- a/care/facility/tasks/cleanup.py +++ b/care/facility/tasks/cleanup.py @@ -1,6 +1,7 @@ from datetime import timedelta from celery import shared_task +from django.conf import settings from django.utils import timezone from care.facility.models.notification import Notification @@ -8,5 +9,7 @@ @shared_task def delete_old_notifications(): - ninety_days_ago = timezone.now() - timedelta(days=90) - Notification.objects.filter(created_date__lte=ninety_days_ago).delete() + retention_days = settings.NOTIFICATION_RETENTION_DAYS + + threshold_date = timezone.now() - timedelta(days=retention_days) + Notification.objects.filter(created_date__lte=threshold_date).delete() diff --git a/care/facility/tests/test_delete_older_notifications_task.py b/care/facility/tests/test_delete_older_notifications_task.py index 9f860770b2..70ded42e71 100644 --- a/care/facility/tests/test_delete_older_notifications_task.py +++ b/care/facility/tests/test_delete_older_notifications_task.py @@ -1,5 +1,6 @@ from datetime import timedelta +from django.conf import settings from django.test import TestCase from django.utils import timezone from freezegun import freeze_time @@ -10,8 +11,10 @@ class DeleteOldNotificationsTest(TestCase): def test_delete_old_notifications(self): - # notifications created 90 days ago - with freeze_time(timezone.now() - timedelta(days=90)): + retention_days = settings.NOTIFICATION_RETENTION_DAYS + + # notifications created at the threshold of retention + with freeze_time(timezone.now() - timedelta(days=retention_days)): notification1 = Notification.objects.create() notification2 = Notification.objects.create() diff --git a/config/settings/base.py b/config/settings/base.py index 65e58d235d..6983433f86 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -530,6 +530,7 @@ "VAPID_PRIVATE_KEY", default="7mf3OFreFsgFF4jd8A71ZGdVaj8kpJdOto4cFbfAS-s" ) SEND_SMS_NOTIFICATION = False +NOTIFICATION_RETENTION_DAYS = env.int("NOTIFICATION_RETENTION_DAYS", default=30) # Cloud and Buckets # ------------------------------------------------------------------------------ From 40d94c5ea064b49aae49f8a359110acb3b655d53 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Tue, 5 Nov 2024 10:45:47 +0530 Subject: [PATCH 27/27] Fix Bug in plugin config --- care/users/api/viewsets/plug_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/care/users/api/viewsets/plug_config.py b/care/users/api/viewsets/plug_config.py index ddfe801353..faeb475dcc 100644 --- a/care/users/api/viewsets/plug_config.py +++ b/care/users/api/viewsets/plug_config.py @@ -15,7 +15,6 @@ class PlugConfigViewset( serializer_class = PLugConfigSerializer queryset = PlugConfig.objects.all().order_by("slug") cache_key = "care_plug_viewset_list" - authentication_classes = [] def list(self, request, *args, **kwargs): # Cache data and return